From 9109f90021979b0b869ef46b455a0d6dac447f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Sat, 29 Aug 2026 13:10:17 +0200 Subject: [PATCH 01/26] feat(web): swipe-open mobile sidebar, configurable APP_NAME, OG tagline --- .env.example | 3 + apps/web/.env.example | 4 + apps/web/src/app/(auth)/login/page.tsx | 3 +- .../(workspace)/[backend]/[[...ids]]/page.tsx | 2 + apps/web/src/app/api/og/route.tsx | 17 +- apps/web/src/app/layout.tsx | 2 +- apps/web/src/app/manifest.ts | 3 + apps/web/src/common/hooks/useMediaQuery.ts | 27 ++ apps/web/src/components/common/Icon.tsx | 2 + apps/web/src/components/forms/LoginForm.tsx | 9 +- .../src/components/layouts/AccountSidebar.tsx | 336 +---------------- .../layouts/AccountSidebarContent.tsx | 346 ++++++++++++++++++ .../components/layouts/WorkspaceClient.tsx | 38 +- .../layouts/mobile/MenuHintModal.tsx | 36 ++ .../layouts/mobile/MobileDrawer.tsx | 143 ++++++++ .../components/layouts/mobile/MobileMenu.tsx | 63 ++++ apps/web/src/configs/app/app.server-config.ts | 6 + apps/web/src/configs/seo/metadata.config.ts | 10 +- docker-compose.yml | 1 + 19 files changed, 693 insertions(+), 358 deletions(-) create mode 100644 apps/web/src/common/hooks/useMediaQuery.ts create mode 100644 apps/web/src/components/layouts/AccountSidebarContent.tsx create mode 100644 apps/web/src/components/layouts/mobile/MenuHintModal.tsx create mode 100644 apps/web/src/components/layouts/mobile/MobileDrawer.tsx create mode 100644 apps/web/src/components/layouts/mobile/MobileMenu.tsx diff --git a/.env.example b/.env.example index c6beb12..69f58c6 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,9 @@ NEXT_PUBLIC_API_URL="http://localhost:4000/api/v1" # Security COOKIE_DOMAIN="localhost" +# Optional: display name of this instance (web, runtime); defaults to "DropTo" when empty. +APP_NAME="DropTo" + # Web build args NEXT_PUBLIC_GOOGLE_API_KEY="your_google_picker_api_key" NEXT_PUBLIC_GOOGLE_APP_ID="your_google_cloud_project_number" diff --git a/apps/web/.env.example b/apps/web/.env.example index 673bd4c..21ed9bf 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -5,6 +5,10 @@ NEXT_PUBLIC_API_URL="http://localhost:4000/api/v1" # In a compose/prod deploy, point this at the API's internal address, e.g. http://dropto-api:4000/api/v1 # API_URL="http://localhost:4000/api/v1" +# Branding (optional) — display name of this instance; defaults to "DropTo" when empty. +# Server-side and read at runtime (not a NEXT_PUBLIC_*), so changing it needs no rebuild. +APP_NAME="DropTo" + # Security COOKIE_DOMAIN="localhost" diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index 8cea774..b9102c0 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import LoginForm from '@/components/forms/LoginForm'; +import { appServerConfig } from '@/configs/app/app.server-config'; export const metadata: Metadata = { title: 'Sign in', @@ -10,5 +11,5 @@ export const metadata: Metadata = { * Login route — the proxy redirects already-authenticated visitors to `/`. **/ export default function LoginPage() { - return ; + return ; } diff --git a/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx b/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx index df68647..0978a91 100644 --- a/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx +++ b/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx @@ -8,6 +8,7 @@ import type { Crumb } from '@/common/types/workspace.types'; import { slugify } from '@/common/utils/storage-url'; import NotFoundContent from '@/components/layouts/NotFoundContent'; import WorkspaceClient from '@/components/layouts/WorkspaceClient'; +import { appServerConfig } from '@/configs/app/app.server-config'; export const metadata: Metadata = { title: 'Workspace', @@ -72,6 +73,7 @@ export default async function WorkspaceBrowsePage({ params }: Props) { return ( { } /** - * OG image (`GET /api/og`) — the DropTo badge + wordmark on a light zinc background. + * OG image (`GET /api/og`) — the badge, the wordmark and the tagline on a light zinc background. **/ export async function GET() { - const poppins = await loadPoppins(600, 'DropTo'); + const poppins = await loadPoppins(600, `${wordmark}${subtitle}`); return new ImageResponse(
- DropTo + {wordmark} +
+ +
+ {subtitle}
, diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 5117bf7..fc9b5dd 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -65,7 +65,7 @@ export default function RootLayout({ children }: Readonly) { return ( - + void) => { + const media = window.matchMedia(query); + + media.addEventListener('change', onChange); + + return () => media.removeEventListener('change', onChange); + }, + [query], + ); + + return useSyncExternalStore( + subscribe, + () => window.matchMedia(query).matches, + () => false, + ); +} diff --git a/apps/web/src/components/common/Icon.tsx b/apps/web/src/components/common/Icon.tsx index 0a77f48..2ea4130 100644 --- a/apps/web/src/components/common/Icon.tsx +++ b/apps/web/src/components/common/Icon.tsx @@ -2,6 +2,7 @@ import { Archive, + ArrowLeftRight, BadgeCheck, Check, ChevronDown, @@ -58,6 +59,7 @@ const ICONS: Record = { ArrowTopRightOnSquare: ExternalLink, ArrowUpTray: Upload, ArrowsPointingIn: Move, + ArrowsRightLeft: ArrowLeftRight, Check: Check, CheckBadge: BadgeCheck, ChevronDown, diff --git a/apps/web/src/components/forms/LoginForm.tsx b/apps/web/src/components/forms/LoginForm.tsx index 03be01a..de5f973 100644 --- a/apps/web/src/components/forms/LoginForm.tsx +++ b/apps/web/src/components/forms/LoginForm.tsx @@ -10,10 +10,15 @@ import Icon from '@/components/common/Icon'; import Input from '@/components/common/Input'; import { useToast } from '@/components/providers/ToastProvider'; +interface Props { + /** Instance name, resolved on the server so the wordmark hydrates identically. */ + appName: string; +} + /** * Login screen — posts credentials via the `login` action, then navigates to `/`. **/ -export default function LoginForm() { +export default function LoginForm({ appName }: Props) { const router = useRouter(); const toast = useToast(); @@ -59,7 +64,7 @@ export default function LoginForm() {
-

DropTo

+

{appName}

Sign in to continue

diff --git a/apps/web/src/components/layouts/AccountSidebar.tsx b/apps/web/src/components/layouts/AccountSidebar.tsx index 5740c1f..084e21d 100644 --- a/apps/web/src/components/layouts/AccountSidebar.tsx +++ b/apps/web/src/components/layouts/AccountSidebar.tsx @@ -1,343 +1,15 @@ 'use client'; -import type { StorageBackend, StorageStatus } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; -import Image from 'next/image'; -import { type ReactNode, useEffect, useState } from 'react'; - -import { getGoogleAuthUrl } from '@/common/services/api/auth.client'; -import { formatBytes } from '@/common/utils/format.functions'; -import Button from '@/components/common/Button'; -import Icon from '@/components/common/Icon'; -import ThemeToggle from '@/components/common/ThemeToggle'; -import LoadingIndicator from '@/components/loadings/LoadingIndicator'; -import { STORAGE_ICON } from '@/configs/storage.config'; - -interface Props { - statuses: StorageStatus[]; - driveStatus: StorageStatus | null; - activeBackend: StorageBackend | null; - loading: boolean; - username: string; - saving: boolean; - isOwner: boolean; - onSelectStorage: (backend: StorageBackend) => void; - onManageFolders: () => void; - onDisconnect: () => void; - onLogout: () => void; - loggingOut: boolean; -} - -function SectionLabel({ children }: { children: ReactNode }) { - return ( -

- {children} -

- ); -} - -/** - * A storage-usage bar (used of total), colored green → amber (≥ 75%) → red (> 95%). - **/ -function StorageMeter({ usage, limit }: { usage: number; limit: number }) { - const pct = limit > 0 ? Math.min(100, Math.round((usage / limit) * 100)) : 0; - const color = pct > 95 ? 'bg-red-500' : pct >= 75 ? 'bg-amber-500' : 'bg-green-600'; - - return ( -
-
-
-
-

- {formatBytes(usage, 2)} of {formatBytes(limit, 2)} used -

-
- ); -} - -/** Directions (in px) the sparks fly out to on a click. */ -const SPARKS = [ - { x: -14, y: -22 }, - { x: -6, y: -34 }, - { x: 14, y: -34 }, - { x: 30, y: -20 }, - { x: 36, y: 2 }, - { x: -12, y: 14 }, -]; - -/** - * Easter egg: the session avatar. Clicking it shouts an ever-longer "Pikachuuuu!" and throws a few sparks around. - **/ -function PikachuAvatar({ username }: { username: string }) { - const [burst, setBurst] = useState<{ id: number; clicks: number } | null>(null); - - useEffect(() => { - if (!burst) return; - - const timeout = setTimeout(() => setBurst(null), 1400); - - return () => clearTimeout(timeout); - }, [burst]); - - return ( -
- - {burst && ( - - Pikach{'u'.repeat(Math.min(2 + burst.clicks, 12))}! - - )} - - - - {burst && - SPARKS.map((spark, index) => ( - - ⚡ - - ))} - - - - setBurst((prev) => ({ id: (prev?.id ?? 0) + 1, clicks: prev ? prev.clicks + 1 : 0 })) - } - whileHover={{ rotate: [0, -12, 12, -8, 8, 0], scale: 1.1 }} - whileTap={{ scale: 0.85 }} - animate={{ - boxShadow: burst ? '0 0 0 4px rgba(250,204,21,0.45)' : '0 0 0 0px rgba(250,204,21,0)', - }} - transition={{ duration: 0.6 }} - title='Pika pika!' - className='inline-flex h-8 w-8 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-green-600/15'> - {username} - -
- ); -} +import AccountSidebarContent, { type AccountSidebarProps } from './AccountSidebarContent'; /** * Left pane: Google account + S3 status, storage switcher, and the session footer. **/ -export default function AccountSidebar({ - statuses, - driveStatus, - activeBackend, - loading, - username, - saving, - isOwner, - onSelectStorage, - onManageFolders, - onDisconnect, - onLogout, - loggingOut, -}: Props) { - const s3Status = statuses.find((status) => status.backend === 's3') ?? null; - const connectedStorages = statuses.filter((status) => status.connected); - +// Only the desktop shell — below `md` the same body is served by the swipe-open `MobileDrawer`. +export default function AccountSidebar(props: AccountSidebarProps) { return ( ); } diff --git a/apps/web/src/components/layouts/AccountSidebarContent.tsx b/apps/web/src/components/layouts/AccountSidebarContent.tsx new file mode 100644 index 0000000..2147304 --- /dev/null +++ b/apps/web/src/components/layouts/AccountSidebarContent.tsx @@ -0,0 +1,346 @@ +'use client'; + +import type { StorageBackend, StorageStatus } from '@dropto/types'; +import { AnimatePresence, motion } from 'framer-motion'; +import Image from 'next/image'; +import { type ReactNode, useEffect, useState } from 'react'; + +import { getGoogleAuthUrl } from '@/common/services/api/auth.client'; +import { formatBytes } from '@/common/utils/format.functions'; +import Button from '@/components/common/Button'; +import Icon from '@/components/common/Icon'; +import ThemeToggle from '@/components/common/ThemeToggle'; +import LoadingIndicator from '@/components/loadings/LoadingIndicator'; +import { STORAGE_ICON } from '@/configs/storage.config'; + +export interface AccountSidebarProps { + /** Instance name, resolved on the server so the wordmark hydrates identically. */ + appName: string; + statuses: StorageStatus[]; + driveStatus: StorageStatus | null; + activeBackend: StorageBackend | null; + loading: boolean; + username: string; + saving: boolean; + isOwner: boolean; + onSelectStorage: (backend: StorageBackend) => void; + onManageFolders: () => void; + onDisconnect: () => void; + onLogout: () => void; + loggingOut: boolean; +} + +function SectionLabel({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +/** + * A storage-usage bar (used of total), colored green → amber (≥ 75%) → red (> 95%). + **/ +function StorageMeter({ usage, limit }: { usage: number; limit: number }) { + const pct = limit > 0 ? Math.min(100, Math.round((usage / limit) * 100)) : 0; + const color = pct > 95 ? 'bg-red-500' : pct >= 75 ? 'bg-amber-500' : 'bg-green-600'; + + return ( +
+
+
+
+

+ {formatBytes(usage, 2)} of {formatBytes(limit, 2)} used +

+
+ ); +} + +/** Directions (in px) the sparks fly out to on a click. */ +const SPARKS = [ + { x: -14, y: -22 }, + { x: -6, y: -34 }, + { x: 14, y: -34 }, + { x: 30, y: -20 }, + { x: 36, y: 2 }, + { x: -12, y: 14 }, +]; + +/** + * Easter egg: the session avatar. Clicking it shouts an ever-longer "Pikachuuuu!" and throws a few sparks around. + **/ +function PikachuAvatar({ username }: { username: string }) { + const [burst, setBurst] = useState<{ id: number; clicks: number } | null>(null); + + useEffect(() => { + if (!burst) return; + + const timeout = setTimeout(() => setBurst(null), 1400); + + return () => clearTimeout(timeout); + }, [burst]); + + return ( +
+ + {burst && ( + + Pikach{'u'.repeat(Math.min(2 + burst.clicks, 12))}! + + )} + + + + {burst && + SPARKS.map((spark, index) => ( + + ⚡ + + ))} + + + + setBurst((prev) => ({ id: (prev?.id ?? 0) + 1, clicks: prev ? prev.clicks + 1 : 0 })) + } + whileHover={{ rotate: [0, -12, 12, -8, 8, 0], scale: 1.1 }} + whileTap={{ scale: 0.85 }} + animate={{ + boxShadow: burst ? '0 0 0 4px rgba(250,204,21,0.45)' : '0 0 0 0px rgba(250,204,21,0)', + }} + transition={{ duration: 0.6 }} + title='Pika pika!' + className='inline-flex h-8 w-8 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-green-600/15'> + {username} + +
+ ); +} + +/** + * The sidebar body — shared by the desktop rail (`AccountSidebar`) and the mobile drawer. + **/ +export default function AccountSidebarContent({ + appName, + statuses, + driveStatus, + activeBackend, + loading, + username, + saving, + isOwner, + onSelectStorage, + onManageFolders, + onDisconnect, + onLogout, + loggingOut, +}: AccountSidebarProps) { + const s3Status = statuses.find((status) => status.backend === 's3') ?? null; + const connectedStorages = statuses.filter((status) => status.connected); + + return ( + <> +
+
+ +
+
+

{appName}

+

+ Storage workspace +

+
+
+ +
+
+ Google account + + {loading ? ( +
+ + Loading… +
+ ) : !driveStatus?.connected ? ( +
+

+ {driveStatus?.error ?? + 'No account connected. Connect a Google account to pick folders and upload.'} +

+ + + {driveStatus?.error ? 'Reconnect Drive' : 'Connect Drive'} + +
+ ) : ( +
+
+
+ +
+
+

{driveStatus.email}

+

Connected

+
+
+ + {driveStatus.quota && driveStatus.quota.limit !== null && ( + + )} + + {isOwner ? ( +
+ + +
+ ) : ( + + + Verify with Google to manage + + )} +
+ )} +
+ +
+ S3 storage + + {loading ? ( +
+ + Loading… +
+ ) : !s3Status?.connected ? ( +
+

+ {s3Status?.error ? 'S3 storage unavailable' : 'No S3 storage connected'} +

+

+ {s3Status?.error ?? + 'Enable and configure S3 in the API environment to browse buckets.'} +

+
+ ) : ( +
+
+ +
+
+

+ {s3Status.roots.length} bucket{s3Status.roots.length === 1 ? '' : 's'} +

+

Connected

+
+
+ )} +
+ + {!loading && connectedStorages.length > 0 && ( +
+ Browse + +
+ )} +
+ +
+
+ + {username} +
+
+ + +
+
+ + ); +} diff --git a/apps/web/src/components/layouts/WorkspaceClient.tsx b/apps/web/src/components/layouts/WorkspaceClient.tsx index 852ff5c..85f28a7 100644 --- a/apps/web/src/components/layouts/WorkspaceClient.tsx +++ b/apps/web/src/components/layouts/WorkspaceClient.tsx @@ -56,7 +56,9 @@ import { UploadProvider, useUploadActions } from '@/components/providers/UploadP import { STORAGE_ICON } from '@/configs/storage.config'; import AccountSidebar from './AccountSidebar'; +import type { AccountSidebarProps } from './AccountSidebarContent'; import FileBrowser from './FileBrowser'; +import MobileMenu from './mobile/MobileMenu'; import PreviewPanel from './PreviewPanel'; import UploadDock from './UploadDock'; @@ -64,6 +66,8 @@ const FOLDER_MIME = 'application/vnd.google-apps.folder'; const DOWNLOAD_PREPARING_MS = 6000; interface Props { + /** Instance name, resolved on the server so the wordmark hydrates identically. */ + appName: string; username: string; /** Statuses fetched server-side, for first paint. */ initialStatuses: StorageStatus[]; @@ -108,6 +112,7 @@ interface ExtensionWarning { * Main workspace: sidebar + file browser + preview, with uploads and account management. **/ function WorkspaceInner({ + appName, username, initialStatuses, initialBackend, @@ -1205,22 +1210,27 @@ function WorkspaceInner({ const rootIcon = activeBackend ? STORAGE_ICON[activeBackend] : 'Home'; const bulkCount = bulkPane === 0 ? selectedIds.size : paneB.selectedIds.size; + // One set of props for both sidebar shells: the desktop rail and the mobile swipe-open drawer. + const sidebarProps: AccountSidebarProps = { + appName, + statuses, + driveStatus, + activeBackend, + loading: loadingStatus, + username, + saving, + isOwner: driveStatus?.isOwner ?? false, + onSelectStorage: selectStorage, + onManageFolders: handleManageFolders, + onDisconnect: handleDisconnect, + onLogout: handleLogout, + loggingOut, + }; + return (
- + +
{/* Both panes share a grid whose second column animates 1fr↔0fr for a smooth resize. */} diff --git a/apps/web/src/components/layouts/mobile/MenuHintModal.tsx b/apps/web/src/components/layouts/mobile/MenuHintModal.tsx new file mode 100644 index 0000000..513e5f1 --- /dev/null +++ b/apps/web/src/components/layouts/mobile/MenuHintModal.tsx @@ -0,0 +1,36 @@ +'use client'; + +import Button from '@/components/common/Button'; +import Icon from '@/components/common/Icon'; +import Modal from '@/components/common/Modal'; + +interface Props { + open: boolean; + onDismiss: () => void; +} + +/** + * First-visit explanation of the swipe-open menu — there is no hamburger to discover. + **/ +export default function MenuHintModal({ open, onDismiss }: Props) { + return ( + +
+ + + + +

The menu is a swipe away

+ +

+ Swipe right anywhere on the page to open your storage menu, then pick what you need. Swipe + left or tap outside to close it again. +

+ + +
+
+ ); +} diff --git a/apps/web/src/components/layouts/mobile/MobileDrawer.tsx b/apps/web/src/components/layouts/mobile/MobileDrawer.tsx new file mode 100644 index 0000000..95944bc --- /dev/null +++ b/apps/web/src/components/layouts/mobile/MobileDrawer.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { AnimatePresence, motion } from 'framer-motion'; +import { useEffect } from 'react'; + +import AccountSidebarContent, { type AccountSidebarProps } from '../AccountSidebarContent'; + +interface Props { + sidebar: AccountSidebarProps; + open: boolean; + onOpen: () => void; + onClose: () => void; +} + +/** How far a horizontal drag has to travel to count as a swipe. */ +const SWIPE_DISTANCE = 70; + +/** How much more horizontal than vertical it has to be, so scrolling never opens the drawer. */ +const SWIPE_RATIO = 1.5; + +/** + * Tells whether a touch started inside something that scrolls sideways. + **/ +// Those elements own their horizontal gestures — a long file name is read by dragging it. +const startedInScroller = (target: EventTarget | null): boolean => { + let node = target instanceof Element ? target : null; + + while (node) { + if (node.scrollWidth > node.clientWidth) { + const { overflowX } = getComputedStyle(node); + + if (overflowX === 'auto' || overflowX === 'scroll') return true; + } + + node = node.parentElement; + } + + return false; +}; + +/** + * The swipe-open account sidebar shown where the desktop rail does not fit. + **/ +export default function MobileDrawer({ sidebar, open, onOpen, onClose }: Props) { + useEffect(() => { + let startX = 0; + let startY = 0; + let tracking = false; + + const onTouchStart = (event: TouchEvent) => { + const touch = event.touches[0]; + + startX = touch.clientX; + startY = touch.clientY; + tracking = !startedInScroller(event.target); + }; + + const onTouchEnd = (event: TouchEvent) => { + if (!tracking) return; + + const touch = event.changedTouches[0]; + const deltaX = touch.clientX - startX; + const deltaY = touch.clientY - startY; + + if (Math.abs(deltaX) < SWIPE_DISTANCE) return; + if (Math.abs(deltaX) < Math.abs(deltaY) * SWIPE_RATIO) return; + + if (deltaX > 0) { + onOpen(); + } else { + onClose(); + } + }; + + window.addEventListener('touchstart', onTouchStart, { passive: true }); + window.addEventListener('touchend', onTouchEnd, { passive: true }); + + return () => { + window.removeEventListener('touchstart', onTouchStart); + window.removeEventListener('touchend', onTouchEnd); + }; + }, [onOpen, onClose]); + + useEffect(() => { + if (!open) return; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + + // The page behind must not scroll while the drawer covers it. + const { overflow } = document.body.style; + document.body.style.overflow = 'hidden'; + window.addEventListener('keydown', onKeyDown); + + return () => { + document.body.style.overflow = overflow; + window.removeEventListener('keydown', onKeyDown); + }; + }, [open, onClose]); + + return ( + + {open && ( +
+ + + + {/* Each of these navigates or opens a dialog, so the drawer gets out of the way. */} + { + sidebar.onSelectStorage(backend); + onClose(); + }} + onManageFolders={() => { + sidebar.onManageFolders(); + onClose(); + }} + onDisconnect={() => { + sidebar.onDisconnect(); + onClose(); + }} + /> + +
+ )} +
+ ); +} diff --git a/apps/web/src/components/layouts/mobile/MobileMenu.tsx b/apps/web/src/components/layouts/mobile/MobileMenu.tsx new file mode 100644 index 0000000..3706d33 --- /dev/null +++ b/apps/web/src/components/layouts/mobile/MobileMenu.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; + +import { useMediaQuery } from '@/common/hooks/useMediaQuery'; + +import type { AccountSidebarProps } from '../AccountSidebarContent'; +import MenuHintModal from './MenuHintModal'; +import MobileDrawer from './MobileDrawer'; + +/** Remembers that the swipe hint has been acknowledged. */ +const HINT_STORAGE_KEY = 'dropto:menu-hint-seen'; + +/** The breakpoint at which the sidebar rail appears, so the drawer and its hint are pointless. */ +const SIDEBAR_QUERY = '(min-width: 48rem)'; + +/** + * Owns the mobile sidebar drawer and its first-visit swipe hint. + **/ +// The media query is the single gate: below `md` there is no rail, so the sidebar is a gesture. +// Above it the drawer, the swipe listener and the hint are never mounted at all. +export default function MobileMenu(sidebar: AccountSidebarProps) { + const hasSidebar = useMediaQuery(SIDEBAR_QUERY); + const [menuOpen, setMenuOpen] = useState(false); + const [hintOpen, setHintOpen] = useState(false); + + const openMenu = useCallback(() => setMenuOpen(true), []); + const closeMenu = useCallback(() => setMenuOpen(false), []); + + // First visit on a device without the rail: nobody would guess the swipe on their own. + useEffect(() => { + if (hasSidebar) return; + + try { + if (localStorage.getItem(HINT_STORAGE_KEY)) return; + } catch { + // Storage blocked (private mode, disabled cookies) — show the hint rather than crash. + } + + setHintOpen(true); + }, [hasSidebar]); + + const dismissHint = useCallback(() => { + setHintOpen(false); + + try { + localStorage.setItem(HINT_STORAGE_KEY, '1'); + } catch { + // Storage blocked — the hint simply returns on the next visit. + } + }, []); + + if (hasSidebar) { + return null; + } + + return ( + <> + + + + ); +} diff --git a/apps/web/src/configs/app/app.server-config.ts b/apps/web/src/configs/app/app.server-config.ts index d0df7c5..9800ec7 100644 --- a/apps/web/src/configs/app/app.server-config.ts +++ b/apps/web/src/configs/app/app.server-config.ts @@ -1,6 +1,8 @@ import 'server-only'; interface AppServerConfig { + /** Display name of this instance — the wordmarks, the titles, the manifest and the OG image. */ + name: string; nodeEnv: { isProduction: boolean; }; @@ -15,7 +17,11 @@ interface AppServerConfig { } // Server-side config. apiUrl prefers a server-only API_URL, else the public one. +// `name` is read here (server, per request) and handed to client components as a prop — never a +// NEXT_PUBLIC_*, which the client inlines at build while the server reads it at runtime: if the two +// values disagree the wordmark hydrates with a mismatch. export const appServerConfig: AppServerConfig = { + name: process.env.APP_NAME?.trim() || 'DropTo', nodeEnv: { isProduction: process.env.NODE_ENV === 'production', }, diff --git a/apps/web/src/configs/seo/metadata.config.ts b/apps/web/src/configs/seo/metadata.config.ts index c06d761..f607c47 100644 --- a/apps/web/src/configs/seo/metadata.config.ts +++ b/apps/web/src/configs/seo/metadata.config.ts @@ -1,3 +1,5 @@ +import { appServerConfig } from '@/configs/app/app.server-config'; + interface MetadataConfig { title: string; shortTitle: string; @@ -11,15 +13,15 @@ interface MetadataConfig { }; } -// Static site metadata for the layout, manifest and robots. +// Site metadata for the layout, manifest, robots and the OG image (name from the env). export const metadataConfig: MetadataConfig = { - title: 'DropTo', - shortTitle: 'DropTo', + title: appServerConfig.name, + shortTitle: appServerConfig.name, tagline: 'Self-hosted file uploader', description: 'Self-hosted file uploader that streams your uploads straight into your own storage — Google Drive or S3-compatible buckets — from a fast, Finder-like workspace.', keywords: [ - 'DropTo', + appServerConfig.name, 'self-hosted file uploader', 'file uploader', 'file manager', diff --git a/docker-compose.yml b/docker-compose.yml index b0b8c5e..7248110 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -88,6 +88,7 @@ services: environment: NODE_ENV: 'production' PORT: '3000' + APP_NAME: '${APP_NAME:-DropTo}' API_URL: 'http://dropto-api:4000/api/v1' COOKIE_DOMAIN: '${COOKIE_DOMAIN:-localhost}' ports: From f075ef602fe89e9dd89769153b8439c4fda79acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Sun, 30 Aug 2026 11:01:38 +0200 Subject: [PATCH 02/26] feat: one-command Docker deployment, rewritten README, JavaScript-required screen --- .dockerignore | 22 ++- .env.example | 26 +++- README.md | 267 +++++++++++++++++++++++++++-------- apps/api/Dockerfile | 21 ++- apps/web/Dockerfile | 18 ++- apps/web/src/app/globals.css | 46 ++++++ apps/web/src/app/layout.tsx | 26 +++- docker-compose.infra.yml | 10 +- docker-compose.yml | 98 +++++++++---- package.json | 2 +- 10 files changed, 429 insertions(+), 107 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3541a86..c3672ef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,22 +1,32 @@ # deps **/node_modules +.pnpm-store # builds apps/api/dist -apps/cdn/dist -apps/socials/.next -apps/api-assets/.next +apps/web/.next +**/out +**/build +**/*.tsbuildinfo # turbo **/.turbo -**/tsconfig.build.tsbuildinfo -# env +# env — the images ship only the examples (the API validates its env against .env.example) **/.env **/.env.* !**/.env.example +# vcs / tooling +.git +.github +.husky +.claude + +# reference material (not part of the app) +example + # misc **/coverage **/*.md -**/.claude +**/.DS_Store diff --git a/.env.example b/.env.example index 69f58c6..bb11377 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,44 @@ +# Root environment — infrastructure + the web app (docker compose reads this file). +# The API's own secrets live in apps/api/.env (see apps/api/.env.example). +# +# cp .env.example .env + # Database (PostgreSQL) +# In the full compose deployment these values create the database AND are handed to the API, +# so this file is the single source of truth for them — no need to repeat them in apps/api/.env. DB_HOST="localhost" DB_PORT=5432 DB_NAME="dropto" DB_USER="root" DB_PASS="password" -# Redis +# Redis (rate limiter) REDIS_PORT=6379 +REDIS_USER="default" REDIS_PASS="password" +REDIS_KEY_PREFIX="dropto:" # URLs +# APP_URL is the public origin of the web app. It is baked in at BUILD time (it feeds the +# canonical / OpenGraph / manifest URLs), so keep the runtime value identical. APP_URL="http://localhost:3000" +# Public API base, used by the browser only for the Google OAuth redirect — so it must be an +# address the operator's browser can reach, not a container name. NEXT_PUBLIC_API_URL="http://localhost:4000/api/v1" # Security +# Auth-cookie domain. Use a shared parent in production (e.g. ".example.com") so the web app and +# the API can both read the cookies. COOKIE_DOMAIN="localhost" -# Optional: display name of this instance (web, runtime); defaults to "DropTo" when empty. +# Branding (optional) — display name of this instance; defaults to "DropTo" when empty. +# Read on the server at runtime, so renaming needs no rebuild. APP_NAME="DropTo" -# Web build args +# Web build args — inlined into the browser bundle at build time. NEXT_PUBLIC_GOOGLE_API_KEY="your_google_picker_api_key" NEXT_PUBLIC_GOOGLE_APP_ID="your_google_cloud_project_number" + +# Host ports the containers publish on, bound to 127.0.0.1 (docker compose only). +WEB_PORT=3000 +API_PORT=4000 diff --git a/README.md b/README.md index 26203db..5a94e32 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,244 @@ # DropTo -> Self-hosted file uploader that lets you route uploads through your own storage backends (Google Drive, S3, and more) instead of your app server's disk. - -- [Prerequisites](#Prerequisites) -- [Dependencies](#Dependencies) -- [Projects](#Projects) -- [Installation](#Installation) -- [Run](#Run) - - [Docker](#Docker) -- [Configuration](#Configuration) -- [Deployment](#Deployment) -- [License](#License) +> Self-hosted file uploader that routes uploads through **your own** storage backends — Google Drive +> or any S3-compatible bucket — instead of your app server's disk. + +- [Prerequisites](#prerequisites) +- [Features](#features) +- [Projects](#projects) +- [Installation](#installation) +- [Run](#run) +- [Docker](#docker) +- [Configuration](#configuration) +- [Connecting storage](#connecting-storage) +- [Deployment](#deployment) +- [License](#license) ## Prerequisites -- Node.js 22+ ([Download](https://nodejs.org/en/download/)) -- Container runtime ([Docker (recommended)](https://www.docker.com/), Colima, ...) -- IDE ([VS Code](https://code.visualstudio.com/), WebStorm, ...) +- Node.js 22+ ([Download](https://nodejs.org/en/download/)) — only for running the apps natively - Package manager ([pnpm (recommended)](https://pnpm.io/installation), npm, ...) - -## Dependencies - -- **Postgres** _(started automatically via docker compose)_ +- [Docker](https://docs.docker.com/get-started/get-docker/) (Docker Desktop, Colima, ...) — required + for the database, and the simplest way to run the whole stack +- IDE ([VS Code](https://code.visualstudio.com/), WebStorm, ...) +- A Google Cloud project (to connect a Google Drive) and/or S3 credentials — see + [Connecting storage](#connecting-storage) + +## Features + +- **Your storage, not the server's disk** — uploads are streamed straight through to Google Drive or + an S3-compatible bucket (AWS S3, MinIO, Cloudflare R2, ...); nothing is buffered on disk +- **Finder-like workspace** — a three-pane browser (storage sidebar, file list, preview) with a + split view, sorting, search, breadcrumbs, deep links and image previews +- **Drag & drop uploads** — whole folder trees, with duplicate handling (replace / keep both / + cancel), per-file progress and ETA in a floating upload dock, and abortable transfers sized by + `MAX_UPLOAD_BYTES` +- **Storage switcher** — browse Google Drive and S3 side by side and move between them in one click +- **Scoped Drive access** — the OAuth scope is `drive.file` only; the owner authorizes specific + folders through the Google Picker and every call is validated server-side to stay inside that tree +- **Single-operator auth** — username/password from the env, JWT access tokens plus rotating, + DB-backed refresh tokens in httpOnly cookies, with reuse detection +- **Secrets stay encrypted** — the Drive refresh token is AES-256-GCM encrypted in Postgres and the + browser never talks to Google (or the API) directly; everything goes through the Next.js BFF +- **Hardened by default** — a strict CSP and security headers, an IP-keyed rate limiter, read-only + containers running as a non-root user +- **Rebrandable** — set `APP_NAME` and the whole app, its titles and its link previews follow ## Projects -| Name | Description | -| ------------------ | ----------------------------------------------------- | -| [web](./apps/web/) | Next.js front-end — setup page + uploader UI | -| [api](./apps/api/) | NestJS API — Google OAuth, Drive integration, uploads | +| Name | Description | +| -------------------------- | ------------------------------------------------------------------------ | +| [web](./apps/web/) | Next.js front-end — login, workspace, Google Picker setup; the BFF layer | +| [api](./apps/api/) | NestJS API — operator auth, Google OAuth, storage providers, uploads | +| [types](./packages/types/) | `@dropto/types` — the shared API contract between the two | ## Installation 1. Clone the repository and navigate to the root: `cd drop-to/` 2. Install all dependencies: `pnpm install` -3. Copy `.env.example` to `.env` in the monorepo root and in each app directory (`apps/web/`, `apps/api/`) and adjust the values accordingly. -4. Start required services: `docker compose -f docker-compose.infra.yml up -d` - > Run all docker compose commands from the monorepo root. -5. **First run only:** Apply database migrations and generate Prisma client: `pnpm --filter api prisma:migrate && pnpm --filter api prisma:generate` -6. Start all services (see [Run](#Run) section). +3. Create the env files and adjust the values: + +```bash +cp .env.example .env # infrastructure + web (docker compose reads this) +cp apps/api/.env.example apps/api/.env # API secrets +cp apps/web/.env.example apps/web/.env # only for running the web app natively +``` + +> In the Docker deployment the web app gets its settings from compose, so `apps/web/.env` is only +> needed when you run `pnpm dev` / `pnpm build` directly. + +4. Generate the two secrets the API needs and paste them into `apps/api/.env`: + +```bash +node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" # JWT_ACCESS_SECRET +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # TOKEN_ENCRYPTION_KEY +``` + +5. Set `AUTH_USERNAME` / `AUTH_PASSWORD` in `apps/api/.env` — that is the account you log in with. + +From here, either run everything in Docker ([Docker](#docker), recommended) or run the apps natively +([Run](#run)). ## Run -- Development mode (native): `pnpm dev` -- Production mode (native): `pnpm build && pnpm start` -- Production mode (Docker): see [Docker](#Docker) section +Native development, with only the infrastructure in Docker: -### Docker +```bash +docker compose -f docker-compose.infra.yml up -d # Postgres + Redis +pnpm --filter api run prisma:migrate:deploy # first run only +pnpm dev # api :4000, web :3000 +``` -Alternatively, you can run each project in a Docker container (recommended for production): +Other scripts (run from the monorepo root): -> **Give the Docker VM enough memory.** The API image compiles TypeScript against the large -> `googleapis` type surface and needs ~4 GiB of RAM for `tsc` — allocate **at least ~6 GiB** to your -> Docker VM or the build can OOM and kill the daemon. With Colima: -> `colima start --cpu 4 --memory 8`. +- Production build: `pnpm build` +- Lint: `pnpm lint` +- Tests: `pnpm test` +- Format: `pnpm format` + +> Run every `docker compose` and `pnpm` command from the monorepo root. -1. Copy `.env.example` to `.env` in the monorepo root and in each app directory (`apps/web/`, `apps/api/`) and adjust the values accordingly. -2. Build the images: `docker compose build` - > Run all docker compose commands from the monorepo root. -3. **First run only:** Start infrastructure, apply migrations, then start all services: +## Docker + +The full stack — Postgres, Redis, the API and the web app — is one command. Both apps ship a +multi-stage `Dockerfile` (Nest `dist` / Next.js standalone output, non-root user, health checks) and +the compose file wires them together. ```bash - docker compose up -d dropto-pg - docker compose run --rm --no-deps dropto-api node_modules/.bin/prisma migrate deploy - docker compose up -d +cp .env.example .env # if you have not already +cp apps/api/.env.example apps/api/.env # compose overrides DB_*/REDIS_* from the root .env +docker compose up -d --build # http://localhost:3000 +docker compose logs -f dropto-web # stream logs for a service +docker compose down # stop; the volumes keep your data ``` -4. **Subsequent runs:** `docker compose up -d` +Database migrations run **automatically** on every `up`: the one-shot `dropto-migrate` service +applies them and the API only starts once it has exited successfully. -## Configuration +> **Give the Docker VM enough memory.** The API image compiles TypeScript against the large +> `googleapis` type surface and needs ~4 GiB for `tsc` — allocate **at least ~6 GiB** to the VM or +> the build can OOM. With Colima: `colima start --cpu 4 --memory 8`. -| App | Port | Technologies | URL | -| --------- | ---- | -------------- | -------------------------------------------------------------------- | -| **api** | 4000 | NestJS, Prisma | http://localhost:4000 · Swagger (dev): http://localhost:4000/swagger | -| **admin** | 3000 | Next.js | http://localhost:3000 | +Building the images without compose: -> Database & Cache +```bash +docker build -f apps/api/Dockerfile -t dropto-api . +docker build -f apps/web/Dockerfile -t dropto-web . \ + --build-arg APP_URL=https://dropto.example.com \ + --build-arg NEXT_PUBLIC_API_URL=https://api.dropto.example.com/api/v1 \ + --build-arg NEXT_PUBLIC_GOOGLE_API_KEY=... \ + --build-arg NEXT_PUBLIC_GOOGLE_APP_ID=... +``` -| Description | Values | -| ----------------- | ----------------------------------- | -| **Ports:** | 5432, 6379 | -| **Technologies:** | PostgreSQL 17, Redis | -| **Credentials:** | `root:password`, `default:password` | +`APP_URL` and every `NEXT_PUBLIC_*` value is inlined at **build time** (they feed the browser bundle +and the prerendered metadata), so pass them as build args for any non-localhost deployment and keep +the runtime values identical. Everything else — including `APP_NAME` — is read at runtime, so +renaming the instance or repointing the API needs no rebuild. -## Deployment +The compose file binds every published port to `127.0.0.1` (the containers are meant to sit behind a +reverse proxy on the same host); drop that prefix in `docker-compose.yml` to expose them on the +network directly — for example to open the workspace from your phone. -DropTo is **self-hosted** — you run it on your own infrastructure, at whatever domain(s) or host/port you choose, routing uploads to whatever storage you want (connect any Google Drive via the setup flow and/or configure your own S3 buckets in the API env). There is **no fixed or official URL**. +## Configuration + +| App | Port | Technologies | URL | +| ------- | ---- | ------------------------ | ---------------------------------------------------------------- | +| **web** | 3000 | Next.js, React, Tailwind | http://localhost:3000 · health: http://localhost:3000/api/health | +| **api** | 4000 | NestJS, Prisma | http://localhost:4000 · health: http://localhost:4000/health | + +> Swagger is served at http://localhost:4000/swagger in development only. + +> Infrastructure + +| Description | Values | +| ---------------- | ----------------------------------- | +| **Ports:** | 5432 (Postgres 17), 6379 (Redis 7) | +| **Credentials:** | `root:password`, `default:password` | + +### Root `.env` — infrastructure + web + +| Variable | Description | +| -------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `DB_*` | Postgres name / user / password; creates the DB **and** is handed to the API | +| `REDIS_*` | Redis password, user and key prefix (the rate limiter's store) | +| `APP_URL` | Public origin of the web app — canonical / OpenGraph / manifest URLs. **Build time** | +| `NEXT_PUBLIC_API_URL` | Public API base; used by the browser only for the Google OAuth redirect. **Build time** | +| `COOKIE_DOMAIN` | Auth-cookie domain; a shared parent domain in production | +| `APP_NAME` | Display name of the instance (optional, default `DropTo`). Runtime | +| `NEXT_PUBLIC_GOOGLE_API_KEY` / `_APP_ID` | Google Picker API key and project number. **Build time** | +| `WEB_PORT` / `API_PORT` / `DB_PORT` / `REDIS_PORT` | Host ports the containers publish on (compose only) | + +### `apps/api/.env` — API + +| Variable | Description | +| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `APP_PORT`, `NODE_ENV` | Listen port and environment | +| `WEB_APP_URL`, `CORS_ALLOWED_ORIGINS` | Where the web app lives, and who may call the API | +| `COOKIE_DOMAIN`, `TRUST_PROXY_HOPS` | Cookie domain; trusted proxy hops, so the rate limiter sees the real IP | +| `JWT_ACCESS_SECRET` | Signs access tokens (refresh tokens are opaque, hashed in the DB) | +| `TOKEN_ENCRYPTION_KEY` | 64 hex chars — AES-256-GCM key for the stored Drive refresh token | +| `AUTH_USERNAME`, `AUTH_PASSWORD` | The single operator account you log in with | +| `GOOGLE_CLIENT_ID` / `_SECRET` / `_REDIRECT_URI` | Google OAuth client for the Drive owner | +| `S3_ENABLED` (+ `S3_BUCKETS`, `S3_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`) | S3 backend; `S3_ENDPOINT` / `S3_FORCE_PATH_STYLE` for MinIO / R2 | +| `MAX_UPLOAD_BYTES`, `OFFLINE_TIMEOUT_MS` | Upload size cap and how long a dropped connection may stall | +| `RATE_LIMIT_ENABLED` | Kill switch for the global rate limiter | +| `DB_*`, `REDIS_*` | Only needed when running natively — compose overrides them from the root `.env` | + +## Connecting storage + +### Google Drive (one-time Google Cloud setup) + +1. Create a Google Cloud project — note the **project number**, it is the Picker `App ID` + (`NEXT_PUBLIC_GOOGLE_APP_ID`). +2. Enable the **Google Drive API** and the **Google Picker API**. +3. Configure the OAuth consent screen (External), add the scope `.../auth/drive.file` and add the + Drive owner's email as a test user. +4. Create an **OAuth client ID** (Web application): + - Authorized redirect URI: `/api/v1/google-auth/google/callback` + - Authorized JavaScript origin: the web app's URL + + → `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` in `apps/api/.env`. + +5. Create an **API key** restricted to the Picker API → `NEXT_PUBLIC_GOOGLE_API_KEY`. +6. Start the app, log in, and use **Connect Drive** in the sidebar. The owner picks the folders to + share; those become the only folders the app can ever see. + +> While the consent screen is in **Testing** mode Google expires the refresh token after 7 days. +> Publish the app to **Production** for permanent use (`drive.file` needs only light verification). + +### S3 / S3-compatible + +Set `S3_ENABLED="true"` in `apps/api/.env` and fill in `S3_BUCKETS` (a comma-separated list — each +bucket becomes a browse root), `S3_REGION`, `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`. For MinIO, +Cloudflare R2 and friends, also set `S3_ENDPOINT` and usually `S3_FORCE_PATH_STYLE="true"`. No UI +step is needed — the buckets show up in the sidebar on the next load. + +## Deployment -Deploy the full stack (web + api + Postgres + Redis) with the bundled compose file (`docker compose up -d`, see [Docker](#Docker)) behind a reverse proxy / TLS of your choice (Caddy, Traefik, nginx, Coolify, Cloudflare Tunnel, …). Everything URL-related is driven by env vars, so point them at wherever you host: +DropTo is **self-hosted** — you run it on your own infrastructure, at whatever domain(s) you choose. +There is no fixed or official URL. + +Run the bundled compose stack behind a reverse proxy / TLS of your choice (Caddy, Traefik, nginx, +Coolify, Cloudflare Tunnel, ...) and point the env vars at wherever you host it: + +- `APP_URL` → the public web origin (e.g. `https://dropto.example.com`) — **build arg** +- `NEXT_PUBLIC_API_URL` → the public API base (e.g. `https://api.dropto.example.com/api/v1`) — + **build arg**, because the browser is redirected there during the Google OAuth flow +- `WEB_APP_URL` and `CORS_ALLOWED_ORIGINS` (API) → the same public web origin +- `COOKIE_DOMAIN` → a shared parent of both hosts (e.g. `.example.com`), so the web app and the API + can read the same auth cookies +- `GOOGLE_REDIRECT_URI` → `/api/v1/google-auth/google/callback`, and the same value + in the Google Cloud OAuth client +- `TRUST_PROXY_HOPS` → the number of proxies in front of the API, so the rate limiter keys on the + real client IP + +> **Serve it over HTTPS.** With `NODE_ENV=production` the auth cookies are issued as `Secure`, so a +> browser will only store them on an `https://` origin — plus `http://localhost`, which browsers +> treat as a secure context. Behind plain http on a LAN IP or a real domain, logging in silently +> fails: the login call succeeds and the cookies are dropped. Terminate TLS at your reverse proxy. + +The images expose health checks (`/api/health` on the web app, `/health` on the API) for the +platform's probes, and both run as a non-root user with a read-only root filesystem. ## License diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 4803a46..780c482 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -12,12 +12,16 @@ RUN apk add --no-cache openssl libc6-compat WORKDIR /app ENV CI=true +# No .git in the build context, so the root `prepare` (husky) has nothing to install. +ENV HUSKY=0 +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/api/package.json ./apps/api/ COPY packages/types/package.json ./packages/types/ -RUN npm install -g pnpm +# corepack resolves pnpm from the `packageManager` field, so the image matches the lockfile. +RUN corepack enable RUN pnpm install --frozen-lockfile COPY apps/api ./apps/api @@ -27,9 +31,9 @@ COPY tsconfig.base.json ./ WORKDIR /app/apps/api -# `pnpm run build` runs `prisma generate` first; it loads prisma.config.ts, whose datasource url is -# templated from DB_* — generate doesn't connect, so build-time placeholders suffice (real values -# come from env_file at runtime). +# The Prisma client is generated into src/prisma/generated and committed, so the build only needs +# tsc. The DB_* placeholders keep prisma.config.ts (whose datasource url is templated from them) +# loadable if a Prisma command runs here; real values come from the environment at runtime. ENV DB_USER=build DB_PASS=build DB_HOST=localhost DB_PORT=5432 DB_NAME=build # tsc needs headroom here (the googleapis type surface is large and OOMs the default ~1 GiB @@ -45,12 +49,14 @@ RUN apk add --no-cache openssl libc6-compat WORKDIR /app ENV CI=true ENV NODE_ENV=production +ENV HUSKY=0 +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/api/package.json ./apps/api/ COPY packages/types/package.json ./packages/types/ -RUN npm install -g pnpm +RUN corepack enable # Production dependencies only (Prisma CLI is a runtime dep — migrations run here). RUN pnpm install --frozen-lockfile --prod @@ -67,4 +73,9 @@ WORKDIR /app/apps/api USER node EXPOSE 4000 + +# 127.0.0.1, not localhost: that also resolves to ::1, which the IPv4 listener refuses. +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget -qO- http://127.0.0.1:4000/health || exit 1 + CMD ["node", "dist/main"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 0f9a258..ab18575 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,19 +1,25 @@ # Multi-stage build, run from the monorepo root: # docker build -f apps/web/Dockerfile -t dropto-web . # -# NEXT_PUBLIC_* values are inlined at build time, so they must be passed as -# build args (Coolify: set them as build-time variables). +# NEXT_PUBLIC_* values and APP_URL are inlined at build time, so they must be passed as +# build args (Coolify: set them as build-time variables). APP_NAME is NOT one of them — it is +# read on the server at runtime, so the instance can be rebranded without a rebuild. FROM node:24-alpine AS builder RUN apk add --no-cache libc6-compat WORKDIR /app ENV CI=true +# No .git in the build context, so the root `prepare` (husky) has nothing to install. +ENV HUSKY=0 +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +ENV NEXT_TELEMETRY_DISABLED=1 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/web/package.json ./apps/web/ COPY packages/types/package.json ./packages/types/ -RUN npm install -g pnpm +# corepack resolves pnpm from the `packageManager` field, so the image matches the lockfile. +RUN corepack enable RUN pnpm install --frozen-lockfile COPY apps/web ./apps/web @@ -40,6 +46,7 @@ WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME=0.0.0.0 +ENV NEXT_TELEMETRY_DISABLED=1 # Standalone output (includes a minimal node_modules + server.js), owned by the non-root user COPY --from=builder --chown=node:node /app/apps/web/.next/standalone ./ @@ -50,4 +57,9 @@ COPY --from=builder --chown=node:node /app/apps/web/public ./apps/web/public USER node EXPOSE 3000 + +# 127.0.0.1, not localhost: that also resolves to ::1, which the IPv4 listener refuses. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1 + CMD ["node", "apps/web/server.js"] diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 384baf6..5273c48 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -64,3 +64,49 @@ @apply cursor-not-allowed opacity-50; } } + +/* Wraps the app so the JavaScript-off screen can hide it with a single rule; generates no box. */ +.app-shell { + display: contents; +} + +/* JavaScript-off screen — themed with `prefers-color-scheme`, as next-themes never adds `.dark`. */ +.noscript-screen { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 0 1rem; + text-align: center; +} + +.noscript-eyebrow { + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-green-600); +} + +.noscript-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-zinc-950); +} + +.noscript-text { + max-width: 28rem; + color: var(--color-zinc-600); +} + +@media (prefers-color-scheme: dark) { + .noscript-title { + color: var(--color-zinc-50); + } + + .noscript-text { + color: var(--color-zinc-400); + } +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index fc9b5dd..82c6666 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -70,9 +70,29 @@ export default function RootLayout({ children }: Readonly) { - - {children} - +
+ + {children} + +
+ + ); diff --git a/docker-compose.infra.yml b/docker-compose.infra.yml index 84c543c..ecff32f 100644 --- a/docker-compose.infra.yml +++ b/docker-compose.infra.yml @@ -1,9 +1,11 @@ # docker-compose.infra.yml -# Infrastructure services only — for local development where apps run natively. +# Infrastructure services only (Postgres + Redis) — for local development where the apps run +# natively. For the full containerised deployment use docker-compose.yml instead. # # Usage: # docker compose -f docker-compose.infra.yml up -d -# pnpm dev # apps run directly and connect to localhost ports +# pnpm --filter api run prisma:migrate:deploy # apply migrations +# pnpm dev # apps run directly and connect to localhost ports services: dropto-pg: @@ -15,7 +17,7 @@ services: POSTGRES_PASSWORD: '${DB_PASS}' POSTGRES_DB: '${DB_NAME}' ports: - - '127.0.0.1:${DB_PORT}:5432' + - '127.0.0.1:${DB_PORT:-5432}:5432' volumes: - dropto_pg_data:/var/lib/postgresql/data networks: @@ -32,7 +34,7 @@ services: restart: unless-stopped command: ['redis-server', '--requirepass', '${REDIS_PASS}'] ports: - - '127.0.0.1:${REDIS_PORT}:6379' + - '127.0.0.1:${REDIS_PORT:-6379}:6379' volumes: - dropto_redis_data:/data networks: diff --git a/docker-compose.yml b/docker-compose.yml index 7248110..81ae1e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,18 @@ # docker-compose.yml -# Runs the full infrastructure + all applications. +# Full deployment: Postgres + Redis + the API + the web app. # # Usage: -# docker compose up -d # all services -# docker compose up -d dropto-pg # infrastructure only -# docker compose logs -f dropto-api # stream logs for a service +# cp .env.example .env # infra + web settings +# cp apps/api/.env.example apps/api/.env # API secrets +# docker compose up -d --build # build + start everything +# docker compose logs -f dropto-web # stream logs for a service +# docker compose down # stop (data survives in the volumes) # -# Infrastructure + web build args: root .env (see .env.example) -# API env vars: apps/api/.env (see apps/api/.env.example) +# Migrations run automatically on every `up` (the one-shot `dropto-migrate` service); the API +# only starts once they have been applied. +# +# Infra + web settings: root .env (see .env.example) +# API secrets: apps/api/.env (see apps/api/.env.example) services: dropto-pg: @@ -19,7 +24,8 @@ services: POSTGRES_PASSWORD: '${DB_PASS}' POSTGRES_DB: '${DB_NAME}' ports: - - '127.0.0.1:${DB_PORT}:5432' + # Loopback only — nothing outside this host needs to reach the database. + - '127.0.0.1:${DB_PORT:-5432}:5432' volumes: - dropto_pg_data:/var/lib/postgresql/data networks: @@ -36,7 +42,7 @@ services: restart: unless-stopped command: ['redis-server', '--requirepass', '${REDIS_PASS}'] ports: - - '127.0.0.1:${REDIS_PORT}:6379' + - '127.0.0.1:${REDIS_PORT:-6379}:6379' volumes: - dropto_redis_data:/data networks: @@ -47,20 +53,59 @@ services: timeout: 5s retries: 5 - dropto-api: + # One-shot: applies pending Prisma migrations, then exits. Shares the API image. + dropto-migrate: build: context: . dockerfile: apps/api/Dockerfile - container_name: dropto-api - restart: unless-stopped + image: dropto-api + container_name: dropto-migrate + restart: 'no' + command: ['node_modules/.bin/prisma', 'migrate', 'deploy'] env_file: - apps/api/.env - environment: + environment: &api_env NODE_ENV: 'production' + # Inside the network the services talk over their container names and default ports; these + # override apps/api/.env so the root .env stays the single source for infra credentials. DB_HOST: 'dropto-pg' + DB_PORT: '5432' + DB_NAME: '${DB_NAME}' + DB_USER: '${DB_USER}' + DB_PASS: '${DB_PASS}' REDIS_HOST: 'dropto-redis' + REDIS_PORT: '6379' + REDIS_USER: '${REDIS_USER:-default}' + REDIS_PASS: '${REDIS_PASS}' + REDIS_KEY_PREFIX: '${REDIS_KEY_PREFIX:-dropto:}' + networks: + - dropto_net + depends_on: + dropto-pg: + condition: service_healthy + + dropto-api: + build: + context: . + dockerfile: apps/api/Dockerfile + image: dropto-api + container_name: dropto-api + restart: unless-stopped + env_file: + - apps/api/.env + environment: *api_env + # The server only reads its own bundle; /tmp is writable for Prisma's query engine. + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true ports: - - '4000:4000' + # The browser hits this directly during the Google OAuth redirect, so it must be reachable + # from wherever the operator's browser runs. Drop the 127.0.0.1 prefix to expose it on the LAN. + - '127.0.0.1:${API_PORT:-4000}:4000' networks: - dropto_net depends_on: @@ -68,41 +113,46 @@ services: condition: service_healthy dropto-redis: condition: service_healthy - healthcheck: - test: ['CMD-SHELL', 'wget -qO- http://localhost:4000/health || exit 1'] - interval: 10s - timeout: 5s - retries: 5 + dropto-migrate: + condition: service_completed_successfully dropto-web: build: context: . dockerfile: apps/web/Dockerfile args: + # Inlined into the client bundle / prerendered metadata — build-time only. NEXT_PUBLIC_API_URL: '${NEXT_PUBLIC_API_URL:-http://localhost:4000/api/v1}' NEXT_PUBLIC_GOOGLE_API_KEY: '${NEXT_PUBLIC_GOOGLE_API_KEY:-}' NEXT_PUBLIC_GOOGLE_APP_ID: '${NEXT_PUBLIC_GOOGLE_APP_ID:-}' APP_URL: '${APP_URL:-http://localhost:3000}' + image: dropto-web container_name: dropto-web restart: unless-stopped + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true environment: NODE_ENV: 'production' PORT: '3000' + # Runtime, so renaming the instance needs no rebuild. APP_NAME: '${APP_NAME:-DropTo}' + APP_URL: '${APP_URL:-http://localhost:3000}' + # Server-to-server, over the compose network — never leaves the host. API_URL: 'http://dropto-api:4000/api/v1' COOKIE_DOMAIN: '${COOKIE_DOMAIN:-localhost}' ports: - - '3000:3000' + # Bound to loopback for a same-host reverse proxy; drop the prefix to expose it directly. + - '127.0.0.1:${WEB_PORT:-3000}:3000' networks: - dropto_net depends_on: dropto-api: condition: service_healthy - healthcheck: - test: ['CMD-SHELL', 'wget -qO- http://localhost:3000 || exit 1'] - interval: 10s - timeout: 5s - retries: 5 volumes: dropto_pg_data: diff --git a/package.json b/package.json index 1b5e4a7..5c828b4 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint": "turbo lint", "test": "turbo test", "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", - "prepare": "husky" + "prepare": "husky || true" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.65.0", From 29eda8cb57251827cd81653c6af6c1e33373d13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Sun, 30 Aug 2026 11:26:12 +0200 Subject: [PATCH 03/26] feat(web): make the split view desktop-only --- .../src/common/constants/layout.constants.ts | 6 ++++++ .../src/components/layouts/FileBrowser.tsx | 7 ++++++- .../components/layouts/WorkspaceClient.tsx | 19 ++++++++++++++++--- .../components/layouts/mobile/MobileMenu.tsx | 6 ++---- 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/common/constants/layout.constants.ts diff --git a/apps/web/src/common/constants/layout.constants.ts b/apps/web/src/common/constants/layout.constants.ts new file mode 100644 index 0000000..9c2704d --- /dev/null +++ b/apps/web/src/common/constants/layout.constants.ts @@ -0,0 +1,6 @@ +/** + * Viewport at which the desktop chrome fits: the sidebar rail and the split view. Tailwind's `md`. + **/ +// Below it the sidebar becomes a swipe-open drawer and the split view is unavailable — two panes +// have no room, and moving items between them is a drag & drop gesture touch never fires. +export const DESKTOP_QUERY = '(min-width: 48rem)'; diff --git a/apps/web/src/components/layouts/FileBrowser.tsx b/apps/web/src/components/layouts/FileBrowser.tsx index 8a4894a..251d818 100644 --- a/apps/web/src/components/layouts/FileBrowser.tsx +++ b/apps/web/src/components/layouts/FileBrowser.tsx @@ -12,6 +12,8 @@ import { useState, } from 'react'; +import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; +import { useMediaQuery } from '@/common/hooks/useMediaQuery'; import type { Crumb, SortDir, @@ -228,6 +230,9 @@ export default function FileBrowser({ onLoadMore, onMoveIntoFolder, }: Props) { + // The split view is desktop-only; the toolbar menu is client-rendered, so gating it on a media + // query touches no server markup. + const canSplit = useMediaQuery(DESKTOP_QUERY); const fileInputRef = useRef(null); const folderInputRef = useRef(null); // Anchor row for Shift-click range selection. @@ -894,7 +899,7 @@ export default function FileBrowser({ onClick={() => runToolbarAction(onNewFolder)} /> )} - {onToggleSplit && ( + {onToggleSplit && canSplit && ( >(new Set()); const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); const [bulkDeleting, setBulkDeleting] = useState(false); + const canSplit = useMediaQuery(DESKTOP_QUERY); const [split, setSplit] = useState(false); const [activePane, setActivePane] = useState<0 | 1>(0); const [bulkPane, setBulkPane] = useState<0 | 1>(0); @@ -563,6 +566,16 @@ function WorkspaceInner({ } }, [activeBackend]); + // Below `md` two panes have no usable width, and moving items between them is a drag & drop + // gesture touch never fires — so the split collapses back to one pane on a narrow viewport. + useEffect(() => { + if (!canSplit) { + setSplit(false); + setDragMove(null); + setActivePane(0); + } + }, [canSplit]); + // Keyboard shortcut (Cmd/Ctrl + \) to toggle the split view. useEffect(() => { const handler = (event: KeyboardEvent) => { @@ -571,8 +584,8 @@ function WorkspaceInner({ if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { return; } - // Only open the split from inside a folder; closing is always allowed. - if (!split && currentFolderId === null) { + // Only open the split from inside a folder, and only where it fits; closing is always allowed. + if (!split && (currentFolderId === null || !canSplit)) { return; } event.preventDefault(); @@ -581,7 +594,7 @@ function WorkspaceInner({ }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [split, currentFolderId, toggleSplit]); + }, [split, currentFolderId, canSplit, toggleSplit]); // Move the given ids into a target folder (Finder-style drop onto a folder row, same pane). const handleMoveIntoFolder = useCallback( diff --git a/apps/web/src/components/layouts/mobile/MobileMenu.tsx b/apps/web/src/components/layouts/mobile/MobileMenu.tsx index 3706d33..e193d7d 100644 --- a/apps/web/src/components/layouts/mobile/MobileMenu.tsx +++ b/apps/web/src/components/layouts/mobile/MobileMenu.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; +import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; import { useMediaQuery } from '@/common/hooks/useMediaQuery'; import type { AccountSidebarProps } from '../AccountSidebarContent'; @@ -11,16 +12,13 @@ import MobileDrawer from './MobileDrawer'; /** Remembers that the swipe hint has been acknowledged. */ const HINT_STORAGE_KEY = 'dropto:menu-hint-seen'; -/** The breakpoint at which the sidebar rail appears, so the drawer and its hint are pointless. */ -const SIDEBAR_QUERY = '(min-width: 48rem)'; - /** * Owns the mobile sidebar drawer and its first-visit swipe hint. **/ // The media query is the single gate: below `md` there is no rail, so the sidebar is a gesture. // Above it the drawer, the swipe listener and the hint are never mounted at all. export default function MobileMenu(sidebar: AccountSidebarProps) { - const hasSidebar = useMediaQuery(SIDEBAR_QUERY); + const hasSidebar = useMediaQuery(DESKTOP_QUERY); const [menuOpen, setMenuOpen] = useState(false); const [hintOpen, setHintOpen] = useState(false); From 6ab0483a751923a73ac1850a9c79c60c90681e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Sun, 30 Aug 2026 15:25:58 +0200 Subject: [PATCH 04/26] docs: correct the Drive OAuth scope and the publishing guidance --- README.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5a94e32..f1470d8 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ cancel), per-file progress and ETA in a floating upload dock, and abortable transfers sized by `MAX_UPLOAD_BYTES` - **Storage switcher** — browse Google Drive and S3 side by side and move between them in one click -- **Scoped Drive access** — the OAuth scope is `drive.file` only; the owner authorizes specific - folders through the Google Picker and every call is validated server-side to stay inside that tree +- **Folder-scoped Drive access** — the owner authorizes specific folders through the Google Picker, + and every single Drive call is validated server-side (ancestor walk) to stay inside that tree; + the authorized roots themselves cannot be deleted - **Single-operator auth** — username/password from the env, JWT access tokens plus rotating, DB-backed refresh tokens in httpOnly cookies, with reuse detection - **Secrets stay encrypted** — the Drive refresh token is AES-256-GCM encrypted in Postgres and the @@ -191,8 +192,8 @@ network directly — for example to open the workspace from your phone. 1. Create a Google Cloud project — note the **project number**, it is the Picker `App ID` (`NEXT_PUBLIC_GOOGLE_APP_ID`). 2. Enable the **Google Drive API** and the **Google Picker API**. -3. Configure the OAuth consent screen (External), add the scope `.../auth/drive.file` and add the - Drive owner's email as a test user. +3. Configure the OAuth consent screen (External), add the scope + `https://www.googleapis.com/auth/drive` and add the Drive owner's email as a test user. 4. Create an **OAuth client ID** (Web application): - Authorized redirect URI: `/api/v1/google-auth/google/callback` - Authorized JavaScript origin: the web app's URL @@ -201,10 +202,23 @@ network directly — for example to open the workspace from your phone. 5. Create an **API key** restricted to the Picker API → `NEXT_PUBLIC_GOOGLE_API_KEY`. 6. Start the app, log in, and use **Connect Drive** in the sidebar. The owner picks the folders to - share; those become the only folders the app can ever see. - -> While the consent screen is in **Testing** mode Google expires the refresh token after 7 days. -> Publish the app to **Production** for permanent use (`drive.file` needs only light verification). + share; those become the only folders the app will touch. + +> **A note on the scope.** DropTo asks for the full `.../auth/drive` scope rather than the +> per-file `drive.file`, because `drive.file` cannot see files that were added to a folder +> _directly in Drive_ — only ones the app created or the Picker handed it, which makes a shared +> folder look half-empty. The trade-off is that the grant covers the owner's whole Drive and the +> confinement to the authorized folders is enforced by the API alone, on every request. + +> **Publish the app, or Drive keeps disconnecting.** While the consent screen sits in **Testing**, +> Google expires the refresh token after **7 days** and the workspace shows "Your Google Drive was +> disconnected". Hit **Publish app** (publishing status → _In production_) and the expiry is gone. +> Verification is **not** needed for this: it is only mandatory for _public_ apps using +> sensitive/restricted scopes. Staying unverified in production costs a one-time "Google hasn't +> verified this app" screen (→ _Advanced_ → _Go to app_), a cap of 100 users and no app name/logo on +> the consent screen — all irrelevant for a single-owner instance. Chasing verified status is not +> worth it: `.../auth/drive` is a _restricted_ scope, which pulls in an annual third-party CASA +> security assessment. ### S3 / S3-compatible From ef7aad3582de9a29a6cdd7f025d3aed644081e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Sun, 30 Aug 2026 16:20:30 +0200 Subject: [PATCH 05/26] style(web): matching 404 with other my apps --- apps/web/src/app/not-found.tsx | 1 + .../components/layouts/NotFoundContent.tsx | 36 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index bbcf039..fd258eb 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -4,6 +4,7 @@ import NotFoundContent from '@/components/layouts/NotFoundContent'; export const metadata: Metadata = { title: 'Page not found', + robots: { index: false, follow: true }, }; /** diff --git a/apps/web/src/components/layouts/NotFoundContent.tsx b/apps/web/src/components/layouts/NotFoundContent.tsx index 9159be5..16dc1cf 100644 --- a/apps/web/src/components/layouts/NotFoundContent.tsx +++ b/apps/web/src/components/layouts/NotFoundContent.tsx @@ -1,32 +1,22 @@ -'use client'; - -import { motion } from 'framer-motion'; import Link from 'next/link'; -import Icon from '@/components/common/Icon'; - /** - * Animated 404 body (client, so the not-found route stays a Server Component). + * 404 body — shared by the not-found route and the workspace route's unknown-backend branch. **/ +// Deliberately static: an error page must not depend on JS having run to become visible. export default function NotFoundContent() { return ( -
- - -

Page not found

-

- The page you are looking for doesn't exist or has been moved. -

- - Back to home - -
+
+

Error 404

+

Page not found

+

+ This page does not exist — but your storage is one click away. +

+ + Back to the workspace +
); } From 61f882758309c9b5acd3f5ffac94e8ecb64ee3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 14:01:46 +0200 Subject: [PATCH 06/26] chore: monorepo audit, major dependency upgrades, and contract cleanup Dependencies - NestJS 11 -> 12 (+ @nestjs/config 4 -> 12). The packages now ship as ESM, so Jest runs with --experimental-vm-modules and no longer transforms node_modules; that also drops the hand-maintained ESM allowlist from transformIgnorePatterns. Requires Node >= 22.12 to run, >= 24.9 to run the tests. - ioredis 5 -> 6 (RESP3 by default), verified against a live Redis: the rate limiter's consume/reject semantics are unchanged, no protocol: 2 fallback needed. - framer-motion 12 -> motion 13 (package renamed; imports moved to 'motion/react'). - googleapis 173 -> 176, next 16.3.3, lucide-react 1.38, plus in-range patch bumps. - Dropped eslint-plugin-import: it declares no ESLint 10 support and backed a single low-value warning. Shared contract - The API now depends on @dropto/types and its Swagger entities `implements` the shared interfaces, so the two descriptions of the wire format cannot drift. This surfaced three places that typed createdAt as Date while the wire carries an ISO string, and a StorageBackend that was declared twice. - UploadStatus -> ResumableUploadStatus, freeing the name from Prisma's enum. Performance - The Drive ancestor walk memoizes the parents of intermediate folders (60s, in-process). The target item is always re-read, so a deleted item still 404s. - The file list filter is debounced, so a keystroke no longer re-filters, re-sorts and re-renders the whole list in both panes. Cleanup - Removed docker-compose.infra.yml: it duplicated the pg/redis services verbatim; `docker compose up -d dropto-pg dropto-redis` builds no app image either way. - Removed the no-op Prisma seed script and unexported an internal JWT helper. - Added apps/api/.env.test.example, without which the e2e suite could not run from a fresh clone. - tsBuildInfoFile now lives inside dist, so wiping dist actually forces a rebuild instead of silently emitting nothing. - Typed the Google Picker globals, removing the last `any` in the repo. --- README.md | 6 +- apps/api/.env.test.example | 60 + apps/api/package.json | 48 +- apps/api/prisma.config.ts | 1 - .../common/utils/ttl-cache.functions.spec.ts | 55 + .../src/common/utils/ttl-cache.functions.ts | 49 + .../modules/auth/entities/auth-user.entity.ts | 3 +- .../entities/allowed-folder.entity.ts | 20 +- .../google-auth/google-auth.service.spec.ts | 13 +- .../google-auth/google-auth.service.ts | 8 +- .../entities/drive-entry-page.entity.ts | 3 +- .../storage/entities/drive-entry.entity.ts | 3 +- .../resumable-upload-session.entity.ts | 3 +- .../storage/entities/storage-status.entity.ts | 5 +- .../storage/entities/upload-result.entity.ts | 3 +- .../storage/entities/upload-status.entity.ts | 3 +- .../interfaces/storage-provider.interface.ts | 6 +- .../providers/google-drive.provider.ts | 32 +- .../storage/providers/s3-storage.provider.ts | 3 +- apps/api/src/prisma/seed.ts | 29 - apps/api/test/helpers/fixtures.helper.ts | 2 +- apps/api/test/jest-e2e.json | 23 +- apps/api/tsconfig.json | 3 +- apps/web/package.json | 16 +- apps/web/prettier.config.ts | 13 +- .../src/actions/storage/storage.actions.ts | 4 +- .../src/common/services/api/storage.api.ts | 6 +- .../src/common/services/picker/usePicker.ts | 52 +- apps/web/src/common/utils/jwt.functions.ts | 2 +- apps/web/src/components/common/Modal.tsx | 2 +- .../web/src/components/common/ThemeToggle.tsx | 2 +- apps/web/src/components/forms/LoginForm.tsx | 2 +- .../layouts/AccountSidebarContent.tsx | 2 +- .../web/src/components/layouts/Breadcrumb.tsx | 2 +- .../src/components/layouts/FileBrowser.tsx | 12 +- .../src/components/layouts/PreviewPanel.tsx | 2 +- .../web/src/components/layouts/UploadDock.tsx | 2 +- .../layouts/mobile/MobileDrawer.tsx | 2 +- .../components/providers/ToastProvider.tsx | 2 +- apps/web/tsconfig.json | 4 +- docker-compose.infra.yml | 55 - eslint.config.base.mjs | 3 - package.json | 12 +- packages/types/index.ts | 2 +- pnpm-lock.yaml | 4935 +++++++---------- pnpm-workspace.yaml | 6 +- turbo.json | 3 + 47 files changed, 2408 insertions(+), 3116 deletions(-) create mode 100644 apps/api/.env.test.example create mode 100644 apps/api/src/common/utils/ttl-cache.functions.spec.ts create mode 100644 apps/api/src/common/utils/ttl-cache.functions.ts delete mode 100644 apps/api/src/prisma/seed.ts delete mode 100644 docker-compose.infra.yml diff --git a/README.md b/README.md index f1470d8..4f768f2 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,9 @@ From here, either run everything in Docker ([Docker](#docker), recommended) or r Native development, with only the infrastructure in Docker: ```bash -docker compose -f docker-compose.infra.yml up -d # Postgres + Redis -pnpm --filter api run prisma:migrate:deploy # first run only -pnpm dev # api :4000, web :3000 +docker compose up -d dropto-pg dropto-redis # infrastructure only — builds no app image +pnpm --filter api run prisma:migrate:deploy # first run only +pnpm dev # api :4000, web :3000 ``` Other scripts (run from the monorepo root): diff --git a/apps/api/.env.test.example b/apps/api/.env.test.example new file mode 100644 index 0000000..e96c69e --- /dev/null +++ b/apps/api/.env.test.example @@ -0,0 +1,60 @@ +# Environment for the e2e suite (`pnpm --filter api run test:e2e`), loaded by test/setup-env.ts. +# The unit specs mock every dependency and need none of this; only the e2e suite boots the app. +# +# cp apps/api/.env.test.example apps/api/.env.test +# +# Nothing here reaches a real service: Google and Prisma are mocked in the e2e helpers, so the +# values only have to satisfy the config validation at boot. Use throwaway values. + +# App +APP_PORT=4001 +NODE_ENV="test" + +# URLs +WEB_APP_URL="http://localhost:3000" + +# CORS +CORS_ALLOWED_ORIGINS="http://localhost:3000" + +# Security +COOKIE_DOMAIN="localhost" + +# JWT +JWT_ACCESS_SECRET="test_access_secret" + +# Database (PostgreSQL) — the Prisma client is mocked, so this is never dialled +DB_HOST="localhost" +DB_PORT=5432 +DB_NAME="dropto_test" +DB_USER="root" +DB_PASS="password" + +# Redis — the rate limiter is disabled below, so this is never dialled +REDIS_HOST="localhost" +REDIS_PORT=6379 +REDIS_USER="default" +REDIS_PASS="password" +REDIS_KEY_PREFIX="dropto:test:" + +# Google OAuth — googleapis is mocked in the e2e helpers +GOOGLE_CLIENT_ID="test_client_id" +GOOGLE_CLIENT_SECRET="test_client_secret" +GOOGLE_REDIRECT_URI="http://localhost:4001/api/v1/google-auth/google/callback" + +# Rate limiting — off, so tests don't trip the limiter when replaying requests +RATE_LIMIT_ENABLED="false" + +# Auth — test/auth/session.e2e-spec.ts logs in with exactly these credentials +AUTH_USERNAME="test-admin" +AUTH_PASSWORD="test-password" + +# Uploads +MAX_UPLOAD_BYTES=1048576 # 1 MiB, enough for the fixtures +OFFLINE_TIMEOUT_MS=1000 + +# S3 storage +S3_ENABLED="false" + +# Token encryption — must stay the all-zero key: test/helpers/fixtures.helper.ts encrypts the +# fixture refresh token with it, and the real CryptoService has to be able to decrypt it. +TOKEN_ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000" diff --git a/apps/api/package.json b/apps/api/package.json index f3b2cc0..64d9cbf 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -7,15 +7,14 @@ "start": "nest start", "dev": "nest start --watch", "lint": "eslint \"{src,test}/**/*.ts\" --fix", - "test": "jest", - "test:cov": "jest --coverage", - "test:e2e": "jest --config ./test/jest-e2e.json", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test:cov": "NODE_OPTIONS=--experimental-vm-modules jest --coverage", + "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:migrate:deploy": "prisma migrate deploy", "prisma:studio": "prisma studio", "prisma:reset": "prisma reset", - "prisma:seed": "ts-node --transpile-only src/prisma/seed.ts", "openapi:export": "ts-node --transpile-only -r tsconfig-paths/register src/common/scripts/export-openapi.ts" }, "jest": { @@ -27,7 +26,7 @@ "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transformIgnorePatterns": [ - "/node_modules/.pnpm/(?!(cookie|archiver|compress-commons|crc32-stream|is-stream|zip-stream)@)", + "/node_modules/", "\\.pnp\\.[^/]+$" ], "transform": { @@ -54,17 +53,18 @@ } }, "dependencies": { - "@aws-sdk/client-s3": "^3.1098.0", - "@aws-sdk/lib-storage": "^3.1098.0", - "@nestjs/common": "^11.1.28", - "@nestjs/config": "^4.0.4", - "@nestjs/core": "^11.1.28", - "@nestjs/jwt": "^11.0.2", - "@nestjs/platform-express": "^11.1.28", - "@nestjs/swagger": "^11.4.6", + "@aws-sdk/client-s3": "^3.1121.0", + "@aws-sdk/lib-storage": "^3.1121.0", + "@dropto/types": "workspace:*", + "@nestjs/common": "^12.0.1", + "@nestjs/config": "^12.0.0", + "@nestjs/core": "^12.0.1", + "@nestjs/jwt": "^12.0.1", + "@nestjs/platform-express": "^12.0.1", + "@nestjs/swagger": "^12.0.1", "@nestjs/terminus": "^11.1.1", - "@prisma/adapter-pg": "^7.9.1", - "@prisma/client": "^7.9.1", + "@prisma/adapter-pg": "^7.10.0", + "@prisma/client": "^7.10.0", "archiver": "^8.0.0", "busboy": "^1.6.0", "class-transformer": "^0.5.1", @@ -73,27 +73,27 @@ "cookie-parser": "^1.4.7", "dotenv": "^17.4.2", "express": "^5.2.1", - "googleapis": "^173.0.0", + "googleapis": "^176.0.0", "helmet": "^8.3.0", - "ioredis": "^5.11.1", - "prisma": "^7.9.1", + "ioredis": "^6.0.0", + "prisma": "^7.10.0", "rate-limiter-flexible": "^11.2.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2" }, "devDependencies": { - "@nestjs/cli": "^11.0.24", - "@nestjs/schematics": "^11.1.0", - "@nestjs/testing": "^11.1.28", + "@nestjs/cli": "^12.0.0", + "@nestjs/schematics": "^12.0.0", + "@nestjs/testing": "^12.0.1", "@types/archiver": "^8.0.0", "@types/busboy": "^1.5.4", "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/jest": "^30.0.0", - "@types/node": "^26.1.2", + "@types/node": "^26.4.0", "@types/supertest": "^7.2.1", - "eslint": "^10.8.0", - "jest": "^30.4.2", + "eslint": "^10.9.1", + "jest": "^30.5.0", "prisma-dbml-generator": "^0.12.0", "supertest": "^7.2.2", "ts-jest": "^29.4.12", diff --git a/apps/api/prisma.config.ts b/apps/api/prisma.config.ts index 7896e79..8177c40 100644 --- a/apps/api/prisma.config.ts +++ b/apps/api/prisma.config.ts @@ -5,7 +5,6 @@ export default defineConfig({ schema: 'src/prisma/schema.prisma', migrations: { path: 'src/prisma/migrations', - seed: 'ts-node --transpile-only src/prisma/seed.ts', }, datasource: { url: `postgresql://${env('DB_USER')}:${env('DB_PASS')}@${env('DB_HOST')}:${env('DB_PORT')}/${env('DB_NAME')}`, diff --git a/apps/api/src/common/utils/ttl-cache.functions.spec.ts b/apps/api/src/common/utils/ttl-cache.functions.spec.ts new file mode 100644 index 0000000..401b79b --- /dev/null +++ b/apps/api/src/common/utils/ttl-cache.functions.spec.ts @@ -0,0 +1,55 @@ +import { TtlCache } from './ttl-cache.functions'; + +describe('TtlCache', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + it('returns a stored value before it expires', () => { + const cache = new TtlCache(1000, 10); + + cache.set('a', 'value'); + + expect(cache.get('a')).toBe('value'); + }); + + it('returns undefined for a key that was never set', () => { + expect(new TtlCache(1000, 10).get('missing')).toBeUndefined(); + }); + + it('drops a value once its TTL has passed', () => { + jest.useFakeTimers(); + + const cache = new TtlCache(1000, 10); + cache.set('a', 'value'); + + jest.advanceTimersByTime(1001); + + expect(cache.get('a')).toBeUndefined(); + }); + + it('evicts the oldest entry when the cap is reached', () => { + const cache = new TtlCache(1000, 2); + + cache.set('a', '1'); + cache.set('b', '2'); + cache.set('c', '3'); + + expect(cache.get('a')).toBeUndefined(); + expect(cache.get('b')).toBe('2'); + expect(cache.get('c')).toBe('3'); + }); + + it('refreshes the TTL when an existing key is written again', () => { + jest.useFakeTimers(); + + const cache = new TtlCache(1000, 10); + cache.set('a', 'first'); + + jest.advanceTimersByTime(800); + cache.set('a', 'second'); + jest.advanceTimersByTime(800); + + expect(cache.get('a')).toBe('second'); + }); +}); diff --git a/apps/api/src/common/utils/ttl-cache.functions.ts b/apps/api/src/common/utils/ttl-cache.functions.ts new file mode 100644 index 0000000..821cf07 --- /dev/null +++ b/apps/api/src/common/utils/ttl-cache.functions.ts @@ -0,0 +1,49 @@ +/** An entry plus the timestamp after which it must be recomputed. */ +interface Entry { + value: T; + expiresAt: number; +} + +/** + * A tiny in-process cache with a per-entry TTL and a hard size cap. + **/ +// In-process on purpose: the app runs as a single API container, so a Redis round-trip would add a +// dependency (and a failure mode) to a hot path for no gain. Entries are cheap to recompute, so a +// restart losing them costs nothing. +export class TtlCache { + private readonly entries = new Map>(); + + constructor( + private readonly ttlMs: number, + private readonly maxEntries: number, + ) {} + + get(key: string): T | undefined { + const entry = this.entries.get(key); + + if (!entry) { + return undefined; + } + + if (entry.expiresAt <= Date.now()) { + this.entries.delete(key); + + return undefined; + } + + return entry.value; + } + + set(key: string, value: T): void { + // Drop the oldest insertion once full; Map preserves insertion order. + if (this.entries.size >= this.maxEntries) { + const oldest = this.entries.keys().next(); + + if (!oldest.done) { + this.entries.delete(oldest.value); + } + } + + this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs }); + } +} diff --git a/apps/api/src/modules/auth/entities/auth-user.entity.ts b/apps/api/src/modules/auth/entities/auth-user.entity.ts index bf4a554..3753db7 100644 --- a/apps/api/src/modules/auth/entities/auth-user.entity.ts +++ b/apps/api/src/modules/auth/entities/auth-user.entity.ts @@ -1,6 +1,7 @@ +import type { AuthUser } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class AuthUserEntity { +export class AuthUserEntity implements AuthUser { @ApiProperty({ type: 'string', description: 'Username of the authenticated operator' }) username: string; } diff --git a/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts b/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts index 613b914..6aa06b4 100644 --- a/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts +++ b/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts @@ -1,6 +1,7 @@ +import type { AllowedFolder } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class AllowedFolderEntity { +export class AllowedFolderEntity implements AllowedFolder { @ApiProperty({ type: 'string', description: 'Internal record id' }) id: string; @@ -15,5 +16,22 @@ export class AllowedFolderEntity { format: 'date-time', description: 'When the folder was authorized', }) + createdAt: string; +} + +/** + * Maps a Prisma allowed-folder row onto the wire entity (Date → ISO string). + **/ +export function toAllowedFolderEntity(row: { + id: string; + folderId: string; + name: string; createdAt: Date; +}): AllowedFolderEntity { + return { + id: row.id, + folderId: row.folderId, + name: row.name, + createdAt: row.createdAt.toISOString(), + }; } diff --git a/apps/api/src/modules/google-auth/google-auth.service.spec.ts b/apps/api/src/modules/google-auth/google-auth.service.spec.ts index 00861ff..831615e 100644 --- a/apps/api/src/modules/google-auth/google-auth.service.spec.ts +++ b/apps/api/src/modules/google-auth/google-auth.service.spec.ts @@ -206,16 +206,21 @@ describe('GoogleAuthService', () => { describe('saveAllowedFolders', () => { it('upserts each selected folder in a transaction and returns the updated list', async () => { + // Prisma hands back a Date; the entity exposes the ISO string the wire contract declares. const saved = [ { id: 'f1', folderId: 'drive-1', name: 'Photos', createdAt: new Date('2026-01-01') }, ]; + const expected = saved.map((folder) => ({ + ...folder, + createdAt: folder.createdAt.toISOString(), + })); prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1' }); prisma.$transaction.mockResolvedValue([]); prisma.allowedFolder.findMany.mockResolvedValue(saved); const dto = { folders: [{ folderId: 'drive-1', name: 'Photos' }] }; - await expect(service.saveAllowedFolders(dto)).resolves.toEqual(saved); + await expect(service.saveAllowedFolders(dto)).resolves.toEqual(expected); expect(prisma.allowedFolder.upsert).toHaveBeenCalledWith( expect.objectContaining({ where: { driveAccountId_folderId: { driveAccountId: 'acc-1', folderId: 'drive-1' } }, @@ -240,6 +245,10 @@ describe('GoogleAuthService', () => { const allowedFolders = [ { id: 'f1', folderId: 'drive-1', name: 'Photos', createdAt: new Date('2026-01-01') }, ]; + const expectedFolders = allowedFolders.map((folder) => ({ + ...folder, + createdAt: folder.createdAt.toISOString(), + })); prisma.driveAccount.findFirst.mockResolvedValue({ email: 'owner@gmail.com', allowedFolders, @@ -248,7 +257,7 @@ describe('GoogleAuthService', () => { await expect(service.getStatus()).resolves.toEqual({ connected: true, email: 'owner@gmail.com', - allowedFolders, + allowedFolders: expectedFolders, }); }); }); diff --git a/apps/api/src/modules/google-auth/google-auth.service.ts b/apps/api/src/modules/google-auth/google-auth.service.ts index 74dcccf..a1e75e3 100644 --- a/apps/api/src/modules/google-auth/google-auth.service.ts +++ b/apps/api/src/modules/google-auth/google-auth.service.ts @@ -12,7 +12,7 @@ import { type GoogleConfig, googleConfig } from '@/config/google.config'; import { PrismaService } from '@/prisma/prisma.service'; import { SaveFoldersDto } from './dto/save-folders.dto'; -import { AllowedFolderEntity } from './entities/allowed-folder.entity'; +import { AllowedFolderEntity, toAllowedFolderEntity } from './entities/allowed-folder.entity'; import { DriveAccountStatusEntity } from './entities/drive-account-status.entity'; import { DRIVE_OWNER_TTL_MS } from './google-auth.constants'; @@ -237,11 +237,13 @@ export class GoogleAuthService { ), ); - return this.prismaService.allowedFolder.findMany({ + const folders = await this.prismaService.allowedFolder.findMany({ where: { driveAccountId }, select: { id: true, folderId: true, name: true, createdAt: true }, orderBy: { createdAt: 'asc' }, }); + + return folders.map(toAllowedFolderEntity); } /** @@ -265,7 +267,7 @@ export class GoogleAuthService { return { connected: true, email: account.email, - allowedFolders: account.allowedFolders, + allowedFolders: account.allowedFolders.map(toAllowedFolderEntity), }; } diff --git a/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts b/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts index 1c51279..8354b79 100644 --- a/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts +++ b/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts @@ -1,3 +1,4 @@ +import type { DriveEntryPage } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; import { DriveEntryEntity } from './drive-entry.entity'; @@ -5,7 +6,7 @@ import { DriveEntryEntity } from './drive-entry.entity'; /** * One page of folder contents; `nextPageToken` is the cursor for the next page (null on the last). **/ -export class DriveEntryPageEntity { +export class DriveEntryPageEntity implements DriveEntryPage { @ApiProperty({ type: [DriveEntryEntity], description: 'Entries in this page' }) entries: DriveEntryEntity[]; diff --git a/apps/api/src/modules/storage/entities/drive-entry.entity.ts b/apps/api/src/modules/storage/entities/drive-entry.entity.ts index 94973bf..3d86588 100644 --- a/apps/api/src/modules/storage/entities/drive-entry.entity.ts +++ b/apps/api/src/modules/storage/entities/drive-entry.entity.ts @@ -1,6 +1,7 @@ +import type { DriveEntry } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class DriveEntryEntity { +export class DriveEntryEntity implements DriveEntry { @ApiProperty({ type: 'string', description: 'Opaque file/folder id (Drive id or S3 ref)' }) id: string; diff --git a/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts b/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts index 8169fc5..a71000b 100644 --- a/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts +++ b/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts @@ -1,9 +1,10 @@ +import type { ResumableUploadSession } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; /** * A resumable upload session; the browser PUTs bytes straight to `uploadUrl`, then calls finalize. **/ -export class ResumableUploadSessionEntity { +export class ResumableUploadSessionEntity implements ResumableUploadSession { @ApiProperty({ type: 'string', description: 'Session URL the browser streams the file bytes to' }) uploadUrl: string; diff --git a/apps/api/src/modules/storage/entities/storage-status.entity.ts b/apps/api/src/modules/storage/entities/storage-status.entity.ts index 4d38794..eff4ff4 100644 --- a/apps/api/src/modules/storage/entities/storage-status.entity.ts +++ b/apps/api/src/modules/storage/entities/storage-status.entity.ts @@ -1,3 +1,4 @@ +import type { StorageRoot, StorageStatus } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; import { StorageBackend } from '../interfaces/storage-provider.interface'; @@ -5,7 +6,7 @@ import { StorageBackend } from '../interfaces/storage-provider.interface'; /** * A browse root within a backend: a Drive folder or an S3 bucket. **/ -export class StorageRootEntity { +export class StorageRootEntity implements StorageRoot { @ApiProperty({ type: 'string', description: 'Opaque id used to browse into this root' }) id: string; @@ -13,7 +14,7 @@ export class StorageRootEntity { name: string; } -export class StorageStatusEntity { +export class StorageStatusEntity implements StorageStatus { @ApiProperty({ enum: ['drive', 's3'], description: 'Storage backend key' }) backend: StorageBackend; diff --git a/apps/api/src/modules/storage/entities/upload-result.entity.ts b/apps/api/src/modules/storage/entities/upload-result.entity.ts index 62ce5a4..5665afd 100644 --- a/apps/api/src/modules/storage/entities/upload-result.entity.ts +++ b/apps/api/src/modules/storage/entities/upload-result.entity.ts @@ -1,6 +1,7 @@ +import type { UploadResult } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class UploadResultEntity { +export class UploadResultEntity implements UploadResult { @ApiProperty({ type: 'string', description: 'Opaque id of the uploaded file (Drive id or S3 ref)', diff --git a/apps/api/src/modules/storage/entities/upload-status.entity.ts b/apps/api/src/modules/storage/entities/upload-status.entity.ts index 3b849d6..5c4688f 100644 --- a/apps/api/src/modules/storage/entities/upload-status.entity.ts +++ b/apps/api/src/modules/storage/entities/upload-status.entity.ts @@ -1,9 +1,10 @@ +import type { ResumableUploadStatus } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; /** * How far a resumable upload session got — lets the browser resume a dropped upload from where it left off **/ -export class UploadStatusEntity { +export class UploadStatusEntity implements ResumableUploadStatus { @ApiProperty({ type: 'boolean', description: 'Whether the upload already finished' }) complete: boolean; diff --git a/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts b/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts index 6663ab2..47dbe8a 100644 --- a/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts +++ b/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts @@ -1,3 +1,4 @@ +import type { StorageBackend } from '@dropto/types'; import { Archiver } from 'archiver'; import { Readable } from 'node:stream'; @@ -9,8 +10,9 @@ import { StorageStatusEntity } from '../entities/storage-status.entity'; import { UploadResultEntity } from '../entities/upload-result.entity'; import { UploadStatusEntity } from '../entities/upload-status.entity'; -/** The storage backends the workspace can browse. */ -export type StorageBackend = 'drive' | 's3'; +// The backend key is part of the wire contract, so it lives in @dropto/types; re-exported here +// so providers and the registry keep importing it from one place. +export type { StorageBackend }; /** All known backend keys, in the order they appear in the sidebar. */ export const STORAGE_BACKENDS: StorageBackend[] = ['drive', 's3']; diff --git a/apps/api/src/modules/storage/providers/google-drive.provider.ts b/apps/api/src/modules/storage/providers/google-drive.provider.ts index 296a76a..9920271 100644 --- a/apps/api/src/modules/storage/providers/google-drive.provider.ts +++ b/apps/api/src/modules/storage/providers/google-drive.provider.ts @@ -12,7 +12,11 @@ import { Archiver, ZipArchive } from 'archiver'; import { Auth, drive_v3, google } from 'googleapis'; import { Readable } from 'node:stream'; -import { AllowedFolderEntity } from '@/modules/google-auth/entities/allowed-folder.entity'; +import { TtlCache } from '@/common/utils/ttl-cache.functions'; +import { + AllowedFolderEntity, + toAllowedFolderEntity, +} from '@/modules/google-auth/entities/allowed-folder.entity'; import { GoogleAuthService } from '@/modules/google-auth/google-auth.service'; import { PrismaService } from '@/prisma/prisma.service'; @@ -48,6 +52,11 @@ const ITEM_MISSING_MESSAGE = 'This item no longer exists in Google Drive.'; // One network page. Small enough to keep first paint fast, big enough that most folders need one call. const PAGE_SIZE = 100; +/** How long a folder's parent list stays trusted. Ancestry changes only when a folder is moved. */ +const ANCESTOR_CACHE_TTL_MS = 60_000; + +const ANCESTOR_CACHE_MAX = 500; + /** * StorageProvider over the Drive API; every op is validated to stay inside the authorized folder tree. **/ @@ -55,6 +64,10 @@ const PAGE_SIZE = 100; export class GoogleDriveProvider implements StorageProvider { readonly backend: StorageBackend = 'drive'; + // Ancestry of *intermediate* folders only — the target item is always re-read, so a deleted item + // still 404s. Keyed by folder id; a moved folder is at worst one TTL stale. + private readonly ancestorCache = new TtlCache(ANCESTOR_CACHE_TTL_MS, ANCESTOR_CACHE_MAX); + private readonly logger = new Logger(GoogleDriveProvider.name); constructor( @@ -215,9 +228,19 @@ export class GoogleDriveProvider implements StorageProvider { } visited.add(id); + const cached = this.ancestorCache.get(id); + + if (cached) { + next.push(...cached); + continue; + } + try { const res = await drive.files.get({ fileId: id, fields: 'id, parents' }); - next.push(...(res.data.parents ?? [])); + const parents = res.data.parents ?? []; + + this.ancestorCache.set(id, parents); + next.push(...parents); } catch { // Not reachable (deleted / no access) — treat as not allowed. continue; @@ -277,7 +300,10 @@ export class GoogleDriveProvider implements StorageProvider { orderBy: { createdAt: 'asc' }, }); - return this.pruneMissingRoots(drive, driveAccountId, folders); + // Deliberately uncached: this call's whole job is to tell the sidebar which roots are still + // live in Drive, and it is already one parallel files.get per root. Caching it made a root + // deleted in Drive linger in the list (an e2e test caught exactly that). + return this.pruneMissingRoots(drive, driveAccountId, folders.map(toAllowedFolderEntity)); } async listContents(folderId: string, options: ListContentsOptions = {}): Promise { diff --git a/apps/api/src/modules/storage/providers/s3-storage.provider.ts b/apps/api/src/modules/storage/providers/s3-storage.provider.ts index f181b5a..30f8956 100644 --- a/apps/api/src/modules/storage/providers/s3-storage.provider.ts +++ b/apps/api/src/modules/storage/providers/s3-storage.provider.ts @@ -218,7 +218,8 @@ export class S3StorageProvider implements StorageProvider { id: encodeId({ bucket, key: '' }), folderId: encodeId({ bucket, key: '' }), name: bucket, - createdAt: new Date(0), + // Buckets carry no creation date of their own; the epoch keeps the shape uniform. + createdAt: new Date(0).toISOString(), })); } diff --git a/apps/api/src/prisma/seed.ts b/apps/api/src/prisma/seed.ts deleted file mode 100644 index 4d4dd44..0000000 --- a/apps/api/src/prisma/seed.ts +++ /dev/null @@ -1,29 +0,0 @@ -import 'dotenv/config'; -import { Logger } from '@nestjs/common'; -import { PrismaPg } from '@prisma/adapter-pg'; - -import { databaseConfig } from '@/config/database.config'; - -import { PrismaClient } from './generated/prisma/client'; - -const logger = new Logger('Seed'); - -const adapter = new PrismaPg({ - connectionString: databaseConfig().url, -}); - -const prisma = new PrismaClient({ adapter }); - -async function main() { - // No seed data required — accounts/folders are created at runtime via the OAuth + Picker flow. -} - -main() - .then(async () => await prisma.$disconnect()) - .catch(async (error) => { - logger.error(`Error during seeding: ${error}`); - - await prisma.$disconnect(); - - process.exit(1); - }); diff --git a/apps/api/test/helpers/fixtures.helper.ts b/apps/api/test/helpers/fixtures.helper.ts index 6dfc10a..e5efe89 100644 --- a/apps/api/test/helpers/fixtures.helper.ts +++ b/apps/api/test/helpers/fixtures.helper.ts @@ -1,6 +1,6 @@ import { CryptoService } from '@/common/services/crypto/crypto.service'; -// Mirrors TOKEN_ENCRYPTION_KEY from setup-env.ts (all-zero 32-byte key). +// Mirrors TOKEN_ENCRYPTION_KEY from .env.test (all-zero 32-byte key; see .env.test.example). const TEST_ENCRYPTION_KEY = Buffer.from('0'.repeat(64), 'hex'); /** A refresh token encrypted with the test key so the real CryptoService can decrypt it. */ diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json index a860874..ae12f2b 100644 --- a/apps/api/test/jest-e2e.json +++ b/apps/api/test/jest-e2e.json @@ -1,14 +1,25 @@ { - "moduleFileExtensions": ["js", "json", "ts"], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], "rootDir": "..", - "roots": ["/test"], + "roots": [ + "/test" + ], "testRegex": ".e2e-spec\\.ts$", "transformIgnorePatterns": [ - "/node_modules/.pnpm/(?!(cookie|archiver|compress-commons|crc32-stream|is-stream|zip-stream)@)", + "/node_modules/", "\\.pnp\\.[^/]+$" ], "transform": { - "^.+\\.(t|j)s$": ["ts-jest", { "tsconfig": "/tsconfig.spec.json" }] + "^.+\\.(t|j)s$": [ + "ts-jest", + { + "tsconfig": "/tsconfig.spec.json" + } + ] }, "moduleNameMapper": { "^(\\.{1,2}/.*)\\.js$": "$1", @@ -17,7 +28,9 @@ "^src/(.*)$": "/src/$1" }, "testEnvironment": "node", - "setupFiles": ["/test/setup-env.ts"], + "setupFiles": [ + "/test/setup-env.ts" + ], "testTimeout": 30000, "maxWorkers": 1, "forceExit": true diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 712047d..5606aa4 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -5,7 +5,7 @@ "moduleResolution": "NodeNext", "ignoreDeprecations": "6.0", "target": "ES2025", - "types": ["node", "jest"], + "types": ["node"], "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, @@ -14,6 +14,7 @@ "rootDir": "./src", "outDir": "./dist", "incremental": true, + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo", "strictPropertyInitialization": false, "paths": { "@/*": ["./src/*"], diff --git a/apps/web/package.json b/apps/web/package.json index 33e1fdb..a28bb42 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,10 +10,10 @@ }, "dependencies": { "@dropto/types": "workspace:*", - "axios": "^1.19.0", - "framer-motion": "^12.43.0", - "lucide-react": "^1.28.0", - "next": "16.2.12", + "axios": "^1.20.0", + "lucide-react": "^1.38.0", + "motion": "^13.1.1", + "next": "16.3.3", "next-themes": "^0.4.6", "react": "19.2.8", "react-dom": "19.2.8", @@ -21,10 +21,10 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.3.3", - "@types/node": "^26.1.2", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "eslint": "^10.8.0", + "@types/node": "^26.4.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "eslint": "^10.9.1", "prettier": "^3.9.6", "prettier-plugin-tailwindcss": "^0.8.1", "tailwindcss": "^4.3.3", diff --git a/apps/web/prettier.config.ts b/apps/web/prettier.config.ts index 796cd7c..eaf6664 100644 --- a/apps/web/prettier.config.ts +++ b/apps/web/prettier.config.ts @@ -1,15 +1,10 @@ import type { Config } from 'prettier'; -// Mirrors the root workspace config, adding the Tailwind class-sorting plugin. +import baseConfig from '../../prettier.config'; + +// The workspace config plus the Tailwind class-sorting plugin (web-only). const config: Config = { - tabWidth: 2, - printWidth: 100, - endOfLine: 'auto', - arrowParens: 'always', - semi: true, - singleQuote: true, - jsxSingleQuote: true, - bracketSameLine: true, + ...baseConfig, plugins: ['prettier-plugin-tailwindcss'], }; diff --git a/apps/web/src/actions/storage/storage.actions.ts b/apps/web/src/actions/storage/storage.actions.ts index 721e89d..34b980b 100644 --- a/apps/web/src/actions/storage/storage.actions.ts +++ b/apps/web/src/actions/storage/storage.actions.ts @@ -5,10 +5,10 @@ import type { DriveEntryPage, ListContentsQuery, ResumableUploadSession, + ResumableUploadStatus, StorageBackend, StorageStatus, UploadResult, - UploadStatus, } from '@dropto/types'; import { @@ -92,7 +92,7 @@ export async function uploadStatusAction( backend: StorageBackend, uploadUrl: string, size: number, -): Promise> { +): Promise> { return runAction(() => getUploadStatus(backend, uploadUrl, size)); } diff --git a/apps/web/src/common/services/api/storage.api.ts b/apps/web/src/common/services/api/storage.api.ts index e2aac7f..0fec545 100644 --- a/apps/web/src/common/services/api/storage.api.ts +++ b/apps/web/src/common/services/api/storage.api.ts @@ -4,10 +4,10 @@ import type { DriveEntryPage, ListContentsQuery, ResumableUploadSession, + ResumableUploadStatus, StorageBackend, StorageStatus, UploadResult, - UploadStatus, } from '@dropto/types'; import { getHttp } from '@/common/services/axios/axios.instance'; @@ -119,9 +119,9 @@ export const getUploadStatus = async ( backend: StorageBackend, uploadUrl: string, size: number, -): Promise => { +): Promise => { const http = await getHttp(); - const { data } = await http.post(`/storage/${backend}/upload-status`, { + const { data } = await http.post(`/storage/${backend}/upload-status`, { uploadUrl, size, }); diff --git a/apps/web/src/common/services/picker/usePicker.ts b/apps/web/src/common/services/picker/usePicker.ts index 000bb3a..3faccc9 100644 --- a/apps/web/src/common/services/picker/usePicker.ts +++ b/apps/web/src/common/services/picker/usePicker.ts @@ -11,14 +11,54 @@ export interface PickedFolder { name: string; } -// gapi/Picker globals have no bundled types. +/** A document handed back by the Picker (only the fields this hook reads). */ +interface PickerDocument { + id: string; + name?: string; +} + +/** Payload of the Picker's callback. */ +interface PickerCallbackData { + action: string; + docs?: PickerDocument[]; +} + +/** Chainable Picker view builder. */ +interface PickerView { + setSelectFolderEnabled: (enabled: boolean) => PickerView; + setIncludeFolders: (include: boolean) => PickerView; + setOwnedByMe: (ownedByMe: boolean) => PickerView; + setMimeTypes: (mimeTypes: string) => PickerView; +} + +/** Chainable Picker builder. */ +interface PickerBuilder { + addView: (view: PickerView) => PickerBuilder; + enableFeature: (feature: string) => PickerBuilder; + setOAuthToken: (token: string) => PickerBuilder; + setDeveloperKey: (apiKey: string) => PickerBuilder; + setAppId: (appId: string) => PickerBuilder; + setCallback: (callback: (data: PickerCallbackData) => void) => PickerBuilder; + build: () => { setVisible: (visible: boolean) => void }; +} + +/** The slice of the `google.picker` namespace this hook drives. */ +interface PickerApi { + DocsView: new (viewId: string) => PickerView; + PickerBuilder: new () => PickerBuilder; + ViewId: { FOLDERS: string }; + Feature: { MULTISELECT_ENABLED: string }; + Action: { PICKED: string }; +} + +// gapi/Picker ship no bundled types, so the globals are declared structurally. declare global { interface Window { gapi?: { load: (name: string, callback: () => void) => void; }; google?: { - picker: any; + picker: PickerApi; }; } } @@ -99,14 +139,14 @@ export function usePicker() { .enableFeature(picker.Feature.MULTISELECT_ENABLED) .setOAuthToken(token) .setDeveloperKey(apiKey) - .setCallback((data: any) => { + .setCallback((data) => { if (data.action !== picker.Action.PICKED) { return; } - const folders: PickedFolder[] = (data.docs ?? []).map((doc: any) => ({ - folderId: doc.id as string, - name: (doc.name as string) ?? 'Untitled', + const folders: PickedFolder[] = (data.docs ?? []).map((doc) => ({ + folderId: doc.id, + name: doc.name ?? 'Untitled', })); onPicked(folders); diff --git a/apps/web/src/common/utils/jwt.functions.ts b/apps/web/src/common/utils/jwt.functions.ts index 13682c0..69d4fd6 100644 --- a/apps/web/src/common/utils/jwt.functions.ts +++ b/apps/web/src/common/utils/jwt.functions.ts @@ -8,7 +8,7 @@ interface JwtPayload { /** * Decodes a JWT payload WITHOUT verifying its signature (proxy reads `exp` only). **/ -export const decodeJwtPayload = (token: string | undefined): JwtPayload | null => { +const decodeJwtPayload = (token: string | undefined): JwtPayload | null => { if (!token) { return null; } diff --git a/apps/web/src/components/common/Modal.tsx b/apps/web/src/components/common/Modal.tsx index 75204b7..22ea195 100644 --- a/apps/web/src/components/common/Modal.tsx +++ b/apps/web/src/components/common/Modal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useEffect } from 'react'; import type { ExtendedProps } from '@/common/types/global.types'; diff --git a/apps/web/src/components/common/ThemeToggle.tsx b/apps/web/src/components/common/ThemeToggle.tsx index 9008252..508c56d 100644 --- a/apps/web/src/components/common/ThemeToggle.tsx +++ b/apps/web/src/components/common/ThemeToggle.tsx @@ -1,6 +1,6 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useTheme } from 'next-themes'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; diff --git a/apps/web/src/components/forms/LoginForm.tsx b/apps/web/src/components/forms/LoginForm.tsx index de5f973..b2fd66f 100644 --- a/apps/web/src/components/forms/LoginForm.tsx +++ b/apps/web/src/components/forms/LoginForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { motion } from 'framer-motion'; +import { motion } from 'motion/react'; import { useRouter } from 'next/navigation'; import { type FormEvent, type KeyboardEvent, useState } from 'react'; diff --git a/apps/web/src/components/layouts/AccountSidebarContent.tsx b/apps/web/src/components/layouts/AccountSidebarContent.tsx index 2147304..1e3f1a4 100644 --- a/apps/web/src/components/layouts/AccountSidebarContent.tsx +++ b/apps/web/src/components/layouts/AccountSidebarContent.tsx @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend, StorageStatus } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import Image from 'next/image'; import { type ReactNode, useEffect, useState } from 'react'; diff --git a/apps/web/src/components/layouts/Breadcrumb.tsx b/apps/web/src/components/layouts/Breadcrumb.tsx index 11f1504..80f4dde 100644 --- a/apps/web/src/components/layouts/Breadcrumb.tsx +++ b/apps/web/src/components/layouts/Breadcrumb.tsx @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend, StorageStatus } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; diff --git a/apps/web/src/components/layouts/FileBrowser.tsx b/apps/web/src/components/layouts/FileBrowser.tsx index 251d818..1edea10 100644 --- a/apps/web/src/components/layouts/FileBrowser.tsx +++ b/apps/web/src/components/layouts/FileBrowser.tsx @@ -1,6 +1,6 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { type ChangeEvent, type DragEvent, @@ -13,6 +13,7 @@ import { } from 'react'; import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; +import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; import { useMediaQuery } from '@/common/hooks/useMediaQuery'; import type { Crumb, @@ -30,6 +31,9 @@ import Breadcrumb, { type BreadcrumbStoragePicker } from './Breadcrumb'; /** Custom DataTransfer type marking an internal drag-to-move (vs an OS file drop). */ const MOVE_MIME = 'application/x-dropto-move'; +/** Delay before a typed query re-filters the list; the input itself stays instant. */ +const FILTER_DEBOUNCE_MS = 150; + /** Dropdown-menu open/close animation (fade + zoom + slight slide), shadcn-style. */ const MENU_MOTION = { initial: { opacity: 0, scale: 0.96, y: -4 }, @@ -291,8 +295,10 @@ export default function FileBrowser({ }; }, [menu, toolbarMenu]); - // Instant client-side filter (superset of Drive's server-side filter; also covers roots + S3). - const query = searchQuery.trim().toLowerCase(); + // Client-side filter (superset of Drive's server-side filter; also covers roots + S3). Debounced + // so a keystroke only re-renders the input: without it every character re-filtered, re-sorted and + // re-rendered the whole list, in both panes. + const query = useDebouncedValue(searchQuery.trim().toLowerCase(), FILTER_DEBOUNCE_MS); const sorted = useMemo(() => { const dir = sortDir === 'asc' ? 1 : -1; const list = query diff --git a/apps/web/src/components/layouts/PreviewPanel.tsx b/apps/web/src/components/layouts/PreviewPanel.tsx index aa10858..4298017 100644 --- a/apps/web/src/components/layouts/PreviewPanel.tsx +++ b/apps/web/src/components/layouts/PreviewPanel.tsx @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useState } from 'react'; import { fileDownloadUrl } from '@/common/services/api/storage.client'; diff --git a/apps/web/src/components/layouts/UploadDock.tsx b/apps/web/src/components/layouts/UploadDock.tsx index a4b3c8d..d731d8d 100644 --- a/apps/web/src/components/layouts/UploadDock.tsx +++ b/apps/web/src/components/layouts/UploadDock.tsx @@ -1,6 +1,6 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useState } from 'react'; import { useOnlineStatus } from '@/common/hooks/useOnlineStatus'; diff --git a/apps/web/src/components/layouts/mobile/MobileDrawer.tsx b/apps/web/src/components/layouts/mobile/MobileDrawer.tsx index 95944bc..2ee58dd 100644 --- a/apps/web/src/components/layouts/mobile/MobileDrawer.tsx +++ b/apps/web/src/components/layouts/mobile/MobileDrawer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useEffect } from 'react'; import AccountSidebarContent, { type AccountSidebarProps } from '../AccountSidebarContent'; diff --git a/apps/web/src/components/providers/ToastProvider.tsx b/apps/web/src/components/providers/ToastProvider.tsx index 6fb8df9..13ce09e 100644 --- a/apps/web/src/components/providers/ToastProvider.tsx +++ b/apps/web/src/components/providers/ToastProvider.tsx @@ -1,7 +1,7 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; import { CircleAlert, CircleCheck } from 'lucide-react'; +import { AnimatePresence, motion } from 'motion/react'; import { createContext, type PropsWithChildren, diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 2063a5f..ab937a3 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -27,5 +27,7 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": ["node_modules"] + // prettier.config.ts extends the workspace config, which is outside the Docker build + // context; it is tooling config, not app code, so keep it out of the Next type-check. + "exclude": ["node_modules", "prettier.config.ts"] } diff --git a/docker-compose.infra.yml b/docker-compose.infra.yml deleted file mode 100644 index ecff32f..0000000 --- a/docker-compose.infra.yml +++ /dev/null @@ -1,55 +0,0 @@ -# docker-compose.infra.yml -# Infrastructure services only (Postgres + Redis) — for local development where the apps run -# natively. For the full containerised deployment use docker-compose.yml instead. -# -# Usage: -# docker compose -f docker-compose.infra.yml up -d -# pnpm --filter api run prisma:migrate:deploy # apply migrations -# pnpm dev # apps run directly and connect to localhost ports - -services: - dropto-pg: - image: postgres:17-alpine - container_name: dropto-pg - restart: unless-stopped - environment: - POSTGRES_USER: '${DB_USER}' - POSTGRES_PASSWORD: '${DB_PASS}' - POSTGRES_DB: '${DB_NAME}' - ports: - - '127.0.0.1:${DB_PORT:-5432}:5432' - volumes: - - dropto_pg_data:/var/lib/postgresql/data - networks: - - dropto_net - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}'] - interval: 10s - timeout: 5s - retries: 5 - - dropto-redis: - image: redis:7-alpine - container_name: dropto-redis - restart: unless-stopped - command: ['redis-server', '--requirepass', '${REDIS_PASS}'] - ports: - - '127.0.0.1:${REDIS_PORT:-6379}:6379' - volumes: - - dropto_redis_data:/data - networks: - - dropto_net - healthcheck: - test: ['CMD', 'redis-cli', '-a', '${REDIS_PASS}', 'ping'] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - dropto_pg_data: - dropto_redis_data: - -networks: - dropto_net: - driver: bridge - name: dropto_net diff --git a/eslint.config.base.mjs b/eslint.config.base.mjs index 2b49b37..eb14a94 100644 --- a/eslint.config.base.mjs +++ b/eslint.config.base.mjs @@ -1,6 +1,5 @@ import tsPlugin from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; -import importPlugin from 'eslint-plugin-import'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; import unusedImports from 'eslint-plugin-unused-imports'; @@ -8,7 +7,6 @@ import unusedImports from 'eslint-plugin-unused-imports'; export const parser = tsParser; export const plugins = { - import: importPlugin, 'unused-imports': unusedImports, 'simple-import-sort': simpleImportSort, '@typescript-eslint': tsPlugin, @@ -41,5 +39,4 @@ export const rules = { { groups: [['^\\u0000', '^@?\\w'], ['^@/'], ['^\\.'], ['^.+\\.(css|scss)$']] }, ], 'simple-import-sort/exports': 'error', - 'import/no-named-as-default-member': 'warn', }; diff --git a/package.json b/package.json index 5c828b4..95a263b 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "description": "Self-hosted file uploader that lets you route uploads through your own storage backends (Google Drive, S3, and more) instead of your app server's disk.", "private": true, "author": { - "name": "Petr Kašpar", + "name": "Petr Ka\u0161par", "url": "https://github.com/ksprptr" }, "packageManager": "pnpm@10.34.4", "engines": { - "node": ">=22", + "node": ">=22.12", "pnpm": ">=10" }, "scripts": { @@ -21,15 +21,13 @@ "prepare": "husky || true" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.65.0", - "@typescript-eslint/parser": "^8.65.0", - "eslint-plugin-import": "^2.32.0", + "@typescript-eslint/eslint-plugin": "^8.68.0", + "@typescript-eslint/parser": "^8.68.0", "eslint-plugin-simple-import-sort": "^14.0.0", "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "prettier": "^3.9.6", - "prettier-plugin-tailwindcss": "^0.8.1", - "turbo": "^2.10.7", + "turbo": "^2.10.12", "typescript": "^6.0.3" } } diff --git a/packages/types/index.ts b/packages/types/index.ts index 60918f5..d340ef4 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -50,7 +50,7 @@ export interface ResumableUploadSession { } /** How far a resumable session got — so a dropped upload resumes from `receivedBytes` (never re-uploads whole). */ -export interface UploadStatus { +export interface ResumableUploadStatus { complete: boolean; receivedBytes: number; fileId: string | null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf0bff8..6660eaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,32 +20,26 @@ importers: .: devDependencies: '@typescript-eslint/eslint-plugin': - specifier: ^8.65.0 - version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + specifier: ^8.68.0 + version: 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^8.65.0 - version: 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)) + specifier: ^8.68.0 + version: 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-simple-import-sort: specifier: ^14.0.0 - version: 14.0.0(eslint@10.8.0(jiti@2.7.0)) + version: 14.0.0(eslint@10.9.1(jiti@2.7.0)) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)) husky: specifier: ^9.1.7 version: 9.1.7 prettier: specifier: ^3.9.6 version: 3.9.6 - prettier-plugin-tailwindcss: - specifier: ^0.8.1 - version: 0.8.1(prettier@3.9.6) turbo: - specifier: ^2.10.7 - version: 2.10.7 + specifier: ^2.10.12 + version: 2.10.12 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -53,38 +47,41 @@ importers: apps/api: dependencies: '@aws-sdk/client-s3': - specifier: ^3.1098.0 - version: 3.1098.0 + specifier: ^3.1121.0 + version: 3.1121.0 '@aws-sdk/lib-storage': - specifier: ^3.1098.0 - version: 3.1098.0(@aws-sdk/client-s3@3.1098.0) + specifier: ^3.1121.0 + version: 3.1121.0(@aws-sdk/client-s3@3.1121.0) + '@dropto/types': + specifier: workspace:* + version: link:../../packages/types '@nestjs/common': - specifier: ^11.1.28 - version: 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^12.0.1 + version: 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^4.0.4 - version: 4.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^12.0.0 + version: 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/jwt': - specifier: ^11.0.2 - version: 11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': - specifier: ^11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1) '@nestjs/swagger': - specifier: ^11.4.6 - version: 11.4.6(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: ^11.1.1 - version: 11.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) '@prisma/adapter-pg': - specifier: ^7.9.1 - version: 7.9.1 + specifier: ^7.10.0 + version: 7.10.0 '@prisma/client': - specifier: ^7.9.1 - version: 7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) + specifier: ^7.10.0 + version: 7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) archiver: specifier: ^8.0.0 version: 8.0.0 @@ -110,17 +107,17 @@ importers: specifier: ^5.2.1 version: 5.2.1 googleapis: - specifier: ^173.0.0 - version: 173.0.0 + specifier: ^176.0.0 + version: 176.0.0 helmet: specifier: ^8.3.0 version: 8.3.0 ioredis: - specifier: ^5.11.1 - version: 5.11.1 + specifier: ^6.0.0 + version: 6.0.0 prisma: - specifier: ^7.9.1 - version: 7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + specifier: ^7.10.0 + version: 7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) rate-limiter-flexible: specifier: ^11.2.0 version: 11.2.0 @@ -132,14 +129,14 @@ importers: version: 7.8.2 devDependencies: '@nestjs/cli': - specifier: ^11.0.24 - version: 11.0.24(@types/node@26.1.2)(prettier@3.9.6) + specifier: ^12.0.0 + version: 12.0.0(@types/node@26.4.0)(fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.3)(webpack@5.106.2))(prettier@3.9.6)(tsconfig-paths-webpack-plugin@4.2.0)(webpack-node-externals@3.0.0)(webpack@5.106.2) '@nestjs/schematics': - specifier: ^11.1.0 - version: 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@6.0.3) + specifier: ^12.0.0 + version: 12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3) '@nestjs/testing': - specifier: ^11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-express@12.0.1) '@types/archiver': specifier: ^8.0.0 version: 8.0.0 @@ -156,17 +153,17 @@ importers: specifier: ^30.0.0 version: 30.0.0 '@types/node': - specifier: ^26.1.2 - version: 26.1.2 + specifier: ^26.4.0 + version: 26.4.0 '@types/supertest': specifier: ^7.2.1 version: 7.2.1 eslint: - specifier: ^10.8.0 - version: 10.8.0(jiti@2.7.0) + specifier: ^10.9.1 + version: 10.9.1(jiti@2.7.0) jest: - specifier: ^30.4.2 - version: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) + specifier: ^30.5.0 + version: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) prisma-dbml-generator: specifier: ^0.12.0 version: 0.12.0 @@ -175,10 +172,10 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.4.12 - version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)))(typescript@6.0.3) + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.5.0)(@jest/types@30.5.0)(babel-jest@30.5.0(@babel/core@7.29.7))(jest-util@30.5.0)(jest@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)))(typescript@6.0.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@26.1.2)(typescript@6.0.3) + version: 10.9.2(@types/node@26.4.0)(typescript@6.0.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -192,17 +189,17 @@ importers: specifier: workspace:* version: link:../../packages/types axios: - specifier: ^1.19.0 - version: 1.19.0 - framer-motion: - specifier: ^12.43.0 - version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^1.20.0 + version: 1.20.0 lucide-react: - specifier: ^1.28.0 - version: 1.28.0(react@19.2.8) + specifier: ^1.38.0 + version: 1.38.0(react@19.2.8) + motion: + specifier: ^13.1.1 + version: 13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next: - specifier: 16.2.12 - version: 16.2.12(@opentelemetry/api@1.4.1)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.3 + version: 16.3.3(@opentelemetry/api@1.4.1)(@types/node@26.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -220,17 +217,17 @@ importers: specifier: ^4.3.3 version: 4.3.3 '@types/node': - specifier: ^26.1.2 - version: 26.1.2 + specifier: ^26.4.0 + version: 26.4.0 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.5 + version: 19.2.5(@types/react@19.2.18) eslint: - specifier: ^10.8.0 - version: 10.8.0(jiti@2.7.0) + specifier: ^10.9.1 + version: 10.9.1(jiti@2.7.0) prettier: specifier: ^3.9.6 version: 3.9.6 @@ -256,113 +253,100 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@angular-devkit/core@19.2.24': - resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - peerDependencies: - chokidar: ^4.0.0 - peerDependenciesMeta: - chokidar: - optional: true - - '@angular-devkit/core@19.2.27': - resolution: {integrity: sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/core@22.1.5': + resolution: {integrity: sha512-HiY6d5dkIdJs5grP9OHvgkf14QOcDIo+hbuT7YKuLItQ++ZxCwUabJMLCCJ5R0KrstE5NqED0dNcyk5WD01t5Q==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: - chokidar: ^4.0.0 + chokidar: ^5.0.0 peerDependenciesMeta: chokidar: optional: true - '@angular-devkit/schematics-cli@19.2.27': - resolution: {integrity: sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics-cli@22.1.5': + resolution: {integrity: sha512-VB/5fH2b/2j8eeMm25vRYCJzBbpaM/VyHc8/+WqPzJahdWuvL0CeLWrxjHHT1exQ/kgbqHLaIWMgSywZe3EBhA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@19.2.24': - resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - - '@angular-devkit/schematics@19.2.27': - resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics@22.1.5': + resolution: {integrity: sha512-HTmo9y8wjXKtJGVTYomTjZsjWzhirpu1pPRAzsVob3eiYOuxv1qDRAgWiHXNGp5CpnbHunZweXY/WffNGOFRyA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@antfu/ni@0.21.4': resolution: {integrity: sha512-O0Uv9LbLDSoEg26fnMDdDRiPwFJnQSoD4WnrflDwKCJm8Cx/0mV4cGxwBLXan5mGIrpK4Dd7vizf4rQm0QCEAA==} hasBin: true - '@aws-sdk/checksums@3.1000.23': - resolution: {integrity: sha512-Hv4VX5+IdDYmAbOdiwZguz8fLh3WnE4VtyJwVNEHLulA+OYy7Z5L0ETGUlX2T4g8DLO8XxCV8CyWrtVL4uwqkg==} + '@aws-sdk/checksums@3.1000.29': + resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-s3@3.1098.0': - resolution: {integrity: sha512-6Z53QG2jdujsCt3eHO2cjCpdfvz2Pgs+hP/NINuGONZUJmdrHLLDvWM9F114Yq0NPIGtk+yA/GCjG3XiUhw+gA==} + '@aws-sdk/client-s3@3.1121.0': + resolution: {integrity: sha512-hBnoqaVBeWdkgXcJElMXA2yUZWkBCBntu2qmN+tfqmzC+j4LzJC3ox8qIgS2WdMS1cb8UwyBogUVrkRXybNm0A==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.977.3': - resolution: {integrity: sha512-6YZTpF5Zzl0KdSrztM0l6ErKdnXrnnodIDYfayHE9SFfZU8Ubxls7H2XnfWT121jgwiG+WP1z5kyfVDsH5V4qA==} + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.64': - resolution: {integrity: sha512-14kkR5aj1c7D+cYCrEcG2W3xw87wMYAEUeB/tMiQ2j0RVKzzdewd4mJw1+Q0JVLr4IFU1JHGDCyj0WqLrtIixg==} + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.66': - resolution: {integrity: sha512-BrZxoXScBZ+nTOYy388zHkz1n5cS8C+uGFMozD4w9OKEWMq39Cu/9l/k385Hb54dtv0VIeh12f8h67o3xDNH9g==} + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.973.9': - resolution: {integrity: sha512-d/mMvS+sQjSWNA6+JCldK1YB/jQmtoWM8Izj10mrOZdRCeWNVb8IYQuuyHHKKKXcpGo5Pd0LIK6onHbLvlhuVQ==} + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.71': - resolution: {integrity: sha512-l93922lXnTs5RjM3QrMCXSmXiEgLFLaQ0Lco9F3tJ08o0mzGdAj6m2nAXDuTMHoUuL0u7RKmrhm+Z8/7GCilyg==} + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.75': - resolution: {integrity: sha512-sdFhR4E3HWZefGL6OsBhxqXdqtFvxGOh8VLfiu9yttPgVNcpEozEDPmrTYmXA3CIf6npUETVLveQPGQ7GBzVhg==} + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.64': - resolution: {integrity: sha512-Q5noG5vfFo++bUaLGcjj8OHX5tmwa1O/5nDVMeaSSZgjzgvWbW6tgu/gSa+aENwcJXjoXDQh+8TRZR2zHds/aw==} + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.973.8': - resolution: {integrity: sha512-37okeZyRdVkGrU6nyKBPGqQvyl/7CEiMpUVOb0+BCNCIR0VAuBxNgjMb0mnsR1EXm8dqQFfQZOJ5YW/0sBfa1w==} + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.70': - resolution: {integrity: sha512-Ps/f9USafScb6CSQTRPNMzdKL5x5XRwDIRgFvnuFWfClGZe7/fI7iCFqXdxKjc9LBxP28E7iUgNloGgfgw406w==} + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} engines: {node: '>=20.0.0'} - '@aws-sdk/lib-storage@3.1098.0': - resolution: {integrity: sha512-GjVYr/nZAlBNVToxFlhPiQkmICcoMFVrH5qEezrmDrcs144vagrDUhW8sfcN6G5fYbdIRRuJstfGyWlHvX6R2g==} + '@aws-sdk/lib-storage@3.1121.0': + resolution: {integrity: sha512-sx86QqM0/HjKNCS5eQGcaP9Q8GKg3FtCExO/6FfGyw00JNUsFJPmEKNCJIkWY7OmKS9jIOuJe/DtAgfr/+G/bQ==} engines: {node: '>=20.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.1098.0 + '@aws-sdk/client-s3': ^3.1121.0 - '@aws-sdk/middleware-sdk-s3@3.972.69': - resolution: {integrity: sha512-ziSQA8AkB+u4K/5t/SStFbeoiNTG04reUeddCLNCAgmdRJGQ3+MJcpZOXXLdD6uyfCqSK1/R6004gRdXvHbYQA==} + '@aws-sdk/middleware-sdk-s3@3.972.75': + resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.997.38': - resolution: {integrity: sha512-jQDs+nxnmgo4+WPOmY373HIC8jx4KyYYMYDI74FWrzX3Ba9fYsV0dSA+7PNYNkhRbbRCRLre83tQ31F1ACCHkA==} + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.43': - resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1098.0': - resolution: {integrity: sha512-6cFAviffeqYGjrXn4FqGbWcTxB3nbpBpQXUm6MvnrWZThJaVBTHHnespHgSUh4yVLfzhipSh9H/XjzkHQd9P4g==} + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.974.2': - resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.37': - resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.3.0': @@ -381,8 +365,8 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.29.7': @@ -423,8 +407,8 @@ packages: 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==} + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} hasBin: true @@ -523,12 +507,12 @@ packages: 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==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -565,8 +549,8 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.2': - resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -577,12 +561,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - 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} @@ -631,309 +609,309 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} 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==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} 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==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/checkbox@4.3.2': - resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} - engines: {node: '>=18'} + '@inquirer/checkbox@5.2.3': + resolution: {integrity: sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} + '@inquirer/confirm@6.3.0': + resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} + '@inquirer/core@12.0.1': + resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/editor@4.2.23': - resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} - engines: {node: '>=18'} + '@inquirer/editor@5.3.1': + resolution: {integrity: sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/expand@4.0.23': - resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} - engines: {node: '>=18'} + '@inquirer/expand@5.1.3': + resolution: {integrity: sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/external-editor@1.0.3': - resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} - engines: {node: '>=18'} + '@inquirer/external-editor@3.0.4': + resolution: {integrity: sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} + '@inquirer/figures@2.0.8': + resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/input@4.3.1': - resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} - engines: {node: '>=18'} + '@inquirer/input@5.1.4': + resolution: {integrity: sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/number@3.0.23': - resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} - engines: {node: '>=18'} + '@inquirer/number@4.2.1': + resolution: {integrity: sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/password@4.0.23': - resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} - engines: {node: '>=18'} + '@inquirer/password@5.2.0': + resolution: {integrity: sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@7.10.1': - resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} - engines: {node: '>=18'} + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@7.3.2': - resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} - engines: {node: '>=18'} + '@inquirer/prompts@8.7.0': + resolution: {integrity: sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/rawlist@4.1.11': - resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} - engines: {node: '>=18'} + '@inquirer/rawlist@5.3.3': + resolution: {integrity: sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/search@3.2.2': - resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} - engines: {node: '>=18'} + '@inquirer/search@4.3.1': + resolution: {integrity: sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/select@4.4.2': - resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} - engines: {node: '>=18'} + '@inquirer/select@5.2.3': + resolution: {integrity: sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} + '@inquirer/type@4.1.0': + resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@ioredis/commands@1.10.0': - resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@ioredis/commands@2.0.0': + resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -947,12 +925,12 @@ packages: resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} - '@jest/console@30.4.1': - resolution: {integrity: sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==} + '@jest/console@30.5.0': + resolution: {integrity: sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/core@30.4.2': - resolution: {integrity: sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==} + '@jest/core@30.5.0': + resolution: {integrity: sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -960,40 +938,40 @@ packages: node-notifier: optional: true - '@jest/diff-sequences@30.4.0': - resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} + '@jest/diff-sequences@30.5.0': + resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment@30.4.1': - resolution: {integrity: sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==} + '@jest/environment@30.5.0': + resolution: {integrity: sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect-utils@30.4.1': - resolution: {integrity: sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==} + '@jest/expect-utils@30.5.0': + resolution: {integrity: sha512-5j0ztPxSy3McUJihjkDdCyCfjvT2hxykFTWsgEBZKB8qsw9ALdCiGTpTRH5gnf/d+qI4SflYUJ0dWNbzjQCWbA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@30.4.1': - resolution: {integrity: sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==} + '@jest/expect@30.5.0': + resolution: {integrity: sha512-jEmgmgJEobJ3zEhDOGp1VAJ6JkoVelpS8uZ1ae1Ul/5lP78UKJKmmU0lciJwd6JdnqOXHaaS/QCKwbf1dHI9MA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/fake-timers@30.4.1': - resolution: {integrity: sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==} + '@jest/fake-timers@30.5.0': + resolution: {integrity: sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + '@jest/get-type@30.5.0': + resolution: {integrity: sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@30.4.1': - resolution: {integrity: sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==} + '@jest/globals@30.5.0': + resolution: {integrity: sha512-h7eJx534czwL8lQMYB0hwLT4/HquO8EX/RtYL7RNUHyUyWWVciYjoudN4Ns5JmNvn2/jh0Vm9UstZjEzJJ5EsQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/pattern@30.4.0': - resolution: {integrity: sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==} + '@jest/pattern@30.5.0': + resolution: {integrity: sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@30.4.1': - resolution: {integrity: sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==} + '@jest/reporters@30.5.0': + resolution: {integrity: sha512-FEAuusWm+PUOn9ydjaHhpOpyPmS6IbGE04HaKTuUI4zd8eqYZiXiNLoCsdCog/l2XnNj53E9pFy64UltcvfJKg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1001,32 +979,32 @@ packages: node-notifier: optional: true - '@jest/schemas@30.4.1': - resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + '@jest/schemas@30.5.0': + resolution: {integrity: sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/snapshot-utils@30.4.1': - resolution: {integrity: sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==} + '@jest/snapshot-utils@30.5.0': + resolution: {integrity: sha512-iWQtIsi2dRsO2oWzVceOeynuRJiYTW8gsDVp5wFQ02ipHluQsNgBpasWSHiawVxukIlsGLdCtCSbIEK7fPtpvQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/source-map@30.0.1': - resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + '@jest/source-map@30.5.0': + resolution: {integrity: sha512-xWpTJP9D0bDFGbPGT8XuWSwwha/iHADyyKzUnMx4UbdgnHugxrDaQFO4RZ8x4ZsFzRP6pNii8uvlgKCDxCIuDg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-result@30.4.1': - resolution: {integrity: sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==} + '@jest/test-result@30.5.0': + resolution: {integrity: sha512-9IlPqUzUMkVDmoDqSSrVVLroVotgN3hTUPWPwq24XWXhh1Zpg915RXZ5pgRJ/7j4YE/kng5nqjSn4p3S68SZJA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-sequencer@30.4.1': - resolution: {integrity: sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==} + '@jest/test-sequencer@30.5.0': + resolution: {integrity: sha512-TXlvSDIVv482b83hD8A8WtxDJEmGF47f60W6jRXOQL0Isfs3hKtk4rZIm9R/jLzAibg8+c56y+Q00BQs8R7Xcg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/transform@30.4.1': - resolution: {integrity: sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==} + '@jest/transform@30.5.0': + resolution: {integrity: sha512-n1cYhoByyULEIXi64wbT4Lq91qeT1E6bwpM//sprFXhw955qaiHTdAmy1c1rNFGB6fCf1J+nxDUSf3RGwgZP5A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/types@30.4.1': - resolution: {integrity: sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==} + '@jest/types@30.5.0': + resolution: {integrity: sha512-s1N+79S4Yp9ZgklCauZXi+YPJdCdtStNYQT32stuD6EeQaIBGHoUfyj2P0YWy8RmuQfaJboO+ulxEvEheR/POQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jridgewell/gen-mapping@0.3.13': @@ -1042,8 +1020,8 @@ packages: '@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/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -1058,27 +1036,46 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - '@nestjs/cli@11.0.24': - resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==} + '@nestjs/cli@12.0.0': + resolution: {integrity: sha512-DxgGUKA4gj92lU2lF23Mlvm6y7AQBKEBuanNKNcmHeP6Cn3yqPZxN0LqyaJys25JCqO1bN8KgE3LICrvYlB93g==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 - '@swc/core': ^1.3.62 + '@rspack/core': ^1.7.7 || ^2.1.10 + '@swc/cli': ^0.8.0 + '@swc/core': ^1.15.18 + fork-ts-checker-webpack-plugin: ^9.1.0 + ts-loader: ^9.5.4 + tsconfig-paths-webpack-plugin: ^4.2.0 + webpack: ^5.105.4 + webpack-node-externals: ^3.0.0 peerDependenciesMeta: + '@rspack/core': + optional: true '@swc/cli': optional: true '@swc/core': optional: true + fork-ts-checker-webpack-plugin: + optional: true + ts-loader: + optional: true + tsconfig-paths-webpack-plugin: + optional: true + webpack: + optional: true + webpack-node-externals: + optional: true - '@nestjs/common@11.1.28': - resolution: {integrity: sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==} + '@nestjs/common@12.0.1': + resolution: {integrity: sha512-v0zTaRCTV2K2xSnb3GnJoQKtaR6VxouJtm+P2gpr5w61dlXeWpXl1WVknCSzUqx2QwTV10tBkHETxvQvkf2Exg==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -1090,20 +1087,20 @@ packages: class-validator: optional: true - '@nestjs/config@4.0.4': - resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + '@nestjs/config@12.0.0': + resolution: {integrity: sha512-pG5+fFfCo3cdgVkLJLvvhUGfxEjwzv9hIAQ1aJVHvHlHVniDWfwX5pR/utgoAkJzSikv2oTR1BmIF03FXdi42Q==} peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^11.0.0 || ^12.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.28': - resolution: {integrity: sha512-06m63xIRj8+l8uOeh/8LnYupGubkyu4f+bPKIadaSui6vK9KpXgoz7HveT1yOVLcEt0M0oCOEW5EuEXZkEmBBQ==} + '@nestjs/core@12.0.1': + resolution: {integrity: sha512-rU6tAi8vDdyzHgN0iW0J4UJvziroOqzHqzlqL7phOSPizaXVEePfv+OUOsdJEOh0Qd14mslc3udOheLrAEcZow==} engines: {node: '>= 20'} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/microservices': ^11.0.0 - '@nestjs/platform-express': ^11.0.0 - '@nestjs/websockets': ^11.0.0 + '@nestjs/common': ^12.0.0 + '@nestjs/microservices': ^12.0.0 + '@nestjs/platform-express': ^12.0.0 + '@nestjs/websockets': ^12.0.0 reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -1114,15 +1111,16 @@ packages: '@nestjs/websockets': optional: true - '@nestjs/jwt@11.0.2': - resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + '@nestjs/jwt@12.0.1': + resolution: {integrity: sha512-AOvjo0JIuVMNr+6JgbEz4fP+4afclGR5ffZnHtjOFs4+HhzerUYKcIVUL06u5uQYQi/ra8WHO+NZR7OFdQeGvA==} peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 - '@nestjs/mapped-types@2.1.1': - resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + '@nestjs/mapped-types@12.0.0': + resolution: {integrity: sha512-aX7lxZcqXldtnSr0bgT2ZPQvpTpIwBMW1GMX29o4s3q02vmq6JoTYuPW0GwYXa8BiFMRX7WbGZVDA703pNqOMQ==} + engines: {node: '>=20.19.0'} peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 || ^12.0.0 class-transformer: ^0.4.0 || ^0.5.0 class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 reflect-metadata: ^0.1.12 || ^0.2.0 @@ -1132,27 +1130,29 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.28': - resolution: {integrity: sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==} + '@nestjs/platform-express@12.0.1': + resolution: {integrity: sha512-nF8bRgROptMyixYBfiJUAsjj2o9k13zxAjUKYaqHTPk8mTSd+gk7hXz6NykXNrfHWmMEYC9RttQlMUdMHNTbuA==} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 - '@nestjs/schematics@11.1.0': - resolution: {integrity: sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==} + '@nestjs/schematics@12.0.0': + resolution: {integrity: sha512-ilsxam7i+SWd2psr12faLVSk86FYKnxRUGO6jAGNi1P9+O0kRaYVOScyrKeIj97W9T9UJoJF38fg+7KgjY/nOg==} + engines: {node: '>=22.12.0'} peerDependencies: prettier: ^3.0.0 - typescript: '>=4.8.2' + typescript: '>=6.0.0' peerDependenciesMeta: prettier: optional: true - '@nestjs/swagger@11.4.6': - resolution: {integrity: sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==} + '@nestjs/swagger@12.0.1': + resolution: {integrity: sha512-fQ0WyutSSHedO9HzSkOFKFaRygYNGBno5EggcZljs+KL8tHzPPTO5oyJpaf3BoOKFGGsN/0B13EbIpH2pkPx8g==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 - '@nestjs/common': ^11.0.1 - '@nestjs/core': ^11.0.1 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 class-transformer: '*' class-validator: '*' reflect-metadata: ^0.1.12 || ^0.2.0 @@ -1212,70 +1212,70 @@ packages: typeorm: optional: true - '@nestjs/testing@11.1.28': - resolution: {integrity: sha512-B+VgRxeLaH7jkOMgAyUP3N3rpFlisQ7JRxixRbgHvG6a0VgKbbkNSofKExexCgKmQQak80undb3+2kE1lUBmRQ==} + '@nestjs/testing@12.0.1': + resolution: {integrity: sha512-prf3fsUaoKS6t04XcQt5aSTp68Po1GxUcbEBPv+3iqAef07hKQEO15r0jmmwA+rWZfONYT1UcV3YIylR++/tCg==} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 - '@nestjs/microservices': ^11.0.0 - '@nestjs/platform-express': ^11.0.0 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 + '@nestjs/microservices': ^12.0.0 + '@nestjs/platform-express': ^12.0.0 peerDependenciesMeta: '@nestjs/microservices': optional: true '@nestjs/platform-express': optional: true - '@next/env@16.2.12': - resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} + '@next/env@16.3.3': + resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} - '@next/swc-darwin-arm64@16.2.12': - resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} + '@next/swc-darwin-arm64@16.3.3': + resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.12': - resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} + '@next/swc-darwin-x64@16.3.3': + resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.12': - resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} + '@next/swc-linux-arm64-gnu@16.3.3': + resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.12': - resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} + '@next/swc-linux-arm64-musl@16.3.3': + resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.12': - resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} + '@next/swc-linux-x64-gnu@16.3.3': + resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.12': - resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} + '@next/swc-linux-x64-musl@16.3.3': + resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.12': - resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} + '@next/swc-win32-arm64-msvc@16.3.3': + resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.12': - resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} + '@next/swc-win32-x64-msvc@16.3.3': + resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1303,6 +1303,88 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1311,14 +1393,14 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@prisma/adapter-pg@7.9.1': - resolution: {integrity: sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==} + '@prisma/adapter-pg@7.10.0': + resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==} - '@prisma/client-runtime-utils@7.9.1': - resolution: {integrity: sha512-mVIBGYdO5CFmK0HvjxrtfIyQQcPdb88pSCeVQriVQPVZyDovIWblpHfOgcS8QO187j3QF0ePArH8qPhp0AU2vg==} + '@prisma/client-runtime-utils@7.10.0': + resolution: {integrity: sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==} - '@prisma/client@7.9.1': - resolution: {integrity: sha512-+xgrh2EhJVF79wC0yX5G4PI1Rdcm7Qn/nekNQ+t/O153wtNggruHal+fXHSa0QE+Tp/Cw5wvxeCEhZZ59xGm8Q==} + '@prisma/client@7.10.0': + resolution: {integrity: sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==} engines: {node: ^20.19 || ^22.12 || >=24.0} peerDependencies: prisma: '*' @@ -1329,38 +1411,38 @@ packages: typescript: optional: true - '@prisma/config@7.9.1': - resolution: {integrity: sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==} + '@prisma/config@7.10.0': + resolution: {integrity: sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==} '@prisma/debug@5.0.0': resolution: {integrity: sha512-3q/M/KqlQ01/HJXifU/zCNOHkoTWu24kGelMF/IBrRxm7njPqTTbwfnT1dh4JK+nuWM5/Dg1Lv00u2c0l7AHxg==} + '@prisma/debug@7.10.0': + resolution: {integrity: sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==} + '@prisma/debug@7.2.0': resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} - '@prisma/debug@7.9.1': - resolution: {integrity: sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==} - '@prisma/dev@0.24.17': resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==} - '@prisma/driver-adapter-utils@7.9.1': - resolution: {integrity: sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==} + '@prisma/driver-adapter-utils@7.10.0': + resolution: {integrity: sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==} - '@prisma/engines-version@7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad': - resolution: {integrity: sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==} + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': + resolution: {integrity: sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==} '@prisma/engines@5.0.0': resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==} - '@prisma/engines@7.9.1': - resolution: {integrity: sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==} + '@prisma/engines@7.10.0': + resolution: {integrity: sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==} '@prisma/fetch-engine@5.0.0': resolution: {integrity: sha512-eSzHTE0KcMvM5+O1++eaMuVf4D1zwWHdqjWr6D70skCg37q7RYsuty4GFnlWBuqC4aXwVf06EvIxiJ0SQIIeRw==} - '@prisma/fetch-engine@7.9.1': - resolution: {integrity: sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==} + '@prisma/fetch-engine@7.10.0': + resolution: {integrity: sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==} '@prisma/generator-helper@5.0.0': resolution: {integrity: sha512-pufQ1mhoH6WzKNtzL79HZDoW4Ql3Lf8QEKVmBoW8e3Tdb50bxpYBYue5LBqp9vNW1xd1pgZO53cNiRfLX2d4Zg==} @@ -1368,12 +1450,12 @@ packages: '@prisma/get-platform@5.0.0': resolution: {integrity: sha512-JT/rz/jaMTggDkd9OIma50si9rPLzSFe7XSrV3mKXwtv9t+rdwx5ZhmKJd+Rz6S1vhn/291k21JLfaxOW6u8KQ==} + '@prisma/get-platform@7.10.0': + resolution: {integrity: sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==} + '@prisma/get-platform@7.2.0': resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} - '@prisma/get-platform@7.9.1': - resolution: {integrity: sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==} - '@prisma/internals@5.0.0': resolution: {integrity: sha512-VGWyFk6QlSBXT8z65Alq5F3o9E8IiTtaBoa3rmKkGpZjUk85kJy3jZz4xkRv53TaeghGE5rWfwkfak26KtY5yQ==} @@ -1469,14 +1551,15 @@ packages: '@types/react': optional: true - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@sinclair/typebox@0.34.49': - resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -1484,35 +1567,35 @@ packages: '@sinonjs/fake-timers@15.4.0': resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} - '@smithy/core@3.31.1': - resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.4.16': - resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==} + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.6.13': - resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==} + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.9.13': - resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==} + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.6.12': - resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} engines: {node: '>=18.0.0'} - '@smithy/types@4.16.1': - resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} engines: {node: '>=18.0.0'} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1613,8 +1696,8 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + '@tsconfig/node10@1.0.13': + resolution: {integrity: sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -1625,33 +1708,33 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@turbo/darwin-64@2.10.7': - resolution: {integrity: sha512-/c9cSBRermWDv85oufLhoH6XRLOVbvzJLRd+WLyfJCP+i0HFLQj4PVNDrHcY17/ve5l8X0Oua4bJBqJUgJPnZA==} + '@turbo/darwin-64@2.10.12': + resolution: {integrity: sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.10.7': - resolution: {integrity: sha512-8lpCCGWZBl9PIF8w8f2iEWrLMbHBWIfJeV6l2UEGqysD6HRIM4ySj/8R7HGEzbECJ4r/gnJcHmxEoG8yjFe64A==} + '@turbo/darwin-arm64@2.10.12': + resolution: {integrity: sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.10.7': - resolution: {integrity: sha512-Midw9Ed00yw9rqkWN82fY3LmLNFQ6yiL1GXB56DJKUEjWaEd27zh7ohCxzzrjfjQVrEeVWd3UPytTAV16XDQlA==} + '@turbo/linux-64@2.10.12': + resolution: {integrity: sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==} cpu: [x64] - os: [linux] + os: [android, linux] - '@turbo/linux-arm64@2.10.7': - resolution: {integrity: sha512-UVEy+MW/xn4BcsiV3v3uv0/oObyaQgVtRT+Jj4WE53rNH05VEYEc1Z13q3zV6276wCZR4Yxc4hiTTcjw7PjSpg==} + '@turbo/linux-arm64@2.10.12': + resolution: {integrity: sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==} cpu: [arm64] - os: [linux] + os: [android, linux] - '@turbo/windows-64@2.10.7': - resolution: {integrity: sha512-l2nH9KGLV46SWjcXvyc2+xo5gdf5J0NVADknQk9OCJhzJBpiNl/byd26yIfwZBjLupdqT6UOdPhPxcpUPxRykQ==} + '@turbo/windows-64@2.10.12': + resolution: {integrity: sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.10.7': - resolution: {integrity: sha512-qjE1apG6RThuX49vUJd5ks2dV2ndXC2qktDChQonFMvUzrMrEnZMQzO+IgxBiYq1j+NbXQcRwj84nGnk1TBw1g==} + '@turbo/windows-arm64@2.10.12': + resolution: {integrity: sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==} cpu: [arm64] os: [win32] @@ -1741,8 +1824,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} @@ -1768,14 +1851,11 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/lodash@4.17.24': - resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -1783,14 +1863,14 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@26.1.2': - resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + '@types/node@26.4.0': + resolution: {integrity: sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -1798,13 +1878,13 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -1821,8 +1901,8 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/superagent@8.1.10': - resolution: {integrity: sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==} + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} '@types/supertest@7.2.1': resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} @@ -1836,67 +1916,67 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.68.0': + resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.68.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==} + '@typescript-eslint/parser@8.68.0': + resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==} 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==} + '@typescript-eslint/project-service@8.68.0': + resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} 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==} + '@typescript-eslint/scope-manager@8.68.0': + resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.68.0': + resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} 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==} + '@typescript-eslint/type-utils@8.68.0': + resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==} 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==} + '@typescript-eslint/types@8.68.0': + resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.68.0': + resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} 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==} + '@typescript-eslint/utils@8.68.0': + resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} 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==} + '@typescript-eslint/visitor-keys@8.68.0': + resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.2': - resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} '@unrs/resolver-binding-android-arm-eabi@1.12.2': resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} @@ -2127,8 +2207,8 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -2173,19 +2253,12 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2194,8 +2267,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -2210,8 +2283,8 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} anymatch@3.1.3: @@ -2249,14 +2322,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - 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-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -2264,22 +2329,6 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - 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'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -2287,26 +2336,18 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} - axios@1.19.0: - resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} @@ -2316,18 +2357,18 @@ packages: react-native-b4a: optional: true - babel-jest@30.4.1: - resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==} + babel-jest@30.5.0: + resolution: {integrity: sha512-PrhPHlKC+MsLnuNzgIH/y1dkz1f6cSfKWaQeaG8WxLMuG44dYWQ8E9uRrsBbAGCU/3+BEFYPN4d6G3Zc5Y+waA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-0 - babel-plugin-istanbul@7.0.1: - resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} - engines: {node: '>=12'} + babel-plugin-istanbul@8.0.0: + resolution: {integrity: sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==} + engines: {node: '>=18'} - babel-plugin-jest-hoist@30.4.0: - resolution: {integrity: sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==} + babel-plugin-jest-hoist@30.5.0: + resolution: {integrity: sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-preset-current-node-syntax@1.2.0: @@ -2335,42 +2376,38 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-jest@30.4.0: - resolution: {integrity: sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==} + babel-preset-jest@30.5.0: + resolution: {integrity: sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 || ^8.0.0 balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.9.1: - resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} peerDependencies: bare-abort-controller: '*' peerDependenciesMeta: bare-abort-controller: optional: true - bare-fs@4.7.3: - resolution: {integrity: sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==} - engines: {bare: '>=1.16.0'} + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' peerDependenciesMeta: bare-buffer: optional: true - bare-os@3.9.3: - resolution: {integrity: sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==} - engines: {bare: '>=1.14.0'} + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} - bare-path@3.0.1: - resolution: {integrity: sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==} - - bare-stream@2.13.3: - resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} peerDependencies: bare-abort-controller: '*' bare-buffer: '*' @@ -2383,14 +2420,14 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.5.2: + resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.42: - resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + baseline-browser-mapping@2.11.20: + resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} engines: {node: '>=6.0.0'} hasBin: true @@ -2422,8 +2459,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.4: - resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2476,10 +2513,6 @@ packages: 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'} @@ -2496,13 +2529,17 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001803: - resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -2537,8 +2574,8 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -2557,13 +2594,13 @@ packages: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} @@ -2584,10 +2621,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} @@ -2610,13 +2643,13 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - comment-json@5.0.0: resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} @@ -2650,8 +2683,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -2778,26 +2811,6 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - 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.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2835,17 +2848,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - - 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'} - defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -2890,26 +2892,14 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - dotenv-expand@12.0.3: - resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + dotenv-expand@13.0.0: + resolution: {integrity: sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==} engines: {node: '>=12'} dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dotenv@17.4.1: - resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} - engines: {node: '>=12'} - dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -2930,8 +2920,8 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} - electron-to-chromium@1.5.387: - resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} + electron-to-chromium@1.5.416: + resolution: {integrity: sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==} elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} @@ -2946,6 +2936,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -2957,12 +2950,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.24.1: - resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} - engines: {node: '>=10.13.0'} - - enhanced-resolve@5.24.3: - resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} env-paths@2.2.1: @@ -2976,14 +2965,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - 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'} @@ -2992,8 +2973,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.0: - resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -3003,13 +2984,8 @@ packages: 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'} + es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -3026,40 +3002,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-import-resolver-node@0.3.10: - resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - - 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-simple-import-sort@14.0.0: resolution: {integrity: sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==} peerDependencies: @@ -3090,8 +3032,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.8.0: - resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} + eslint@10.9.1: + resolution: {integrity: sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3152,16 +3094,16 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} - expect@30.4.1: - resolution: {integrity: sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==} + expect@30.5.0: + resolution: {integrity: sha512-8fiMWcEjPU7B9nErC4FtFcCzf2tC6I75Qf7m8wzBAWC2taZmcno3yAFEjIQL34SwoGZNgPf63UDiJLyh4SMPaw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - exsolve@1.1.0: - resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3195,14 +3137,23 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.4: resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fast-write-atomic@0.2.1: resolution: {integrity: sha512-WvJe06IfNYlr+6cO3uQkdKdy3Cb1LlCJSF8zRs2eT8yuhdbSlR9nIt+TgQ92RUxiRrQm+/S7RARnMfCs5iuAjw==} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -3224,9 +3175,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-type@21.3.4: - resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} - engines: {node: '>=20'} + file-type@22.0.2: + resolution: {integrity: sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==} + engines: {node: '>=22'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -3240,8 +3191,8 @@ packages: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} - find-my-way@9.7.0: - resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} + find-my-way@9.9.0: + resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} engines: {node: '>=20'} find-up@4.1.0: @@ -3256,8 +3207,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} @@ -3268,10 +3219,6 @@ packages: debug: optional: true - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -3302,15 +3249,12 @@ packages: fp-ts@2.16.0: resolution: {integrity: sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ==} - framer-motion@12.43.0: - resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} + framer-motion@13.1.1: + resolution: {integrity: sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA==} peerDependencies: - '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true react: optional: true react-dom: @@ -3340,27 +3284,15 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - 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==} - gaxios@7.1.3: resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} engines: {node: '>=18'} - gaxios@7.2.0: - resolution: {integrity: sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==} + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} engines: {node: '>=18'} gcp-metadata@8.1.2: @@ -3370,10 +3302,6 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - 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'} @@ -3382,6 +3310,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3401,10 +3333,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - giget@3.3.1: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} hasBin: true @@ -3442,10 +3370,6 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -3458,12 +3382,12 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} - googleapis-common@8.0.2: - resolution: {integrity: sha512-5MXeQzIZaqCH7B+HJWqhQm946VARpZep6acbWSr/fcgF2cQANq7allgX+i/G0EqF0WyUxB277gtWMzRYHMl9tg==} - engines: {node: '>=18.0.0'} + googleapis-common@8.0.3: + resolution: {integrity: sha512-7g1yzQKx0mmNTjiK0H9dJ8eqKqDBveES9vLHeg5neb3BMQy/d1oQefIMhIpOVT8a+f+LOcixMEdRbFIW/cQUJw==} + engines: {node: '>=18'} - googleapis@173.0.0: - resolution: {integrity: sha512-xEJJYLZ4qeenVyfzispNfRjCe9bsv7CzBv5zYFLvScOze9snJ8S9W6hjQ729CWPQt5mvn/JrcRaCHzQiukt0ng==} + googleapis@176.0.0: + resolution: {integrity: sha512-v24+WfeIw3R9iF8LT7eixlawezy5SGGNB/WjFV8qeiBS9G6KCsqaFngsnTXvBrVH4womehN2Co8UYJxZ0KpbvQ==} engines: {node: '>=18'} gopd@1.2.0: @@ -3484,21 +3408,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - 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'} @@ -3569,8 +3482,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.7: + resolution: {integrity: sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -3601,74 +3514,34 @@ packages: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ioredis@5.11.1: - resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} - engines: {node: '>=12.22.0'} + ioredis@6.0.0: + resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==} + engines: {node: '>=20.0.0'} ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - 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-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-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true - 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-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -3677,29 +3550,13 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} - 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-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - - 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-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -3719,18 +3576,6 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - 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-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -3739,33 +3584,9 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} - 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-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - - 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'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} @@ -3778,9 +3599,6 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3811,16 +3629,16 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-changed-files@30.4.1: - resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==} + jest-changed-files@30.5.0: + resolution: {integrity: sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-circus@30.4.2: - resolution: {integrity: sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==} + jest-circus@30.5.0: + resolution: {integrity: sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-cli@30.4.2: - resolution: {integrity: sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==} + jest-cli@30.5.0: + resolution: {integrity: sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -3829,8 +3647,8 @@ packages: node-notifier: optional: true - jest-config@30.4.2: - resolution: {integrity: sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==} + jest-config@30.5.0: + resolution: {integrity: sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@types/node': '*' @@ -3844,97 +3662,88 @@ packages: ts-node: optional: true - jest-diff@30.4.1: - resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} + jest-diff@30.5.0: + resolution: {integrity: sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-docblock@30.4.0: - resolution: {integrity: sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==} + jest-docblock@30.5.0: + resolution: {integrity: sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-each@30.4.1: - resolution: {integrity: sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==} + jest-each@30.5.0: + resolution: {integrity: sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-environment-node@30.4.1: - resolution: {integrity: sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==} + jest-environment-node@30.5.0: + resolution: {integrity: sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-haste-map@30.4.1: - resolution: {integrity: sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==} + jest-haste-map@30.5.0: + resolution: {integrity: sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-leak-detector@30.4.1: - resolution: {integrity: sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==} + jest-leak-detector@30.5.0: + resolution: {integrity: sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-matcher-utils@30.4.1: - resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==} + jest-matcher-utils@30.5.0: + resolution: {integrity: sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@30.4.1: - resolution: {integrity: sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==} + jest-message-util@30.5.0: + resolution: {integrity: sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-mock@30.4.1: - resolution: {integrity: sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==} + jest-mock@30.5.0: + resolution: {integrity: sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@30.4.0: - resolution: {integrity: sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==} + jest-regex-util@30.5.0: + resolution: {integrity: sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@30.4.2: - resolution: {integrity: sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==} + jest-resolve-dependencies@30.5.0: + resolution: {integrity: sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve@30.4.1: - resolution: {integrity: sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==} + jest-resolve@30.5.0: + resolution: {integrity: sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runner@30.4.2: - resolution: {integrity: sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==} + jest-runner@30.5.0: + resolution: {integrity: sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runtime@30.4.2: - resolution: {integrity: sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==} + jest-runtime@30.5.0: + resolution: {integrity: sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-snapshot@30.4.1: - resolution: {integrity: sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==} + jest-snapshot@30.5.0: + resolution: {integrity: sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-util@30.4.1: - resolution: {integrity: sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==} + jest-util@30.5.0: + resolution: {integrity: sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@30.4.1: - resolution: {integrity: sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==} + jest-validate@30.5.0: + resolution: {integrity: sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-watcher@30.4.1: - resolution: {integrity: sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==} + jest-watcher@30.5.0: + resolution: {integrity: sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jest-worker@30.4.1: - resolution: {integrity: sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==} + jest-worker@30.5.0: + resolution: {integrity: sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@30.4.2: - resolution: {integrity: sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==} + jest@30.5.0: + resolution: {integrity: sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -3950,12 +3759,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.15.0: - resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true js-yaml@5.2.2: @@ -3985,10 +3794,6 @@ packages: 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'} @@ -4033,8 +3838,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.13.8: - resolution: {integrity: sha512-80xal1m93rADejw2pMp2MSzFhHCPLEspjHxnH2UtqI+DgAmElsbmLMiqk9niwH9NWAfjsRtaJI+qBrOEmRx9nQ==} + libphonenumber-js@1.13.12: + resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==} lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} @@ -4168,9 +3973,9 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -4178,8 +3983,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -4189,17 +3994,17 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@1.28.0: - resolution: {integrity: sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==} + lucide-react@1.38.0: + resolution: {integrity: sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.0.0: + resolution: {integrity: sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -4211,9 +4016,6 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4222,8 +4024,8 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} memfs@3.5.3: @@ -4274,12 +4076,16 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -4300,11 +4106,22 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - motion-dom@12.43.0: - resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} + motion-dom@13.1.1: + resolution: {integrity: sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA==} + + motion-utils@13.0.0: + resolution: {integrity: sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==} - motion-utils@12.39.0: - resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + motion@13.1.1: + resolution: {integrity: sha512-WNZoK6xiF+kkTqkZ5K7FDDh6A8BG4i5Hc7KXtW8gtTxkpJFds+hIOrDaQGKjQj/AE/i4hJqAaUHEqp/Qo02y6Q==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -4316,9 +4133,9 @@ packages: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} engines: {node: '>= 10.16.0'} - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} mysql2@3.15.3: resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} @@ -4328,8 +4145,8 @@ packages: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -4341,9 +4158,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -4358,8 +4175,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.12: - resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + next@16.3.3: + resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -4382,17 +4199,17 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-emoji@1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - - node-exports-info@1.6.2: - resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} - engines: {node: '>= 0.4'} + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} node-fetch@2.6.11: resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} @@ -4419,8 +4236,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.50: - resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} normalize-package-data@2.5.0: @@ -4455,32 +4272,8 @@ packages: 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'} - - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -4493,6 +4286,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -4501,13 +4298,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} @@ -4611,15 +4404,15 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.15.0: - resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.22.0: - resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -4637,14 +4430,14 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -4660,12 +4453,8 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss@8.5.23: - resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -4756,16 +4545,16 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@30.4.1: - resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + pretty-format@30.5.0: + resolution: {integrity: sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} prisma-dbml-generator@0.12.0: resolution: {integrity: sha512-b5CqA9cCY5jmNJGjx8oUbPw3KSgVd+mu8711MSZIRY9dIF0Vlcs/Au6LR3S5guncydM7Zkh8iz8vC+c2CDj3Xw==} hasBin: true - prisma@7.9.1: - resolution: {integrity: sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==} + prisma@7.10.0: + resolution: {integrity: sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==} engines: {node: ^20.19 || ^22.12 || >=24.0} hasBin: true peerDependencies: @@ -4813,8 +4602,8 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -4879,29 +4668,17 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} - redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - 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'} - remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} @@ -4933,14 +4710,9 @@ packages: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true - resolve@2.0.0-next.7: - resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} - engines: {node: '>= 0.4'} - hasBin: true - - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} @@ -4977,30 +4749,15 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.4: - resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} - engines: {node: '>=0.4'} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 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'} - safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -5046,23 +4803,11 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - 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'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -5104,6 +4849,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -5116,9 +4865,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -5126,9 +4872,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -5167,9 +4913,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -5178,8 +4924,8 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - streamx@2.28.0: - resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -5193,17 +4939,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - 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'} + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -5280,12 +5018,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swagger-ui-dist@5.32.8: - resolution: {integrity: sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==} - - symbol-observable@4.0.0: - resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} - engines: {node: '>=0.10'} + swagger-ui-dist@5.32.14: + resolution: {integrity: sha512-nOA2pSQhcmODMUQZpJHYKNuwniDUqcOWGNaSCOoZv12FdOSJ9JxV95HtyRGNMqEBj6h6lCNTy20TgZDYTSuUIg==} synckit@0.11.13: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} @@ -5302,8 +5036,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar-stream@3.2.0: - resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -5371,14 +5105,14 @@ packages: uglify-js: optional: true - terser@5.48.0: - resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} engines: {node: '>=10'} hasBin: true - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -5391,9 +5125,6 @@ packages: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} engines: {node: '>=8.17.0'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5463,9 +5194,6 @@ packages: resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} engines: {node: '>=10.13.0'} - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -5473,8 +5201,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - turbo@2.10.7: - resolution: {integrity: sha512-GHx6WExIFSKNJ5qMlzDpXBXlu9ApxaMjqxAVrCNcW94xf/+uqgIz41SAuRUMbXva2ExNAaY/h8V0q90SWSzmRw==} + turbo@2.10.12: + resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==} hasBin: true type-check@0.4.0: @@ -5517,30 +5245,9 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - 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'} - typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -5559,13 +5266,13 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -5581,8 +5288,8 @@ packages: 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==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -5632,16 +5339,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.5.2: resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -5653,8 +5354,8 @@ packages: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} - webpack-sources@3.5.0: - resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} engines: {node: '>=10.13.0'} webpack@5.106.2: @@ -5670,22 +5371,6 @@ packages: 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'} @@ -5702,10 +5387,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5748,8 +5429,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} zeptomatch@2.1.0: @@ -5767,233 +5448,209 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@angular-devkit/core@19.2.24(chokidar@4.0.3)': - dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - jsonc-parser: 3.3.1 - picomatch: 4.0.4 - rxjs: 7.8.1 - source-map: 0.7.4 - optionalDependencies: - chokidar: 4.0.3 - - '@angular-devkit/core@19.2.27(chokidar@4.0.3)': + '@angular-devkit/core@22.1.5(chokidar@5.0.0)': dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) jsonc-parser: 3.3.1 - picomatch: 4.0.4 - rxjs: 7.8.1 - source-map: 0.7.4 + picomatch: 4.0.5 + rxjs: 7.8.2 + source-map: 0.7.6 optionalDependencies: - chokidar: 4.0.3 + chokidar: 5.0.0 - '@angular-devkit/schematics-cli@19.2.27(@types/node@26.1.2)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@22.1.5(@types/node@26.4.0)(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 19.2.27(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@26.1.2) - ansi-colors: 4.1.3 - symbol-observable: 4.0.0 - yargs-parser: 21.1.1 + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + '@inquirer/prompts': 8.5.2(@types/node@26.4.0) transitivePeerDependencies: - '@types/node' - chokidar - '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': - dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - jsonc-parser: 3.3.1 - magic-string: 0.30.17 - ora: 5.4.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - - '@angular-devkit/schematics@19.2.27(chokidar@4.0.3)': + '@angular-devkit/schematics@22.1.5(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) jsonc-parser: 3.3.1 - magic-string: 0.30.17 - ora: 5.4.1 - rxjs: 7.8.1 + magic-string: 1.0.0 + ora: 9.4.1 + rxjs: 7.8.2 transitivePeerDependencies: - chokidar '@antfu/ni@0.21.4': {} - '@aws-sdk/checksums@3.1000.23': + '@aws-sdk/checksums@3.1000.29': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1098.0': - dependencies: - '@aws-sdk/checksums': 3.1000.23 - '@aws-sdk/core': 3.977.3 - '@aws-sdk/credential-provider-node': 3.972.75 - '@aws-sdk/middleware-sdk-s3': 3.972.69 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 + '@aws-sdk/client-s3@3.1121.0': + dependencies: + '@aws-sdk/checksums': 3.1000.29 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/middleware-sdk-s3': 3.972.75 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/core@3.977.3': + '@aws-sdk/core@3.977.9': dependencies: - '@aws-sdk/types': 3.974.2 - '@aws-sdk/xml-builder': 3.972.37 + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.31.1 - '@smithy/signature-v4': 5.6.12 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.64': + '@aws-sdk/credential-provider-env@3.972.70': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.66': + '@aws-sdk/credential-provider-http@3.972.72': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.973.9': - dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/credential-provider-env': 3.972.64 - '@aws-sdk/credential-provider-http': 3.972.66 - '@aws-sdk/credential-provider-login': 3.972.71 - '@aws-sdk/credential-provider-process': 3.972.64 - '@aws-sdk/credential-provider-sso': 3.973.8 - '@aws-sdk/credential-provider-web-identity': 3.972.70 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/credential-provider-imds': 4.4.16 - '@smithy/types': 4.16.1 + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-login@3.972.71': + '@aws-sdk/credential-provider-login@3.972.77': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-node@3.972.75': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.64 - '@aws-sdk/credential-provider-http': 3.972.66 - '@aws-sdk/credential-provider-ini': 3.973.9 - '@aws-sdk/credential-provider-process': 3.972.64 - '@aws-sdk/credential-provider-sso': 3.973.8 - '@aws-sdk/credential-provider-web-identity': 3.972.70 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/credential-provider-imds': 4.4.16 - '@smithy/types': 4.16.1 + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.972.64': + '@aws-sdk/credential-provider-process@3.972.70': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.973.8': + '@aws-sdk/credential-provider-sso@3.973.14': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/token-providers': 3.1098.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.70': + '@aws-sdk/credential-provider-web-identity@3.972.76': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/lib-storage@3.1098.0(@aws-sdk/client-s3@3.1098.0)': + '@aws-sdk/lib-storage@3.1121.0(@aws-sdk/client-s3@3.1121.0)': dependencies: - '@aws-sdk/client-s3': 3.1098.0 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/client-s3': 3.1121.0 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.69': + '@aws-sdk/middleware-sdk-s3@3.972.75': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.997.38': + '@aws-sdk/nested-clients@3.997.44': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.43': + '@aws-sdk/signature-v4-multi-region@3.996.46': dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/signature-v4': 5.6.12 - '@smithy/types': 4.16.1 + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1098.0': + '@aws-sdk/token-providers@3.1116.0': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/types@3.974.2': + '@aws-sdk/types@3.974.5': dependencies: - '@smithy/types': 4.16.1 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.37': + '@aws-sdk/xml-builder@3.972.40': dependencies: - '@smithy/types': 4.16.1 + '@smithy/types': 4.17.2 tslib: 2.8.1 '@aws/lambda-invoke-store@0.3.0': {} @@ -6009,14 +5666,14 @@ snapshots: '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -6026,10 +5683,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -6038,7 +5695,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.4 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -6046,8 +5703,8 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -6056,7 +5713,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8 transitivePeerDependencies: - supports-color @@ -6071,11 +5728,11 @@ snapshots: '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@babel/parser@7.29.7': + '@babel/parser@7.29.8': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': dependencies: @@ -6165,22 +5822,22 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.8': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 @@ -6217,7 +5874,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 @@ -6227,14 +5884,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0))': - dependencies: - eslint: 10.8.0(jiti@2.7.0) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.9.1(eslint@10.8.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.9.1(jiti@2.7.0))': dependencies: - eslint: 10.8.0(jiti@2.7.0) + eslint: 10.9.1(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -6243,7 +5895,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 transitivePeerDependencies: - supports-color @@ -6281,251 +5933,245 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.3': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.3': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.3': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.2': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.2': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.3': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.3': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.3': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.3': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.3': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.3': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.3': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.3': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.3': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.3': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.3': + '@img/sharp-win32-x64@0.35.4': optional: true - '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.7': {} - '@inquirer/checkbox@4.3.2(@types/node@26.1.2)': + '@inquirer/checkbox@5.2.3(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/confirm@5.1.21(@types/node@26.1.2)': + '@inquirer/confirm@6.3.0(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/core@10.3.2(@types/node@26.1.2)': + '@inquirer/core@12.0.1(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) cli-width: 4.1.0 - mute-stream: 2.0.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/editor@4.2.23(@types/node@26.1.2)': + '@inquirer/editor@5.3.1(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/external-editor': 1.0.3(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/external-editor': 3.0.4(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/expand@4.0.23(@types/node@26.1.2)': + '@inquirer/expand@5.1.3(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/external-editor@1.0.3(@types/node@26.1.2)': + '@inquirer/external-editor@3.0.4(@types/node@26.4.0)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.8': {} - '@inquirer/input@4.3.1(@types/node@26.1.2)': + '@inquirer/input@5.1.4(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/number@3.0.23(@types/node@26.1.2)': + '@inquirer/number@4.2.1(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/password@4.0.23(@types/node@26.1.2)': + '@inquirer/password@5.2.0(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 - - '@inquirer/prompts@7.10.1(@types/node@26.1.2)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@26.1.2) - '@inquirer/confirm': 5.1.21(@types/node@26.1.2) - '@inquirer/editor': 4.2.23(@types/node@26.1.2) - '@inquirer/expand': 4.0.23(@types/node@26.1.2) - '@inquirer/input': 4.3.1(@types/node@26.1.2) - '@inquirer/number': 3.0.23(@types/node@26.1.2) - '@inquirer/password': 4.0.23(@types/node@26.1.2) - '@inquirer/rawlist': 4.1.11(@types/node@26.1.2) - '@inquirer/search': 3.2.2(@types/node@26.1.2) - '@inquirer/select': 4.4.2(@types/node@26.1.2) + '@types/node': 26.4.0 + + '@inquirer/prompts@8.5.2(@types/node@26.4.0)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@26.4.0) + '@inquirer/confirm': 6.3.0(@types/node@26.4.0) + '@inquirer/editor': 5.3.1(@types/node@26.4.0) + '@inquirer/expand': 5.1.3(@types/node@26.4.0) + '@inquirer/input': 5.1.4(@types/node@26.4.0) + '@inquirer/number': 4.2.1(@types/node@26.4.0) + '@inquirer/password': 5.2.0(@types/node@26.4.0) + '@inquirer/rawlist': 5.3.3(@types/node@26.4.0) + '@inquirer/search': 4.3.1(@types/node@26.4.0) + '@inquirer/select': 5.2.3(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 - - '@inquirer/prompts@7.3.2(@types/node@26.1.2)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@26.1.2) - '@inquirer/confirm': 5.1.21(@types/node@26.1.2) - '@inquirer/editor': 4.2.23(@types/node@26.1.2) - '@inquirer/expand': 4.0.23(@types/node@26.1.2) - '@inquirer/input': 4.3.1(@types/node@26.1.2) - '@inquirer/number': 3.0.23(@types/node@26.1.2) - '@inquirer/password': 4.0.23(@types/node@26.1.2) - '@inquirer/rawlist': 4.1.11(@types/node@26.1.2) - '@inquirer/search': 3.2.2(@types/node@26.1.2) - '@inquirer/select': 4.4.2(@types/node@26.1.2) + '@types/node': 26.4.0 + + '@inquirer/prompts@8.7.0(@types/node@26.4.0)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@26.4.0) + '@inquirer/confirm': 6.3.0(@types/node@26.4.0) + '@inquirer/editor': 5.3.1(@types/node@26.4.0) + '@inquirer/expand': 5.1.3(@types/node@26.4.0) + '@inquirer/input': 5.1.4(@types/node@26.4.0) + '@inquirer/number': 4.2.1(@types/node@26.4.0) + '@inquirer/password': 5.2.0(@types/node@26.4.0) + '@inquirer/rawlist': 5.3.3(@types/node@26.4.0) + '@inquirer/search': 4.3.1(@types/node@26.4.0) + '@inquirer/select': 5.2.3(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/rawlist@4.1.11(@types/node@26.1.2)': + '@inquirer/rawlist@5.3.3(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/search@3.2.2(@types/node@26.1.2)': + '@inquirer/search@4.3.1(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/select@4.4.2(@types/node@26.1.2)': + '@inquirer/select@5.2.3(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/type@3.0.10(@types/node@26.1.2)': + '@inquirer/type@4.1.0(@types/node@26.4.0)': optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@ioredis/commands@1.10.0': {} + '@ioredis/commands@2.0.0': {} '@isaacs/cliui@8.0.2': dependencies: @@ -6541,49 +6187,49 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.15.0 + js-yaml: 3.15.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} - '@jest/console@30.4.1': + '@jest/console@30.5.0': dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 - jest-message-util: 30.4.1 - jest-util: 30.4.1 + jest-message-util: 30.5.0 + jest-util: 30.5.0 slash: 3.0.0 - '@jest/core@30.4.2(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3))': + '@jest/core@30.5.0(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3))': dependencies: - '@jest/console': 30.4.1 - '@jest/pattern': 30.4.0 - '@jest/reporters': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/console': 30.5.0 + '@jest/pattern': 30.5.0 + '@jest/reporters': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.4.0 exit-x: 0.2.2 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-changed-files: 30.4.1 - jest-config: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - jest-haste-map: 30.4.1 - jest-message-util: 30.4.1 - jest-regex-util: 30.4.0 - jest-resolve: 30.4.1 - jest-resolve-dependencies: 30.4.2 - jest-runner: 30.4.2 - jest-runtime: 30.4.2 - jest-snapshot: 30.4.1 - jest-util: 30.4.1 - jest-validate: 30.4.1 - jest-watcher: 30.4.1 - pretty-format: 30.4.1 + jest-changed-files: 30.5.0 + jest-config: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + jest-haste-map: 30.5.0 + jest-message-util: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-resolve-dependencies: 30.5.0 + jest-runner: 30.5.0 + jest-runtime: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 + jest-watcher: 30.5.0 + pretty-format: 30.5.0 slash: 3.0.0 transitivePeerDependencies: - babel-plugin-macros @@ -6591,142 +6237,143 @@ snapshots: - supports-color - ts-node - '@jest/diff-sequences@30.4.0': {} + '@jest/diff-sequences@30.5.0': {} - '@jest/environment@30.4.1': + '@jest/environment@30.5.0': dependencies: - '@jest/fake-timers': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 - jest-mock: 30.4.1 + '@jest/fake-timers': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 + jest-mock: 30.5.0 - '@jest/expect-utils@30.4.1': + '@jest/expect-utils@30.5.0': dependencies: - '@jest/get-type': 30.1.0 + '@jest/get-type': 30.5.0 - '@jest/expect@30.4.1': + '@jest/expect@30.5.0': dependencies: - expect: 30.4.1 - jest-snapshot: 30.4.1 + expect: 30.5.0 + jest-snapshot: 30.5.0 transitivePeerDependencies: - supports-color - '@jest/fake-timers@30.4.1': + '@jest/fake-timers@30.5.0': dependencies: - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 '@sinonjs/fake-timers': 15.4.0 - '@types/node': 26.1.2 - jest-message-util: 30.4.1 - jest-mock: 30.4.1 - jest-util: 30.4.1 + '@types/node': 26.4.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 - '@jest/get-type@30.1.0': {} + '@jest/get-type@30.5.0': {} - '@jest/globals@30.4.1': + '@jest/globals@30.5.0': dependencies: - '@jest/environment': 30.4.1 - '@jest/expect': 30.4.1 - '@jest/types': 30.4.1 - jest-mock: 30.4.1 + '@jest/environment': 30.5.0 + '@jest/expect': 30.5.0 + '@jest/types': 30.5.0 + jest-mock: 30.5.0 transitivePeerDependencies: - supports-color - '@jest/pattern@30.4.0': + '@jest/pattern@30.5.0': dependencies: - '@types/node': 26.1.2 - jest-regex-util: 30.4.0 + '@types/node': 26.4.0 + jest-regex-util: 30.5.0 - '@jest/reporters@30.4.1': + '@jest/reporters@30.5.0': dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 + '@jest/console': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 26.1.2 + '@types/node': 26.4.0 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 - glob: 10.5.0 + glob: 13.0.6 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - jest-message-util: 30.4.1 - jest-util: 30.4.1 - jest-worker: 30.4.1 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + jest-worker: 30.5.0 slash: 3.0.0 string-length: 4.0.2 v8-to-istanbul: 9.3.0 transitivePeerDependencies: - supports-color - '@jest/schemas@30.4.1': + '@jest/schemas@30.5.0': dependencies: - '@sinclair/typebox': 0.34.49 + '@sinclair/typebox': 0.34.52 - '@jest/snapshot-utils@30.4.1': + '@jest/snapshot-utils@30.5.0': dependencies: - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 chalk: 4.1.2 graceful-fs: 4.2.11 natural-compare: 1.4.0 - '@jest/source-map@30.0.1': + '@jest/source-map@30.5.0': dependencies: '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 + convert-source-map: 2.0.0 graceful-fs: 4.2.11 - '@jest/test-result@30.4.1': + '@jest/test-result@30.5.0': dependencies: - '@jest/console': 30.4.1 - '@jest/types': 30.4.1 + '@jest/console': 30.5.0 + '@jest/types': 30.5.0 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.3 - '@jest/test-sequencer@30.4.1': + '@jest/test-sequencer@30.5.0': dependencies: - '@jest/test-result': 30.4.1 + '@jest/test-result': 30.5.0 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 + jest-haste-map: 30.5.0 slash: 3.0.0 - '@jest/transform@30.4.1': + '@jest/transform@30.5.0': dependencies: '@babel/core': 7.29.7 - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 7.0.1 + babel-plugin-istanbul: 8.0.0 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 - jest-regex-util: 30.4.0 - jest-util: 30.4.1 + jest-haste-map: 30.5.0 + jest-regex-util: 30.5.0 + jest-util: 30.5.0 pirates: 4.0.7 slash: 3.0.0 write-file-atomic: 5.0.1 transitivePeerDependencies: - supports-color - '@jest/types@30.4.1': + '@jest/types@30.5.0': dependencies: - '@jest/pattern': 30.4.0 - '@jest/schemas': 30.4.1 + '@jest/pattern': 30.5.0 + '@jest/schemas': 30.5.0 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/yargs': 17.0.35 chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -6740,69 +6387,60 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@lukeed/csprng@1.1.0': {} '@microsoft/tsdoc@0.16.0': {} - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.2.3(@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 - '@nestjs/cli@11.0.24(@types/node@26.1.2)(prettier@3.9.6)': + '@nestjs/cli@12.0.0(@types/node@26.4.0)(fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.3)(webpack@5.106.2))(prettier@3.9.6)(tsconfig-paths-webpack-plugin@4.2.0)(webpack-node-externals@3.0.0)(webpack@5.106.2)': dependencies: - '@angular-devkit/core': 19.2.27(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.27(@types/node@26.1.2)(chokidar@4.0.3) - '@inquirer/prompts': 7.10.1(@types/node@26.1.2) - '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3) - ansis: 4.2.0 - chokidar: 4.0.3 + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics-cli': 22.1.5(@types/node@26.4.0)(chokidar@5.0.0) + '@inquirer/prompts': 8.7.0(@types/node@26.4.0) + '@nestjs/schematics': 12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3) + ansis: 4.3.1 + chokidar: 5.0.0 cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2) - glob: 13.0.6 - node-emoji: 1.11.0 - ora: 5.4.1 + commander: 15.0.0 + minimatch: 10.2.6 + node-emoji: 2.2.0 + ora: 9.4.1 tsconfig-paths: 4.2.0 + typescript: 6.0.3 + optionalDependencies: + fork-ts-checker-webpack-plugin: 9.1.0(typescript@6.0.3)(webpack@5.106.2) tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.9.3 webpack: 5.106.2 webpack-node-externals: 3.0.0 transitivePeerDependencies: - - '@minify-html/node' - - '@swc/css' - - '@swc/html' - '@types/node' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - prettier - - uglify-js - - webpack-cli - '@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.3.4 + '@standard-schema/spec': 1.1.0 + file-type: 22.0.2 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -6815,17 +6453,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - dotenv: 17.4.1 - dotenv-expand: 12.0.3 - lodash: 4.18.1 + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@standard-schema/spec': 1.1.0 + dotenv: 17.4.2 + dotenv-expand: 13.0.0 + es-toolkit: 1.51.0 rxjs: 7.8.2 - '@nestjs/core@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -6834,26 +6473,26 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/platform-express': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1) - '@nestjs/jwt@11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/jwt@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/platform-express@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/platform-express@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 multer: 2.2.0 @@ -6862,23 +6501,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3)': - dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) - comment-json: 5.0.0 - jsonc-parser: 3.3.1 - pluralize: 8.0.0 - typescript: 5.9.3 - optionalDependencies: - prettier: 3.9.6 - transitivePeerDependencies: - - chokidar - - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@6.0.3)': + '@nestjs/schematics@12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3)': dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) comment-json: 5.0.0 jsonc-parser: 3.3.1 pluralize: 8.0.0 @@ -6888,64 +6514,65 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@standard-schema/spec': 1.1.0 + es-toolkit: 1.51.0 js-yaml: 5.2.2 - lodash: 4.18.1 path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 - swagger-ui-dist: 5.32.8 + swagger-ui-dist: 5.32.14 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 optionalDependencies: - '@prisma/client': 7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) + '@prisma/client': 7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) - '@nestjs/testing@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28)': + '@nestjs/testing@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-express@12.0.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/platform-express': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1) - '@next/env@16.2.12': {} + '@next/env@16.3.3': {} - '@next/swc-darwin-arm64@16.2.12': + '@next/swc-darwin-arm64@16.3.3': optional: true - '@next/swc-darwin-x64@16.2.12': + '@next/swc-darwin-x64@16.3.3': optional: true - '@next/swc-linux-arm64-gnu@16.2.12': + '@next/swc-linux-arm64-gnu@16.3.3': optional: true - '@next/swc-linux-arm64-musl@16.2.12': + '@next/swc-linux-arm64-musl@16.3.3': optional: true - '@next/swc-linux-x64-gnu@16.2.12': + '@next/swc-linux-x64-gnu@16.3.3': optional: true - '@next/swc-linux-x64-musl@16.2.12': + '@next/swc-linux-x64-musl@16.3.3': optional: true - '@next/swc-win32-arm64-msvc@16.2.12': + '@next/swc-win32-arm64-msvc@16.3.3': optional: true - '@next/swc-win32-x64-msvc@16.2.12': + '@next/swc-win32-x64-msvc@16.3.3': optional: true '@noble/hashes@1.8.0': {} @@ -6960,7 +6587,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.20.3 '@opentelemetry/api@1.4.1': {} @@ -6968,30 +6595,86 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.7 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.3.6': {} - '@prisma/adapter-pg@7.9.1': + '@prisma/adapter-pg@7.10.0': dependencies: - '@prisma/driver-adapter-utils': 7.9.1 - '@types/pg': 8.20.0 - pg: 8.22.0 + '@prisma/driver-adapter-utils': 7.10.0 + '@types/pg': 8.23.1 + pg: 8.23.0 postgres-array: 3.0.4 transitivePeerDependencies: - pg-native - '@prisma/client-runtime-utils@7.9.1': {} + '@prisma/client-runtime-utils@7.10.0': {} - '@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)': + '@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@prisma/client-runtime-utils': 7.9.1 + '@prisma/client-runtime-utils': 7.10.0 optionalDependencies: - prisma: 7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + prisma: 7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) typescript: 6.0.3 - '@prisma/config@7.9.1': + '@prisma/config@7.10.0': dependencies: c12: 3.3.4 deepmerge-ts: 7.1.5 @@ -7008,9 +6691,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@prisma/debug@7.2.0': {} + '@prisma/debug@7.10.0': {} - '@prisma/debug@7.9.1': {} + '@prisma/debug@7.2.0': {} '@prisma/dev@0.24.17(typescript@6.0.3)': dependencies: @@ -7020,7 +6703,7 @@ snapshots: '@prisma/get-platform': 7.2.0 '@prisma/query-plan-executor': 7.2.0 '@prisma/streams-local': 0.1.11 - find-my-way: 9.7.0 + find-my-way: 9.9.0 foreground-child: 3.3.1 get-port-please: 3.2.0 pathe: 2.0.3 @@ -7032,20 +6715,20 @@ snapshots: transitivePeerDependencies: - typescript - '@prisma/driver-adapter-utils@7.9.1': + '@prisma/driver-adapter-utils@7.10.0': dependencies: - '@prisma/debug': 7.9.1 + '@prisma/debug': 7.10.0 - '@prisma/engines-version@7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad': {} + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': {} '@prisma/engines@5.0.0': {} - '@prisma/engines@7.9.1': + '@prisma/engines@7.10.0': dependencies: - '@prisma/debug': 7.9.1 - '@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad - '@prisma/fetch-engine': 7.9.1 - '@prisma/get-platform': 7.9.1 + '@prisma/debug': 7.10.0 + '@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 + '@prisma/fetch-engine': 7.10.0 + '@prisma/get-platform': 7.10.0 '@prisma/fetch-engine@5.0.0': dependencies: @@ -7070,11 +6753,11 @@ snapshots: - encoding - supports-color - '@prisma/fetch-engine@7.9.1': + '@prisma/fetch-engine@7.10.0': dependencies: - '@prisma/debug': 7.9.1 - '@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad - '@prisma/get-platform': 7.9.1 + '@prisma/debug': 7.10.0 + '@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 + '@prisma/get-platform': 7.10.0 '@prisma/generator-helper@5.0.0': dependencies: @@ -7100,13 +6783,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@prisma/get-platform@7.2.0': + '@prisma/get-platform@7.10.0': dependencies: - '@prisma/debug': 7.2.0 + '@prisma/debug': 7.10.0 - '@prisma/get-platform@7.9.1': + '@prisma/get-platform@7.2.0': dependencies: - '@prisma/debug': 7.9.1 + '@prisma/debug': 7.2.0 '@prisma/internals@5.0.0': dependencies: @@ -7168,10 +6851,10 @@ snapshots: env-paths: 3.0.0 proper-lockfile: 4.1.2 - '@prisma/studio-core@0.33.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@prisma/studio-core@0.33.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@types/react': 19.2.17 + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/event': 4.0.1-alpha.0 '@visx/grid': 4.0.1-alpha.0(react@19.2.8) @@ -7189,65 +6872,65 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 - - '@rtsao/scc@1.1.0': {} + '@types/react': 19.2.18 '@scarf/scarf@1.4.0': {} - '@sinclair/typebox@0.34.49': {} + '@sinclair/typebox@0.34.52': {} + + '@sindresorhus/is@4.6.0': {} '@sinonjs/commons@3.0.1': dependencies: @@ -7257,49 +6940,49 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@smithy/core@3.31.1': + '@smithy/core@3.33.3': dependencies: - '@smithy/types': 4.16.1 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.4.16': + '@smithy/credential-provider-imds@4.5.2': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.6.13': + '@smithy/fetch-http-handler@5.7.2': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/node-http-handler@4.9.13': + '@smithy/node-http-handler@4.11.3': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/signature-v4@5.6.12': + '@smithy/signature-v4@5.7.3': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/types@4.16.1': + '@smithy/types@4.17.2': dependencies: tslib: 2.8.1 '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.3 + enhanced-resolve: 5.24.5 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -7362,7 +7045,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 - postcss: 8.5.23 + postcss: 8.5.26 tailwindcss: 4.3.3 '@tokenizer/inflate@0.4.1': @@ -7374,7 +7057,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tsconfig/node10@1.0.12': {} + '@tsconfig/node10@1.0.13': {} '@tsconfig/node12@1.0.11': {} @@ -7382,22 +7065,22 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@turbo/darwin-64@2.10.7': + '@turbo/darwin-64@2.10.12': optional: true - '@turbo/darwin-arm64@2.10.7': + '@turbo/darwin-arm64@2.10.12': optional: true - '@turbo/linux-64@2.10.7': + '@turbo/linux-64@2.10.12': optional: true - '@turbo/linux-arm64@2.10.7': + '@turbo/linux-arm64@2.10.12': optional: true - '@turbo/windows-64@2.10.7': + '@turbo/windows-64@2.10.12': optional: true - '@turbo/windows-arm64@2.10.7': + '@turbo/windows-arm64@2.10.12': optional: true '@tybys/wasm-util@0.10.3': @@ -7407,42 +7090,42 @@ snapshots: '@types/archiver@8.0.0': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/readdir-glob': 1.1.5 '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/busboy@1.5.4': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/connect@3.4.38': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/cookie-parser@1.4.10(@types/express@5.0.6)': dependencies: @@ -7452,7 +7135,7 @@ snapshots: '@types/cross-spawn@6.0.2': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/d3-array@3.0.3': {} @@ -7492,19 +7175,21 @@ snapshots: dependencies: '@types/eslint': 9.6.1 '@types/estree': 1.0.9 + optional: true '@types/eslint@9.6.1': dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 + optional: true '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.1.1': + '@types/express-serve-static-core@5.1.3': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -7512,7 +7197,7 @@ snapshots: '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 + '@types/express-serve-static-core': 5.1.3 '@types/serve-static': 2.2.0 '@types/geojson@7946.0.16': {} @@ -7531,76 +7216,74 @@ snapshots: '@types/jest@30.0.0': dependencies: - expect: 30.4.1 - pretty-format: 30.4.1 + expect: 30.5.0 + pretty-format: 30.5.0 '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@types/lodash@4.17.24': {} + '@types/lodash@4.17.25': {} '@types/methods@1.1.4': {} '@types/ms@2.1.0': {} - '@types/node@26.1.2': + '@types/node@26.4.0': dependencies: undici-types: 8.3.0 '@types/normalize-package-data@2.4.4': {} - '@types/pg@8.20.0': + '@types/pg@8.23.1': dependencies: - '@types/node': 26.1.2 - pg-protocol: 1.15.0 + '@types/node': 26.4.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.5(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/retry@0.12.0': {} '@types/send@1.2.1': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/stack-utils@2.0.3': {} - '@types/superagent@8.1.10': + '@types/superagent@8.1.11': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 26.1.2 + '@types/node': 26.4.0 form-data: 4.0.6 '@types/supertest@7.2.1': dependencies: '@types/methods': 1.1.4 - '@types/superagent': 8.1.10 + '@types/superagent': 8.1.11 '@types/validator@13.15.10': {} @@ -7610,74 +7293,74 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 10.8.0(jiti@2.7.0) - ignore: 7.0.5 + '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/type-utils': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.68.0 + eslint: 10.9.1(jiti@2.7.0) + ignore: 7.0.7 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@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(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(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.68.0 debug: 4.4.3 - eslint: 10.8.0(jiti@2.7.0) + eslint: 10.9.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.68.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.68.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.8.0(jiti@2.7.0) + eslint: 10.9.1(jiti@2.7.0) 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/types@8.68.0': {} - '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.68.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(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 + '@typescript-eslint/project-service': 8.68.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -7685,23 +7368,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - eslint: 10.8.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@6.0.3) + eslint: 10.9.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.68.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.68.0 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.2': {} + '@ungap/structured-clone@1.4.0': {} '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true @@ -7761,7 +7444,7 @@ snapshots: 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) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': @@ -7779,12 +7462,12 @@ snapshots: '@visx/event@4.0.1-alpha.0': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@visx/point': 4.0.1-alpha.0 '@visx/grid@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/group': 4.0.1-alpha.0(react@19.2.8) '@visx/point': 4.0.1-alpha.0 @@ -7795,7 +7478,7 @@ snapshots: '@visx/group@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 classnames: 2.5.1 react: 19.2.8 @@ -7803,8 +7486,8 @@ snapshots: '@visx/responsive@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/lodash': 4.17.24 - '@types/react': 19.2.17 + '@types/lodash': 4.17.25 + '@types/react': 19.2.18 lodash: 4.18.1 react: 19.2.8 @@ -7814,8 +7497,8 @@ snapshots: '@visx/shape@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/lodash': 4.17.24 - '@types/react': 19.2.17 + '@types/lodash': 4.17.25 + '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/group': 4.0.1-alpha.0(react@19.2.8) '@visx/scale': 4.0.1-alpha.0 @@ -7854,20 +7537,26 @@ snapshots: dependencies: '@webassemblyjs/helper-numbers': 1.13.2 '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + optional: true - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + '@webassemblyjs/floating-point-hex-parser@1.13.2': + optional: true - '@webassemblyjs/helper-api-error@1.13.2': {} + '@webassemblyjs/helper-api-error@1.13.2': + optional: true - '@webassemblyjs/helper-buffer@1.14.1': {} + '@webassemblyjs/helper-buffer@1.14.1': + optional: true '@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 + optional: true - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + optional: true '@webassemblyjs/helper-wasm-section@1.14.1': dependencies: @@ -7875,16 +7564,20 @@ snapshots: '@webassemblyjs/helper-buffer': 1.14.1 '@webassemblyjs/helper-wasm-bytecode': 1.13.2 '@webassemblyjs/wasm-gen': 1.14.1 + optional: true '@webassemblyjs/ieee754@1.13.2': dependencies: '@xtuc/ieee754': 1.2.0 + optional: true '@webassemblyjs/leb128@1.13.2': dependencies: '@xtuc/long': 4.2.2 + optional: true - '@webassemblyjs/utf8@1.13.2': {} + '@webassemblyjs/utf8@1.13.2': + optional: true '@webassemblyjs/wasm-edit@1.14.1': dependencies: @@ -7896,6 +7589,7 @@ snapshots: '@webassemblyjs/wasm-opt': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 '@webassemblyjs/wast-printer': 1.14.1 + optional: true '@webassemblyjs/wasm-gen@1.14.1': dependencies: @@ -7904,6 +7598,7 @@ snapshots: '@webassemblyjs/ieee754': 1.13.2 '@webassemblyjs/leb128': 1.13.2 '@webassemblyjs/utf8': 1.13.2 + optional: true '@webassemblyjs/wasm-opt@1.14.1': dependencies: @@ -7911,6 +7606,7 @@ snapshots: '@webassemblyjs/helper-buffer': 1.14.1 '@webassemblyjs/wasm-gen': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 + optional: true '@webassemblyjs/wasm-parser@1.14.1': dependencies: @@ -7920,15 +7616,19 @@ snapshots: '@webassemblyjs/ieee754': 1.13.2 '@webassemblyjs/leb128': 1.13.2 '@webassemblyjs/utf8': 1.13.2 + optional: true '@webassemblyjs/wast-printer@1.14.1': dependencies: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + optional: true - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} + '@xtuc/ieee754@1.2.0': + optional: true + + '@xtuc/long@4.2.2': + optional: true abort-controller@3.0.0: dependencies: @@ -7937,21 +7637,22 @@ snapshots: accepts@2.0.0: dependencies: mime-types: 3.0.2 - negotiator: 1.0.0 + negotiator: 1.1.0 - acorn-import-phases@1.0.4(acorn@8.17.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 + optional: true - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk@8.3.5: dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} agent-base@6.0.2: dependencies: @@ -7969,19 +7670,22 @@ snapshots: ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 + optional: true - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 ajv-keywords@3.5.2(ajv@6.15.0): dependencies: ajv: 6.15.0 + optional: true ajv-keywords@5.1.0(ajv@8.20.0): dependencies: ajv: 8.20.0 fast-deep-equal: 3.1.3 + optional: true ajv@6.15.0: dependencies: @@ -7990,13 +7694,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.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 - ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -8008,15 +7705,13 @@ snapshots: dependencies: string-width: 4.2.3 - ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} + ansi-regex@6.3.0: {} ansi-styles@4.3.0: dependencies: @@ -8026,7 +7721,7 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.2.0: {} + ansis@4.3.1: {} anymatch@3.1.3: dependencies: @@ -8080,7 +7775,7 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 readdir-glob: 3.0.0 - tar-stream: 3.2.0 + tar-stream: 3.2.1 zip-stream: 7.0.5 transitivePeerDependencies: - bare-abort-controller @@ -8097,77 +7792,21 @@ snapshots: argparse@2.0.1: {} - 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-timsort@1.0.3: {} array-union@2.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 - - 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 - asap@2.0.6: {} astral-regex@2.0.0: {} - async-function@1.0.0: {} - async@3.2.6: {} asynckit@0.4.0: {} - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - aws-ssl-profiles@1.1.2: {} - axios@1.19.0: + axios@1.20.0: dependencies: follow-redirects: 1.16.0 form-data: 4.0.6 @@ -8179,30 +7818,30 @@ snapshots: b4a@1.8.1: {} - babel-jest@30.4.1(@babel/core@7.29.7): + babel-jest@30.5.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 - '@jest/transform': 30.4.1 + '@jest/transform': 30.5.0 '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.4.0(@babel/core@7.29.7) + babel-plugin-istanbul: 8.0.0 + babel-preset-jest: 30.5.0(@babel/core@7.29.7) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@7.0.1: + babel-plugin-istanbul@8.0.0: dependencies: '@babel/helper-plugin-utils': 7.29.7 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.6 istanbul-lib-instrument: 6.0.3 - test-exclude: 6.0.0 + test-exclude: 7.0.2 transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@30.4.0: + babel-plugin-jest-hoist@30.5.0: dependencies: '@types/babel__core': 7.20.5 @@ -8225,50 +7864,46 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) - babel-preset-jest@30.4.0(@babel/core@7.29.7): + babel-preset-jest@30.5.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 - babel-plugin-jest-hoist: 30.4.0 + babel-plugin-jest-hoist: 30.5.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) balanced-match@4.0.4: {} - bare-events@2.9.1: {} + bare-events@2.9.2: {} - bare-fs@4.7.3: + bare-fs@4.8.1: dependencies: - bare-events: 2.9.1 - bare-path: 3.0.1 - bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 + bare-events: 2.9.2 + bare-path: 3.1.1 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.2 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller - react-native-b4a - bare-os@3.9.3: {} + bare-path@3.1.1: {} - bare-path@3.0.1: - dependencies: - bare-os: 3.9.3 - - bare-stream@2.13.3(bare-events@2.9.1): + bare-stream@2.13.4(bare-events@2.9.2): dependencies: b4a: 1.8.1 - streamx: 2.28.0 + streamx: 2.28.1 teex: 1.0.1 optionalDependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.5.2: dependencies: - bare-path: 3.0.1 + bare-path: 3.1.1 base64-js@1.5.1: {} - baseline-browser-mapping@2.10.42: {} + baseline-browser-mapping@2.11.20: {} better-result@2.10.0: {} @@ -8283,12 +7918,12 @@ snapshots: body-parser@2.3.0: dependencies: bytes: 3.1.2 - content-type: 2.0.0 + content-type: 2.1.0 debug: 4.4.3 http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -8315,13 +7950,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.4: + browserslist@4.28.8: dependencies: - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 - electron-to-chromium: 1.5.387 - node-releases: 2.0.50 - update-browserslist-db: 1.2.3(browserslist@4.28.4) + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.416 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) bs-logger@0.2.6: dependencies: @@ -8366,10 +8001,10 @@ snapshots: confbox: 0.2.4 defu: 6.1.7 dotenv: 17.4.2 - exsolve: 1.1.0 + exsolve: 1.1.1 giget: 3.3.1 jiti: 2.7.0 - ohash: 2.0.11 + ohash: 2.0.12 pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.3.1 @@ -8380,13 +8015,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.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 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8398,13 +8026,15 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001803: {} + caniuse-lite@1.0.30001810: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + char-regex@1.0.2: {} chardet@2.2.0: {} @@ -8426,25 +8056,27 @@ snapshots: chokidar@4.0.3: dependencies: readdirp: 4.1.2 + optional: true chokidar@5.0.0: dependencies: - readdirp: 5.0.0 + readdirp: 5.1.1 - chrome-trace-event@1.0.4: {} + chrome-trace-event@1.0.4: + optional: true ci-info@3.8.0: {} ci-info@4.4.0: {} - cjs-module-lexer@2.2.0: {} + cjs-module-lexer@2.2.1: {} class-transformer@0.5.1: {} class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.13.8 + libphonenumber-js: 1.13.12 validator: 13.15.35 classnames@2.5.1: {} @@ -8453,11 +8085,11 @@ snapshots: cli-boxes@2.2.1: {} - cli-cursor@3.1.0: + cli-cursor@5.0.0: dependencies: - restore-cursor: 3.1.0 + restore-cursor: 5.1.0 - cli-spinners@2.9.2: {} + cli-spinners@3.4.0: {} cli-table3@0.6.5: dependencies: @@ -8480,8 +8112,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone@1.0.4: {} - cluster-key-slot@1.1.1: {} co@4.6.0: {} @@ -8498,9 +8128,10 @@ snapshots: dependencies: delayed-stream: 1.0.0 - commander@2.20.3: {} + commander@15.0.0: {} - commander@4.1.1: {} + commander@2.20.3: + optional: true comment-json@5.0.0: dependencies: @@ -8539,7 +8170,7 @@ snapshots: content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@2.0.0: {} @@ -8565,14 +8196,15 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@8.3.6(typescript@5.9.3): + cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.2 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 + optional: true crc-32@1.2.2: {} @@ -8652,28 +8284,6 @@ snapshots: data-uri-to-buffer@4.0.1: {} - 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: - dependencies: - ms: 2.1.3 - debug@4.3.4: dependencies: ms: 2.1.2 @@ -8690,22 +8300,6 @@ snapshots: deepmerge@4.3.1: {} - defaults@1.0.4: - dependencies: - clone: 1.0.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 - defu@6.1.7: {} del@6.1.1: @@ -8746,20 +8340,12 @@ snapshots: dependencies: path-type: 4.0.0 - doctrine@2.1.0: + dotenv-expand@13.0.0: dependencies: - esutils: 2.0.3 - - dotenv-expand@12.0.3: - dependencies: - dotenv: 16.6.1 + dotenv: 17.4.2 dotenv@16.0.3: {} - dotenv@16.6.1: {} - - dotenv@17.4.1: {} - dotenv@17.4.2: {} dunder-proto@1.0.1: @@ -8781,7 +8367,7 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.5.387: {} + electron-to-chromium@1.5.416: {} elkjs@0.11.1: {} @@ -8791,6 +8377,8 @@ snapshots: emoji-regex@9.2.2: {} + emojilib@2.4.0: {} + empathic@2.0.0: {} encodeurl@2.0.0: {} @@ -8799,12 +8387,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.24.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - enhanced-resolve@5.24.3: + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -8817,75 +8400,11 @@ snapshots: dependencies: is-arrayish: 0.2.1 - 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.1 - 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-module-lexer@2.3.0: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.2: dependencies: @@ -8898,18 +8417,7 @@ snapshots: 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 + es-toolkit@1.51.0: {} escalade@3.2.0: {} @@ -8919,67 +8427,21 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-import-resolver-node@0.3.10: + eslint-plugin-simple-import-sort@14.0.0(eslint@10.9.1(jiti@2.7.0)): dependencies: - debug: 3.2.7 - is-core-module: 2.16.2 - resolve: 2.0.0-next.7 - transitivePeerDependencies: - - supports-color + eslint: 10.9.1(jiti@2.7.0) - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.8.0(jiti@2.7.0)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)): dependencies: - debug: 3.2.7 + eslint: 10.9.1(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.8.0(jiti@2.7.0) - eslint-import-resolver-node: 0.3.10 - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)): - 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 - doctrine: 2.1.0 - eslint: 10.8.0(jiti@2.7.0) - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.8.0(jiti@2.7.0)) - 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@10.8.0(jiti@2.7.0))(typescript@6.0.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-simple-import-sort@14.0.0(eslint@10.8.0(jiti@2.7.0)): - dependencies: - eslint: 10.8.0(jiti@2.7.0) - - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)): - dependencies: - eslint: 10.8.0(jiti@2.7.0) - optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 + optional: true eslint-scope@9.1.2: dependencies: @@ -8992,9 +8454,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.8.0(jiti@2.7.0): + eslint@10.9.1(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.8.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -9021,7 +8483,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -9031,8 +8493,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esprima@4.0.1: {} @@ -9045,7 +8507,8 @@ snapshots: dependencies: estraverse: 5.3.0 - estraverse@4.3.0: {} + estraverse@4.3.0: + optional: true estraverse@5.3.0: {} @@ -9057,7 +8520,7 @@ snapshots: events-universal@1.0.1: dependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - bare-abort-controller @@ -9077,14 +8540,14 @@ snapshots: exit-x@0.2.2: {} - expect@30.4.1: + expect@30.5.0: dependencies: - '@jest/expect-utils': 30.4.1 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.4.1 - jest-message-util: 30.4.1 - jest-mock: 30.4.1 - jest-util: 30.4.1 + '@jest/expect-utils': 30.5.0 + '@jest/get-type': 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 express@5.2.1: dependencies: @@ -9108,7 +8571,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.3 + qs: 6.16.0 range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 @@ -9119,7 +8582,7 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.1.0: {} + exsolve@1.1.1: {} extend@3.0.2: {} @@ -9151,11 +8614,21 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.4: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fast-write-atomic@0.2.1: {} - fastq@1.20.1: + fastq@1.20.3: dependencies: reusify: 1.1.0 @@ -9163,9 +8636,9 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fetch-blob@3.2.0: dependencies: @@ -9176,7 +8649,7 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-type@21.3.4: + file-type@22.0.2: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 @@ -9206,7 +8679,7 @@ snapshots: make-dir: 3.1.0 pkg-dir: 4.2.0 - find-my-way@9.7.0: + find-my-way@9.9.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -9224,28 +8697,24 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.4: {} follow-redirects@1.16.0: {} - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2): + fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.3)(webpack@5.106.2): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@6.0.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 @@ -9254,8 +8723,9 @@ snapshots: schema-utils: 3.3.0 semver: 7.8.5 tapable: 2.3.3 - typescript: 5.9.3 + typescript: 6.0.3 webpack: 5.106.2 + optional: true form-data@4.0.6: dependencies: @@ -9279,10 +8749,10 @@ snapshots: fp-ts@2.16.0: {} - framer-motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + framer-motion@13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - motion-dom: 12.43.0 - motion-utils: 12.39.0 + motion-dom: 13.1.1 + motion-utils: 13.0.0 tslib: 2.8.1 optionalDependencies: react: 19.2.8 @@ -9297,6 +8767,7 @@ snapshots: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 + optional: true fs-extra@11.1.1: dependencies: @@ -9308,29 +8779,13 @@ snapshots: dependencies: minimatch: 5.1.9 - fs-monkey@1.1.0: {} + fs-monkey@1.1.0: + optional: true fs.realpath@1.0.0: {} - fsevents@2.3.3: - optional: true - function-bind@1.1.2: {} - 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 - - functions-have-names@1.2.3: {} - gaxios@7.1.3: dependencies: extend: 3.0.2 @@ -9340,7 +8795,7 @@ snapshots: transitivePeerDependencies: - supports-color - gaxios@7.2.0: + gaxios@7.3.1: dependencies: extend: 3.0.2 https-proxy-agent: 7.0.6 @@ -9350,7 +8805,7 @@ snapshots: gcp-metadata@8.1.2: dependencies: - gaxios: 7.2.0 + gaxios: 7.3.1 google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: @@ -9360,12 +8815,12 @@ snapshots: dependencies: is-property: 1.0.2 - generator-function@2.0.1: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9390,12 +8845,6 @@ snapshots: get-stream@6.0.1: {} - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - giget@3.3.1: {} glob-parent@5.1.2: @@ -9406,7 +8855,8 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} + glob-to-regexp@0.4.1: + optional: true glob@10.5.0: dependencies: @@ -9419,7 +8869,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 @@ -9444,11 +8894,6 @@ snapshots: dependencies: ini: 2.0.0 - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -9462,7 +8907,7 @@ snapshots: dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 7.2.0 + gaxios: 7.3.1 gcp-metadata: 8.1.2 google-logging-utils: 1.1.3 jws: 4.0.1 @@ -9471,21 +8916,21 @@ snapshots: google-logging-utils@1.1.3: {} - googleapis-common@8.0.2: + googleapis-common@8.0.3: dependencies: extend: 3.0.2 gaxios: 7.1.3 google-auth-library: 10.9.0 google-logging-utils: 1.1.3 - qs: 6.15.3 + qs: 6.16.0 url-template: 2.0.8 transitivePeerDependencies: - supports-color - googleapis@173.0.0: + googleapis@176.0.0: dependencies: google-auth-library: 10.9.0 - googleapis-common: 8.0.2 + googleapis-common: 8.0.3 transitivePeerDependencies: - supports-color @@ -9506,18 +8951,8 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - has-bigints@1.1.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -9591,12 +9026,13 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.7: {} import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + optional: true import-local@3.2.0: dependencies: @@ -9616,108 +9052,40 @@ snapshots: ini@2.0.0: {} - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.4 - side-channel: 1.1.1 - internmap@2.0.3: {} - ioredis@5.11.1: + ioredis@6.0.0: dependencies: - '@ioredis/commands': 1.10.0 + '@ioredis/commands': 2.0.0 cluster-key-slot: 1.1.1 debug: 4.4.3 denque: 2.1.0 redis-errors: 1.2.0 - redis-parser: 3.0.0 standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color ipaddr.js@1.9.1: {} - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-arrayish@0.2.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 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-callable@1.2.7: {} - is-core-module@2.16.2: dependencies: hasown: 2.0.4 - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-docker@2.2.1: {} - 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-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} - 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 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-interactive@1.0.0: {} - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 + is-interactive@2.0.0: {} is-number@7.0.0: {} @@ -9729,50 +9097,11 @@ snapshots: is-property@1.0.2: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - is-stream@2.0.1: {} is-stream@4.0.1: {} - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.22 - - is-unicode-supported@0.1.0: {} - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 + is-unicode-supported@2.1.0: {} is-windows@1.0.2: {} @@ -9782,8 +9111,6 @@ snapshots: isarray@1.0.0: {} - isarray@2.0.5: {} - isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -9791,7 +9118,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 7.8.5 @@ -9825,31 +9152,31 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jest-changed-files@30.4.1: + jest-changed-files@30.5.0: dependencies: execa: 5.1.1 - jest-util: 30.4.1 + jest-util: 30.5.0 p-limit: 3.1.0 - jest-circus@30.4.2: + jest-circus@30.5.0: dependencies: - '@jest/environment': 30.4.1 - '@jest/expect': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/environment': 30.5.0 + '@jest/expect': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 is-generator-fn: 2.1.0 - jest-each: 30.4.1 - jest-matcher-utils: 30.4.1 - jest-message-util: 30.4.1 - jest-runtime: 30.4.2 - jest-snapshot: 30.4.1 - jest-util: 30.4.1 + jest-each: 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-runtime: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 p-limit: 3.1.0 - pretty-format: 30.4.1 + pretty-format: 30.5.0 pure-rand: 7.0.1 slash: 3.0.0 stack-utils: 2.0.6 @@ -9857,17 +9184,17 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)): + jest-cli@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)): dependencies: - '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - '@jest/test-result': 30.4.1 - '@jest/types': 30.4.1 + '@jest/core': 30.5.0(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - jest-util: 30.4.1 - jest-validate: 30.4.1 + jest-config: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + jest-util: 30.5.0 + jest-validate: 30.5.0 yargs: 17.7.3 transitivePeerDependencies: - '@types/node' @@ -9876,266 +9203,263 @@ snapshots: - supports-color - ts-node - jest-config@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)): + jest-config@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)): dependencies: '@babel/core': 7.29.7 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.4.0 - '@jest/test-sequencer': 30.4.1 - '@jest/types': 30.4.1 - babel-jest: 30.4.1(@babel/core@7.29.7) + '@jest/get-type': 30.5.0 + '@jest/pattern': 30.5.0 + '@jest/test-sequencer': 30.5.0 + '@jest/types': 30.5.0 + babel-jest: 30.5.0(@babel/core@7.29.7) chalk: 4.1.2 ci-info: 4.4.0 deepmerge: 4.3.1 - glob: 10.5.0 + glob: 13.0.6 graceful-fs: 4.2.11 - jest-circus: 30.4.2 - jest-docblock: 30.4.0 - jest-environment-node: 30.4.1 - jest-regex-util: 30.4.0 - jest-resolve: 30.4.1 - jest-runner: 30.4.2 - jest-util: 30.4.1 - jest-validate: 30.4.1 + jest-circus: 30.5.0 + jest-docblock: 30.5.0 + jest-environment-node: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-runner: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 parse-json: 5.2.0 - pretty-format: 30.4.1 + pretty-format: 30.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 26.1.2 - ts-node: 10.9.2(@types/node@26.1.2)(typescript@6.0.3) + '@types/node': 26.4.0 + ts-node: 10.9.2(@types/node@26.4.0)(typescript@6.0.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-diff@30.4.1: + jest-diff@30.5.0: dependencies: - '@jest/diff-sequences': 30.4.0 - '@jest/get-type': 30.1.0 + '@jest/diff-sequences': 30.5.0 + '@jest/get-type': 30.5.0 chalk: 4.1.2 - pretty-format: 30.4.1 + pretty-format: 30.5.0 - jest-docblock@30.4.0: + jest-docblock@30.5.0: dependencies: detect-newline: 3.1.0 - jest-each@30.4.1: + jest-each@30.5.0: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.4.1 + '@jest/get-type': 30.5.0 + '@jest/types': 30.5.0 chalk: 4.1.2 - jest-util: 30.4.1 - pretty-format: 30.4.1 + jest-util: 30.5.0 + pretty-format: 30.5.0 - jest-environment-node@30.4.1: + jest-environment-node@30.5.0: dependencies: - '@jest/environment': 30.4.1 - '@jest/fake-timers': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 - jest-mock: 30.4.1 - jest-util: 30.4.1 - jest-validate: 30.4.1 + '@jest/environment': 30.5.0 + '@jest/fake-timers': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 - jest-haste-map@30.4.1: + jest-haste-map@30.5.0: dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/types': 30.5.0 + '@parcel/watcher': 2.6.0 + '@types/node': 26.4.0 anymatch: 3.1.3 fb-watchman: 2.0.2 + fdir: 6.5.0(picomatch@4.0.7) graceful-fs: 4.2.11 - jest-regex-util: 30.4.0 - jest-util: 30.4.1 - jest-worker: 30.4.1 - picomatch: 4.0.5 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 + jest-regex-util: 30.5.0 + jest-util: 30.5.0 + jest-worker: 30.5.0 + picomatch: 4.0.7 - jest-leak-detector@30.4.1: + jest-leak-detector@30.5.0: dependencies: - '@jest/get-type': 30.1.0 - pretty-format: 30.4.1 + '@jest/get-type': 30.5.0 + pretty-format: 30.5.0 - jest-matcher-utils@30.4.1: + jest-matcher-utils@30.5.0: dependencies: - '@jest/get-type': 30.1.0 + '@jest/get-type': 30.5.0 chalk: 4.1.2 - jest-diff: 30.4.1 - pretty-format: 30.4.1 + jest-diff: 30.5.0 + pretty-format: 30.5.0 - jest-message-util@30.4.1: + jest-message-util@30.5.0: dependencies: '@babel/code-frame': 7.29.7 - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - jest-util: 30.4.1 - picomatch: 4.0.5 - pretty-format: 30.4.1 + jest-util: 30.5.0 + picomatch: 4.0.7 + pretty-format: 30.5.0 slash: 3.0.0 stack-utils: 2.0.6 - jest-mock@30.4.1: + jest-mock@30.5.0: dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 - jest-util: 30.4.1 - - jest-pnp-resolver@1.2.3(jest-resolve@30.4.1): - optionalDependencies: - jest-resolve: 30.4.1 + '@jest/expect-utils': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 + jest-util: 30.5.0 - jest-regex-util@30.4.0: {} + jest-regex-util@30.5.0: {} - jest-resolve-dependencies@30.4.2: + jest-resolve-dependencies@30.5.0: dependencies: - jest-regex-util: 30.4.0 - jest-snapshot: 30.4.1 + jest-regex-util: 30.5.0 + jest-snapshot: 30.5.0 transitivePeerDependencies: - supports-color - jest-resolve@30.4.1: + jest-resolve@30.5.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 - jest-pnp-resolver: 1.2.3(jest-resolve@30.4.1) - jest-util: 30.4.1 - jest-validate: 30.4.1 + jest-haste-map: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 slash: 3.0.0 unrs-resolver: 1.12.2 - jest-runner@30.4.2: + jest-runner@30.5.0: dependencies: - '@jest/console': 30.4.1 - '@jest/environment': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/console': 30.5.0 + '@jest/environment': 30.5.0 + '@jest/source-map': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-docblock: 30.4.0 - jest-environment-node: 30.4.1 - jest-haste-map: 30.4.1 - jest-leak-detector: 30.4.1 - jest-message-util: 30.4.1 - jest-resolve: 30.4.1 - jest-runtime: 30.4.2 - jest-util: 30.4.1 - jest-watcher: 30.4.1 - jest-worker: 30.4.1 + jest-docblock: 30.5.0 + jest-environment-node: 30.5.0 + jest-haste-map: 30.5.0 + jest-leak-detector: 30.5.0 + jest-message-util: 30.5.0 + jest-resolve: 30.5.0 + jest-runtime: 30.5.0 + jest-util: 30.5.0 + jest-watcher: 30.5.0 + jest-worker: 30.5.0 p-limit: 3.1.0 - source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - jest-runtime@30.4.2: + jest-runtime@30.5.0: dependencies: - '@jest/environment': 30.4.1 - '@jest/fake-timers': 30.4.1 - '@jest/globals': 30.4.1 - '@jest/source-map': 30.0.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/environment': 30.5.0 + '@jest/fake-timers': 30.5.0 + '@jest/globals': 30.5.0 + '@jest/source-map': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 - cjs-module-lexer: 2.2.0 + cjs-module-lexer: 2.2.1 collect-v8-coverage: 1.0.3 - glob: 10.5.0 + es-module-lexer: 2.3.2 + glob: 13.0.6 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 - jest-message-util: 30.4.1 - jest-mock: 30.4.1 - jest-regex-util: 30.4.0 - jest-resolve: 30.4.1 - jest-snapshot: 30.4.1 - jest-util: 30.4.1 + jest-haste-map: 30.5.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.4.1: + jest-snapshot@30.5.0: dependencies: '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/types': 7.29.7 - '@jest/expect-utils': 30.4.1 - '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 + '@babel/types': 7.29.8 + '@jest/expect-utils': 30.5.0 + '@jest/get-type': 30.5.0 + '@jest/snapshot-utils': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) chalk: 4.1.2 - expect: 30.4.1 + expect: 30.5.0 graceful-fs: 4.2.11 - jest-diff: 30.4.1 - jest-matcher-utils: 30.4.1 - jest-message-util: 30.4.1 - jest-util: 30.4.1 - pretty-format: 30.4.1 + jest-diff: 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + pretty-format: 30.5.0 semver: 7.8.5 synckit: 0.11.13 transitivePeerDependencies: - supports-color - jest-util@30.4.1: + jest-util@30.5.0: dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 - picomatch: 4.0.5 + picomatch: 4.0.7 - jest-validate@30.4.1: + jest-validate@30.5.0: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.4.1 + '@jest/get-type': 30.5.0 + '@jest/types': 30.5.0 camelcase: 6.3.0 chalk: 4.1.2 leven: 3.1.0 - pretty-format: 30.4.1 + pretty-format: 30.5.0 - jest-watcher@30.4.1: + jest-watcher@30.5.0: dependencies: - '@jest/test-result': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 30.4.1 + jest-util: 30.5.0 string-length: 4.0.2 jest-worker@27.5.1: dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 merge-stream: 2.0.0 supports-color: 8.1.1 + optional: true - jest-worker@30.4.1: + jest-worker@30.5.0: dependencies: - '@types/node': 26.1.2 - '@ungap/structured-clone': 1.3.2 - jest-util: 30.4.1 + '@types/node': 26.4.0 + '@ungap/structured-clone': 1.4.0 + jest-util: 30.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)): + jest@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)): dependencies: - '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - '@jest/types': 30.4.1 + '@jest/core': 30.5.0(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + '@jest/types': 30.5.0 import-local: 3.2.0 - jest-cli: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) + jest-cli: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10147,14 +9471,15 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.15.0: + js-yaml@3.15.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 + optional: true js-yaml@5.2.2: dependencies: @@ -10176,10 +9501,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json5@1.0.2: - dependencies: - minimist: 1.2.8 - json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -10233,7 +9554,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.13.8: {} + libphonenumber-js@1.13.12: {} lightningcss-android-arm64@1.32.0: optional: true @@ -10288,7 +9609,8 @@ snapshots: load-esm@1.0.3: {} - loader-runner@4.3.2: {} + loader-runner@4.3.2: + optional: true locate-path@5.0.0: dependencies: @@ -10324,16 +9646,16 @@ snapshots: lodash@4.18.1: {} - log-symbols@4.1.0: + log-symbols@7.0.1: dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 long@5.3.2: {} lru-cache@10.4.3: {} - lru-cache@11.5.1: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: dependencies: @@ -10341,17 +9663,17 @@ snapshots: lru.min@1.1.4: {} - lucide-react@1.28.0(react@19.2.8): + lucide-react@1.38.0(react@19.2.8): dependencies: react: 19.2.8 - magic-string@0.30.17: + magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - magic-string@0.30.21: + magic-string@1.0.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 make-dir@3.1.0: dependencies: @@ -10363,19 +9685,16 @@ snapshots: make-error@1.3.6: {} - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - math-intrinsics@1.1.0: {} media-typer@0.3.0: {} - media-typer@1.1.0: {} + media-typer@1.1.1: {} memfs@3.5.3: dependencies: fs-monkey: 1.1.0 + optional: true merge-descriptors@2.0.0: {} @@ -10406,9 +9725,11 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + min-indent@1.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: brace-expansion: 5.0.8 @@ -10428,11 +9749,19 @@ snapshots: minipass@7.1.3: {} - motion-dom@12.43.0: + motion-dom@13.1.1: dependencies: - motion-utils: 12.39.0 + motion-utils: 13.0.0 + + motion-utils@13.0.0: {} - motion-utils@12.39.0: {} + motion@13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + framer-motion: 13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) ms@2.1.2: {} @@ -10445,7 +9774,7 @@ snapshots: concat-stream: 2.0.0 type-is: 1.6.18 - mute-stream@2.0.0: {} + mute-stream@3.0.0: {} mysql2@3.15.3: dependencies: @@ -10463,13 +9792,15 @@ snapshots: dependencies: lru.min: 1.1.4 - nanoid@3.3.16: {} + nanoid@3.3.18: {} napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} - negotiator@1.0.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 neo-async@2.6.2: {} @@ -10480,46 +9811,45 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - next@16.2.12(@opentelemetry/api@1.4.1)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.3(@opentelemetry/api@1.4.1)(@types/node@26.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.2.12 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 - postcss: 8.5.23 + '@next/env': 16.3.3 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + postcss: 8.5.26 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(react@19.2.8) 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 + '@next/swc-darwin-arm64': 16.3.3 + '@next/swc-darwin-x64': 16.3.3 + '@next/swc-linux-arm64-gnu': 16.3.3 + '@next/swc-linux-arm64-musl': 16.3.3 + '@next/swc-linux-x64-gnu': 16.3.3 + '@next/swc-linux-x64-musl': 16.3.3 + '@next/swc-win32-arm64-msvc': 16.3.3 + '@next/swc-win32-x64-msvc': 16.3.3 '@opentelemetry/api': 1.4.1 - sharp: 0.35.3(@types/node@26.1.2) + sharp: 0.35.4(@types/node@26.4.0) transitivePeerDependencies: - '@babel/core' - '@types/node' - babel-plugin-macros - node-abort-controller@3.1.1: {} + node-abort-controller@3.1.1: + optional: true - node-domexception@1.0.0: {} + node-addon-api@7.1.1: {} - node-emoji@1.11.0: - dependencies: - lodash: 4.18.1 + node-domexception@1.0.0: {} - node-exports-info@1.6.2: + node-emoji@2.2.0: dependencies: - array.prototype.flatmap: 1.3.3 - es-errors: 1.3.0 - object.entries: 1.1.9 - semver: 6.3.1 + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 node-fetch@2.6.11: dependencies: @@ -10537,7 +9867,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.50: {} + node-releases@2.0.54: {} normalize-package-data@2.5.0: dependencies: @@ -10569,45 +9899,7 @@ snapshots: 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 - - ohash@2.0.11: {} + ohash@2.0.12: {} on-finished@2.4.1: dependencies: @@ -10621,6 +9913,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -10635,23 +9931,16 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@5.4.1: + ora@9.4.1: dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.2 p-filter@2.1.0: dependencies: @@ -10691,6 +9980,7 @@ snapshots: parent-module@1.0.1: dependencies: callsites: 3.1.0 + optional: true parse-json@5.2.0: dependencies: @@ -10716,7 +10006,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.2 minipass: 7.1.3 path-to-regexp@8.4.2: {} @@ -10734,11 +10024,11 @@ snapshots: pg-int8@1.0.1: {} - pg-pool@3.14.0(pg@8.22.0): + pg-pool@3.14.0(pg@8.23.0): dependencies: - pg: 8.22.0 + pg: 8.23.0 - pg-protocol@1.15.0: {} + pg-protocol@1.16.0: {} pg-types@2.2.0: dependencies: @@ -10748,11 +10038,11 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.22.0: + pg@8.23.0: dependencies: pg-connection-string: 2.14.0 - pg-pool: 3.14.0(pg@8.22.0) - pg-protocol: 1.15.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -10766,10 +10056,10 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} + picomatch@4.0.7: {} + pirates@4.0.7: {} pkg-dir@4.2.0: @@ -10779,16 +10069,14 @@ snapshots: pkg-types@2.3.1: dependencies: confbox: 0.2.4 - exsolve: 1.1.0 + exsolve: 1.1.1 pathe: 2.0.3 pluralize@8.0.0: {} - possible-typed-array-names@1.1.0: {} - - postcss@8.5.23: + postcss@8.5.26: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -10814,12 +10102,12 @@ snapshots: prettier@3.9.6: {} - pretty-format@30.4.1: + pretty-format@30.5.0: dependencies: - '@jest/schemas': 30.4.1 + '@jest/react-is-18': react-is@18.3.1 + '@jest/react-is-19': react-is@19.2.8 + '@jest/schemas': 30.5.0 ansi-styles: 5.2.0 - react-is-18: react-is@18.3.1 - react-is-19: react-is@19.2.8 prisma-dbml-generator@0.12.0: dependencies: @@ -10829,12 +10117,12 @@ snapshots: - encoding - supports-color - prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): + prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): dependencies: - '@prisma/config': 7.9.1 + '@prisma/config': 7.10.0 '@prisma/dev': 0.24.17(typescript@6.0.3) - '@prisma/engines': 7.9.1 - '@prisma/studio-core': 0.33.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@prisma/engines': 7.10.0 + '@prisma/studio-core': 0.33.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: @@ -10876,7 +10164,7 @@ snapshots: pure-rand@7.0.1: {} - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -10953,40 +10241,17 @@ snapshots: readdir-glob@3.0.0: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 - readdirp@4.1.2: {} + readdirp@4.1.2: + optional: true - readdirp@5.0.0: {} + readdirp@5.1.1: {} redis-errors@1.2.0: {} - redis-parser@3.0.0: - dependencies: - redis-errors: 1.2.0 - reflect-metadata@0.2.2: {} - 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 - remeda@2.33.4: {} replace-string@3.1.0: {} @@ -10999,7 +10264,8 @@ snapshots: dependencies: resolve-from: 5.0.0 - resolve-from@4.0.0: {} + resolve-from@4.0.0: + optional: true resolve-from@5.0.0: {} @@ -11009,19 +10275,10 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.7: + restore-cursor@5.1.0: 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 - - restore-cursor@3.1.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 + onetime: 7.0.0 + signal-exit: 4.1.0 ret@0.5.0: {} @@ -11055,37 +10312,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - rxjs@7.8.2: dependencies: tslib: 2.8.1 - 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.1.2: {} 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 - safe-regex2@5.1.1: dependencies: ret: 0.5.0 @@ -11099,6 +10333,7 @@ snapshots: '@types/json-schema': 7.0.15 ajv: 6.15.0 ajv-keywords: 3.5.2(ajv@6.15.0) + optional: true schema-utils@4.3.3: dependencies: @@ -11106,6 +10341,7 @@ snapshots: ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) + optional: true semver@5.7.2: {} @@ -11142,62 +10378,40 @@ snapshots: server-only@0.0.1: {} - 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 - setprototypeof@1.2.0: {} - sharp@0.35.3(@types/node@26.1.2): + sharp@0.35.4(@types/node@26.4.0): 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': 26.1.2 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 26.4.0 optional: true shebang-command@2.0.0: @@ -11240,6 +10454,10 @@ snapshots: sisteransi@1.0.5: {} + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + slash@3.0.0: {} slice-ansi@3.0.0: @@ -11250,19 +10468,15 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + optional: true source-map@0.6.1: {} - source-map@0.7.4: {} + source-map@0.7.6: {} spdx-correct@3.2.0: dependencies: @@ -11294,10 +10508,7 @@ snapshots: std-env@3.10.0: {} - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 + stdin-discarder@0.3.2: {} stream-browserify@3.0.0: dependencies: @@ -11306,7 +10517,7 @@ snapshots: streamsearch@1.1.0: {} - streamx@2.28.0: + streamx@2.28.1: dependencies: events-universal: 1.0.1 fast-fifo: 1.3.2 @@ -11332,29 +10543,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 - 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: + string-width@8.2.2: 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 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 string_decoder@1.1.1: dependencies: @@ -11370,7 +10562,7 @@ snapshots: strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.3.0 strip-bom@3.0.0: {} @@ -11403,7 +10595,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.15.3 + qs: 6.16.0 transitivePeerDependencies: - supports-color @@ -11430,12 +10622,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swagger-ui-dist@5.32.8: + swagger-ui-dist@5.32.14: dependencies: '@scarf/scarf': 1.4.0 - symbol-observable@4.0.0: {} - synckit@0.11.13: dependencies: '@pkgr/core': 0.3.6 @@ -11452,12 +10642,12 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar-stream@3.2.0: + tar-stream@3.2.1: dependencies: b4a: 1.8.1 - bare-fs: 4.7.3 + bare-fs: 4.8.1 fast-fifo: 1.3.2 - streamx: 2.28.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -11465,7 +10655,7 @@ snapshots: teex@1.0.1: dependencies: - streamx: 2.28.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - react-native-b4a @@ -11500,21 +10690,23 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.48.0 + terser: 5.51.2 webpack: 5.106.2 + optional: true - terser@5.48.0: + terser@5.51.2: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true - test-exclude@6.0.0: + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 - glob: 7.2.3 - minimatch: 3.1.5 + glob: 10.5.0 + minimatch: 10.2.6 text-decoder@1.2.7: dependencies: @@ -11524,15 +10716,13 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tmp@0.2.1: dependencies: rimraf: 3.0.2 - tmpl@1.0.5: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -11551,12 +10741,12 @@ snapshots: dependencies: typescript: 6.0.3 - ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)))(typescript@6.0.3): + ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.5.0)(@jest/types@30.5.0)(babel-jest@30.5.0(@babel/core@7.29.7))(jest-util@30.5.0)(jest@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)))(typescript@6.0.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) + jest: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11566,20 +10756,20 @@ snapshots: yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.7 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - babel-jest: 30.4.1(@babel/core@7.29.7) - jest-util: 30.4.1 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + babel-jest: 30.5.0(@babel/core@7.29.7) + jest-util: 30.5.0 - ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3): + ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 + '@tsconfig/node10': 1.0.13 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 26.1.2 - acorn: 8.17.0 + '@types/node': 26.4.0 + acorn: 8.18.0 acorn-walk: 8.3.5 arg: 4.1.3 create-require: 1.1.1 @@ -11594,16 +10784,10 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.24.1 + enhanced-resolve: 5.24.5 tapable: 2.3.3 tsconfig-paths: 4.2.0 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 + optional: true tsconfig-paths@4.2.0: dependencies: @@ -11613,14 +10797,14 @@ snapshots: tslib@2.8.1: {} - turbo@2.10.7: + turbo@2.10.12: optionalDependencies: - '@turbo/darwin-64': 2.10.7 - '@turbo/darwin-arm64': 2.10.7 - '@turbo/linux-64': 2.10.7 - '@turbo/linux-arm64': 2.10.7 - '@turbo/windows-64': 2.10.7 - '@turbo/windows-arm64': 2.10.7 + '@turbo/darwin-64': 2.10.12 + '@turbo/darwin-arm64': 2.10.12 + '@turbo/linux-64': 2.10.12 + '@turbo/linux-arm64': 2.10.12 + '@turbo/windows-64': 2.10.12 + '@turbo/windows-arm64': 2.10.12 type-check@0.4.0: dependencies: @@ -11647,47 +10831,12 @@ snapshots: type-is@2.1.0: dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 + content-type: 2.1.0 + media-typer: 1.1.1 mime-types: 3.0.2 - 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 - typedarray@0.0.6: {} - typescript@5.9.3: {} - typescript@6.0.3: {} uglify-js@3.19.3: @@ -11699,15 +10848,10 @@ snapshots: uint8array-extras@1.5.0: {} - 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@8.3.0: {} + unicode-emoji-modifier-base@1.0.0: {} + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 @@ -11743,9 +10887,9 @@ 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.4): + update-browserslist-db@1.3.2(browserslist@4.28.8): dependencies: - browserslist: 4.28.4 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 @@ -11782,25 +10926,20 @@ snapshots: vary@1.1.2: {} - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - watchpack@2.5.2: dependencies: graceful-fs: 4.2.11 - - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 + optional: true web-streams-polyfill@3.3.3: {} webidl-conversions@3.0.1: {} - webpack-node-externals@3.0.0: {} + webpack-node-externals@3.0.0: + optional: true - webpack-sources@3.5.0: {} + webpack-sources@3.5.1: + optional: true webpack@5.106.2: dependencies: @@ -11810,12 +10949,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@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.4 + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) + browserslist: 4.28.8 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.1 - es-module-lexer: 2.3.0 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.2 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -11827,7 +10966,7 @@ snapshots: tapable: 2.3.3 terser-webpack-plugin: 5.6.1(webpack@5.106.2) watchpack: 2.5.2 - webpack-sources: 3.5.0 + webpack-sources: 3.5.1 transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -11841,53 +10980,13 @@ snapshots: - lightningcss - postcss - uglify-js + optional: true 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 @@ -11900,12 +10999,6 @@ snapshots: wordwrap@1.0.0: {} - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -11947,7 +11040,7 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.3: {} + yoctocolors@2.2.0: {} zeptomatch@2.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ca5057..b9a489a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,9 +2,9 @@ packages: - 'apps/*' - 'packages/*' overrides: - # googleapis 173 pulls google-auth-library ^10.2.0 (10.9.0) while its own - # googleapis-common pins 10.5.0 exactly — two copies make the OAuth2Client - # types incompatible. Dedupe to a single (latest) version. + # googleapis 176 and its googleapis-common both pin google-auth-library 10.5.0, but other + # dependents resolve 10.9.0 — without this override pnpm installs both copies and the two + # OAuth2Client types stop being assignable. Dedupe to a single (latest) version. google-auth-library: 10.9.0 # Security: force patched transitive versions (advisories on the ranges Next # pulled). sharp < 0.35.0 = libvips CVEs (image optimization, production); diff --git a/turbo.json b/turbo.json index fd36514..9a811ba 100644 --- a/turbo.json +++ b/turbo.json @@ -7,6 +7,9 @@ "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, + "@dropto/types#build": { + "outputs": [] + }, "dev": { "cache": false, "persistent": true From d7cfd5be855b70171f68687c60a874b063f730d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 15:05:02 +0200 Subject: [PATCH 07/26] refactor(web): share the pane listing and selection between both panes --- apps/web/next.config.ts | 13 +- apps/web/src/common/hooks/useBrowsePane.ts | 178 ++----- apps/web/src/common/hooks/useEntryListing.ts | 153 ++++++ .../web/src/common/hooks/useEntrySelection.ts | 66 +++ apps/web/src/common/hooks/useUploadQueue.ts | 285 ++++++++++++ .../components/layouts/WorkspaceClient.tsx | 438 +++--------------- 6 files changed, 607 insertions(+), 526 deletions(-) create mode 100644 apps/web/src/common/hooks/useEntryListing.ts create mode 100644 apps/web/src/common/hooks/useEntrySelection.ts create mode 100644 apps/web/src/common/hooks/useUploadQueue.ts diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index c2616f1..5b2fbc1 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -4,6 +4,10 @@ import path from 'node:path'; const monorepoRoot = path.join(__dirname, '../..'); const isDevelopment = process.env.NODE_ENV === 'development'; +// HSTS on https origins only. Sent from a local http build it would pin `localhost` to https for +// two years — and localhost is shared by every dev server on this machine. +const servesHttps = (process.env.APP_URL ?? '').startsWith('https://'); + // Google origins (Picker/OAuth scripts, frames, XHRs, images) allowed by the CSP. const GOOGLE_SCRIPT = 'https://apis.google.com https://*.gstatic.com'; const GOOGLE_FRAME = @@ -36,12 +40,19 @@ const securityHeaders = [ { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, - { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, // Drop ambient access to sensor/geolocation APIs the app never uses. { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()', }, + ...(servesHttps + ? [ + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + ] + : []), ]; const nextConfig: NextConfig = { diff --git a/apps/web/src/common/hooks/useBrowsePane.ts b/apps/web/src/common/hooks/useBrowsePane.ts index 461ce95..dbd9337 100644 --- a/apps/web/src/common/hooks/useBrowsePane.ts +++ b/apps/web/src/common/hooks/useBrowsePane.ts @@ -3,22 +3,12 @@ import type { StorageBackend } from '@dropto/types'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { listContentsAction } from '@/actions/storage/storage.actions'; import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; +import { type PaneError, useEntryListing } from '@/common/hooks/useEntryListing'; +import { useEntrySelection } from '@/common/hooks/useEntrySelection'; import type { Crumb, SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; -import { toViewEntries } from '@/common/utils/view-entry.functions'; -/** - * Drive filters by name server-side; S3 lists a whole level and is filtered client-side. - **/ -const serverSearch = (backend: StorageBackend | null, term: string): string | undefined => - backend === 'drive' && term ? term : undefined; - -/** A failed read, surfaced to the pane's error handler. */ -export interface PaneError { - error?: string; - status?: number; -} +export type { PaneError }; /** A single independently-browsable pane of folders/files. */ export interface BrowsePane { @@ -52,42 +42,43 @@ export interface BrowsePane { /** * Owns one browsing pane's state (path, entries, loading, multi-selection). **/ +// The split pane keeps its location in local state; the main pane keeps it in the URL. Everything +// below the location — listing, pagination, selection — is shared with it via the two primitives. export function useBrowsePane( backend: StorageBackend | null, roots: ViewEntry[], onError: (error: PaneError) => void, ): BrowsePane { const [path, setPath] = useState([]); - const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedIds, setSelectedIds] = useState>(new Set()); const [sortKey, setSortKey] = useState('name'); const [sortDir, setSortDir] = useState('asc'); const [search, setSearch] = useState(''); - const [nextPageToken, setNextPageToken] = useState(null); - const [loadingMore, setLoadingMore] = useState(false); const currentFolderId = path.length > 0 ? path[path.length - 1].id : null; const atRoots = path.length === 0; const debouncedSearch = useDebouncedValue(search.trim(), 250); - // Bumped on every listing read; a resolved request whose id no longer matches is stale (the pane - // navigated away mid-flight) and must not paint over the current folder. - const listSeq = useRef(0); - useEffect(() => { setPath([]); }, [backend]); - useEffect(() => { - setSelectedIds(new Set()); - }, [currentFolderId, backend]); - // A new folder starts unfiltered. useEffect(() => { setSearch(''); }, [currentFolderId, backend]); + const listing = useEntryListing({ + backend, + currentFolderId, + roots, + search: debouncedSearch, + sortKey, + sortDir, + onError, + }); + + const selection = useEntrySelection(currentFolderId, backend); + // Keep the latest sortKey for toggleSort without making it depend on (and re-create) on every change. const sortKeyRef = useRef(sortKey); sortKeyRef.current = sortKey; @@ -97,70 +88,6 @@ export function useBrowsePane( setSortKey(key); }, []); - const reload = useCallback(async () => { - const seq = ++listSeq.current; - - if (currentFolderId === null || backend === null) { - setEntries(roots); - setNextPageToken(null); - setLoading(false); - return; - } - - setLoading(true); - const result = await listContentsAction(backend, currentFolderId, { - search: serverSearch(backend, debouncedSearch), - sortKey, - sortDir, - }); - - // Superseded while in flight — a newer read owns the pane now. - if (seq !== listSeq.current) { - return; - } - - if (result.ok) { - setEntries(toViewEntries(result.data?.entries ?? [])); - setNextPageToken(result.data?.nextPageToken ?? null); - } else { - onError({ error: result.error, status: result.status }); - } - setLoading(false); - }, [backend, currentFolderId, roots, onError, debouncedSearch, sortKey, sortDir]); - - useEffect(() => { - void reload(); - }, [reload]); - - const loadMore = useCallback(async () => { - if (nextPageToken === null || backend === null || currentFolderId === null || loadingMore) { - return; - } - - const seq = listSeq.current; - setLoadingMore(true); - const result = await listContentsAction(backend, currentFolderId, { - pageToken: nextPageToken, - search: serverSearch(backend, debouncedSearch), - sortKey, - sortDir, - }); - - // The folder changed under us — appending this page would mix two listings. - if (seq !== listSeq.current) { - setLoadingMore(false); - return; - } - - if (result.ok) { - setEntries((current) => [...current, ...toViewEntries(result.data?.entries ?? [])]); - setNextPageToken(result.data?.nextPageToken ?? null); - } else { - onError({ error: result.error, status: result.status }); - } - setLoadingMore(false); - }, [nextPageToken, backend, currentFolderId, loadingMore, debouncedSearch, sortKey, sortDir, onError]); - const openFolder = useCallback((entry: ViewEntry) => { setPath((current) => [ ...current, @@ -174,43 +101,19 @@ export function useBrowsePane( const goTo = useCallback((next: Crumb[]) => setPath(next), []); - const toggleSelect = useCallback((id: string) => { - setSelectedIds((current) => { - const next = new Set(current); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }, []); - - const selectAll = useCallback(() => { - setSelectedIds(new Set(entries.map((entry) => entry.id))); - }, [entries]); - - const clearSelection = useCallback(() => setSelectedIds(new Set()), []); - - const setSelection = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); - - const pruneSelection = useCallback((id: string) => { - setSelectedIds((current) => { - if (!current.has(id)) { - return current; - } - const next = new Set(current); - next.delete(id); - return next; - }); - }, []); + const { entries } = listing; + const { selectAll: selectIds } = selection; + const selectAll = useCallback( + () => selectIds(entries.map((entry) => entry.id)), + [entries, selectIds], + ); return useMemo( () => ({ path, - entries, - loading, - selectedIds, + entries: listing.entries, + loading: listing.loading, + selectedIds: selection.selectedIds, currentFolderId, atRoots, sortKey, @@ -218,42 +121,33 @@ export function useBrowsePane( toggleSort, search, setSearch, - hasMore: nextPageToken !== null, - loadingMore, - loadMore, + hasMore: listing.hasMore, + loadingMore: listing.loadingMore, + loadMore: listing.loadMore, openFolder, navigate, goTo, - reload, - toggleSelect, + reload: listing.reload, + toggleSelect: selection.toggleSelect, selectAll, - clearSelection, - setSelection, - pruneSelection, + clearSelection: selection.clearSelection, + setSelection: selection.setSelection, + pruneSelection: selection.pruneSelection, }), [ path, - entries, - loading, - selectedIds, + listing, + selection, currentFolderId, atRoots, sortKey, sortDir, toggleSort, search, - nextPageToken, - loadingMore, - loadMore, openFolder, navigate, goTo, - reload, - toggleSelect, selectAll, - clearSelection, - setSelection, - pruneSelection, ], ); } diff --git a/apps/web/src/common/hooks/useEntryListing.ts b/apps/web/src/common/hooks/useEntryListing.ts new file mode 100644 index 0000000..e22a779 --- /dev/null +++ b/apps/web/src/common/hooks/useEntryListing.ts @@ -0,0 +1,153 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { listContentsAction } from '@/actions/storage/storage.actions'; +import type { SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; +import { toViewEntries } from '@/common/utils/view-entry.functions'; + +/** A failed read, surfaced to the pane's error handler. */ +export interface PaneError { + error?: string; + status?: number; +} + +/** Where and how a pane wants its entries listed. */ +interface ListingOptions { + backend: StorageBackend | null; + /** Null at the roots level, where `roots` is shown instead of a server listing. */ + currentFolderId: string | null; + roots: ViewEntry[]; + /** Already debounced by the caller. */ + search: string; + sortKey: SortKey; + sortDir: SortDir; + onError: (error: PaneError) => void; +} + +/** One pane's listing: the entries plus their loading and pagination state. */ +export interface EntryListing { + entries: ViewEntry[]; + setEntries: (update: (current: ViewEntry[]) => ViewEntry[]) => void; + loading: boolean; + /** Whether another page can be loaded (infinite scroll). */ + hasMore: boolean; + loadingMore: boolean; + loadMore: () => Promise; + reload: () => Promise; +} + +/** + * Drive filters by name server-side; S3 lists a whole level and is filtered client-side. + **/ +const serverSearch = (backend: StorageBackend | null, term: string): string | undefined => + backend === 'drive' && term ? term : undefined; + +/** + * Loads one pane's entries, with cursor pagination and stale-response protection. + **/ +// Shared by both panes. They differ only in where the folder and sort come from (the URL for the +// main pane, local state for the split pane), never in how a listing is read. +export function useEntryListing({ + backend, + currentFolderId, + roots, + search, + sortKey, + sortDir, + onError, +}: ListingOptions): EntryListing { + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(false); + const [nextPageToken, setNextPageToken] = useState(null); + const [loadingMore, setLoadingMore] = useState(false); + + // Bumped on every listing read; a resolved request whose id no longer matches is stale (the pane + // navigated away mid-flight) and must not paint over the current folder. + const listSeq = useRef(0); + + const reload = useCallback(async () => { + const seq = ++listSeq.current; + + if (currentFolderId === null || backend === null) { + setEntries(roots); + setNextPageToken(null); + setLoading(false); + return; + } + + setLoading(true); + const result = await listContentsAction(backend, currentFolderId, { + search: serverSearch(backend, search), + sortKey, + sortDir, + }); + + // Superseded while in flight — a newer read owns the pane now. + if (seq !== listSeq.current) { + return; + } + + if (result.ok) { + setEntries(toViewEntries(result.data?.entries ?? [])); + setNextPageToken(result.data?.nextPageToken ?? null); + } else { + onError({ error: result.error, status: result.status }); + } + setLoading(false); + }, [backend, currentFolderId, roots, onError, search, sortKey, sortDir]); + + useEffect(() => { + void reload(); + }, [reload]); + + const loadMore = useCallback(async () => { + if (nextPageToken === null || backend === null || currentFolderId === null || loadingMore) { + return; + } + + const seq = listSeq.current; + setLoadingMore(true); + const result = await listContentsAction(backend, currentFolderId, { + pageToken: nextPageToken, + search: serverSearch(backend, search), + sortKey, + sortDir, + }); + + // The folder changed under us — appending this page would mix two listings. + if (seq !== listSeq.current) { + setLoadingMore(false); + return; + } + + if (result.ok) { + // Append (never replace) so the scroll position is preserved during infinite scroll. + setEntries((current) => [...current, ...toViewEntries(result.data?.entries ?? [])]); + setNextPageToken(result.data?.nextPageToken ?? null); + } else { + onError({ error: result.error, status: result.status }); + } + setLoadingMore(false); + }, [ + nextPageToken, + backend, + currentFolderId, + loadingMore, + search, + sortKey, + sortDir, + onError, + ]); + + return { + entries, + setEntries, + loading, + hasMore: nextPageToken !== null, + loadingMore, + loadMore, + reload, + }; +} diff --git a/apps/web/src/common/hooks/useEntrySelection.ts b/apps/web/src/common/hooks/useEntrySelection.ts new file mode 100644 index 0000000..e321cf7 --- /dev/null +++ b/apps/web/src/common/hooks/useEntrySelection.ts @@ -0,0 +1,66 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useEffect, useState } from 'react'; + +/** Multi-selection over the entries of one pane. */ +export interface EntrySelection { + selectedIds: Set; + toggleSelect: (id: string) => void; + selectAll: (ids: string[]) => void; + clearSelection: () => void; + setSelection: (ids: string[]) => void; + /** Drops a single id (an entry that was deleted or moved away). */ + pruneSelection: (id: string) => void; +} + +/** + * Owns a pane's multi-selection, resetting it whenever the pane changes folder or backend. + **/ +// Shared by both panes: the split pane and the URL-driven main pane had identical copies of this. +export function useEntrySelection( + currentFolderId: string | null, + backend: StorageBackend | null, +): EntrySelection { + const [selectedIds, setSelectedIds] = useState>(new Set()); + + // A different folder (or storage) means a different set of entries — the old ids are meaningless. + useEffect(() => { + setSelectedIds(new Set()); + }, [currentFolderId, backend]); + + const toggleSelect = useCallback((id: string) => { + setSelectedIds((current) => { + const next = new Set(current); + + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + + return next; + }); + }, []); + + const selectAll = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); + + const clearSelection = useCallback(() => setSelectedIds(new Set()), []); + + const setSelection = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); + + const pruneSelection = useCallback((id: string) => { + setSelectedIds((current) => { + if (!current.has(id)) { + return current; + } + + const next = new Set(current); + next.delete(id); + + return next; + }); + }, []); + + return { selectedIds, toggleSelect, selectAll, clearSelection, setSelection, pruneSelection }; +} diff --git a/apps/web/src/common/hooks/useUploadQueue.ts b/apps/web/src/common/hooks/useUploadQueue.ts new file mode 100644 index 0000000..328e2f4 --- /dev/null +++ b/apps/web/src/common/hooks/useUploadQueue.ts @@ -0,0 +1,285 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useRef, useState } from 'react'; + +import { createFolderAction, deleteItemAction } from '@/actions/storage/storage.actions'; +import { uploadFile } from '@/common/services/api/storage.client'; +import type { + BatchRuntime, + UploadBatch, + UploadItem, + ViewEntry, +} from '@/common/types/workspace.types'; +import { extractApiErrorMessage, isCanceledError } from '@/common/utils/error.functions'; +import { + isIgnoredUploadName, + isTopLevelFolder, + topLevelName, + uniqueName, +} from '@/common/utils/upload.functions'; +import { useToast } from '@/components/providers/ToastProvider'; +import { useUploadActions } from '@/components/providers/UploadProvider'; + +/** Where an upload is headed: the target folder and what is already in it. */ +export interface UploadTarget { + folderId: string | null; + entries: ViewEntry[]; +} + +/** The duplicate prompt plus the entry point for a drop. */ +export interface UploadQueue { + /** Top-level names that already exist in the target folder; null when nothing is being asked. */ + duplicate: string[] | null; + resolveDuplicate: (choice: 'replace' | 'keep' | 'cancel') => void; + handleUpload: (items: UploadItem[], pane?: 0 | 1) => Promise; +} + +/** + * Runs the upload queue: duplicate handling, folder creation, per-file progress and rollback. + **/ +// Lifted out of WorkspaceClient, which had it inline. The panes are reached through +// `getPaneTarget` so this never has to know how the workspace stores them. +export function useUploadQueue( + activeBackend: StorageBackend | null, + getPaneTarget: (pane: 0 | 1) => UploadTarget, + reloadPanes: () => Promise, +): UploadQueue { + const toast = useToast(); + const { setBatches, updateTask, setBatchStatus, scheduleRemoveBatch, batchRuntime } = + useUploadActions(); + + const [duplicate, setDuplicate] = useState(null); + const duplicateResolve = useRef<((choice: 'replace' | 'keep' | 'cancel') => void) | null>(null); + + // Undo a batch: delete everything it created so a cancel leaves nothing behind. + const rollbackBatch = useCallback( + async (batchId: string, backend: StorageBackend) => { + // Flip the batch and its rows to cancelled (the dock turns them red). + setBatches((current) => + current.map((batch) => + batch.id === batchId + ? { + ...batch, + status: 'canceling', + tasks: batch.tasks.map((task) => ({ ...task, status: 'canceled', rate: null })), + } + : batch, + ), + ); + const runtime = batchRuntime.current.get(batchId); + if (runtime) { + await Promise.all( + [...runtime.rootFolderIds, ...runtime.looseFileIds].map((id) => + deleteItemAction(backend, id), + ), + ); + batchRuntime.current.delete(batchId); + } + await reloadPanes(); + setBatchStatus(batchId, 'canceled'); + scheduleRemoveBatch(batchId); + }, + [reloadPanes, setBatchStatus, scheduleRemoveBatch, setBatches, batchRuntime], + ); + + const askDuplicate = useCallback((names: string[]): Promise<'replace' | 'keep' | 'cancel'> => { + setDuplicate(names); + return new Promise((resolve) => { + duplicateResolve.current = resolve; + }); + }, []); + + const resolveDuplicate = useCallback((choice: 'replace' | 'keep' | 'cancel') => { + setDuplicate(null); + duplicateResolve.current?.(choice); + duplicateResolve.current = null; + }, []); + + const handleUpload = useCallback( + async (droppedItems: UploadItem[], pane: 0 | 1 = 0) => { + const items = droppedItems.filter((item) => !isIgnoredUploadName(item.relativePath)); + const { folderId: targetFolderId, entries: existing } = getPaneTarget(pane); + if (targetFolderId === null || activeBackend === null || items.length === 0) { + return; + } + const backend = activeBackend; + + // Detect name conflicts among the top-level items being added here. + const existingNames = new Set(existing.map((entry) => entry.name)); + const topLevel = new Map(); + for (const item of items) { + const top = topLevelName(item.relativePath); + topLevel.set(top, topLevel.get(top) === true || isTopLevelFolder(item.relativePath)); + } + const conflicts = [...topLevel.keys()].filter((name) => existingNames.has(name)); + + const renameMap = new Map(); + // "Replace": upload first, delete originals only once they're safely in. + let replaceIds: string[] = []; + if (conflicts.length > 0) { + const choice = await askDuplicate(conflicts); + if (choice === 'cancel') { + return; + } + if (choice === 'replace') { + replaceIds = existing + .filter((entry) => conflicts.includes(entry.name)) + .map((entry) => entry.id); + } else { + const taken = new Set(existingNames); + for (const name of conflicts) { + const renamed = uniqueName(name, taken); + taken.add(renamed); + renameMap.set(name, renamed); + } + } + } + + // Apply any renames (folders via their path, loose files via an upload name). + const finalItems: UploadItem[] = items.map((item) => { + const top = topLevelName(item.relativePath); + const renamed = renameMap.get(top); + if (!renamed) { + return item; + } + return isTopLevelFolder(item.relativePath) + ? { ...item, relativePath: `${renamed}${item.relativePath.slice(top.length)}` } + : { ...item, relativePath: renamed, uploadName: renamed }; + }); + + const folderTops = new Set( + finalItems + .filter((item) => isTopLevelFolder(item.relativePath)) + .map((item) => topLevelName(item.relativePath)), + ); + const looseCount = finalItems.filter((item) => !isTopLevelFolder(item.relativePath)).length; + const kind: UploadBatch['kind'] = + folderTops.size === 1 && looseCount === 0 ? 'folder' : 'files'; + const folderName = kind === 'folder' ? [...folderTops][0] : null; + + const batchId = crypto.randomUUID(); + const runtime: BatchRuntime = { + controller: new AbortController(), + looseFileIds: [], + rootFolderIds: [], + }; + batchRuntime.current.set(batchId, runtime); + const { signal } = runtime.controller; + + const queue = finalItems.map((item) => { + const slash = item.relativePath.lastIndexOf('/'); + return { + taskId: crypto.randomUUID(), + item, + dirPath: slash === -1 ? '' : item.relativePath.slice(0, slash), + }; + }); + setBatches((current) => [ + ...current, + { + id: batchId, + kind, + folderName, + status: 'uploading', + tasks: queue.map(({ taskId, item }) => ({ + id: taskId, + name: item.uploadName ?? item.file.name, + size: item.file.size, + percent: 0, + rate: null, + status: 'pending' as const, + })), + }, + ]); + + // Recreate the nested folder structure, tracking top-level folders for cancel. + const folderIdByPath = new Map([['', targetFolderId]]); + const ensureFolder = async (dirPath: string): Promise => { + const cached = folderIdByPath.get(dirPath); + if (cached) { + return cached; + } + const slash = dirPath.lastIndexOf('/'); + const parentPath = slash === -1 ? '' : dirPath.slice(0, slash); + const name = slash === -1 ? dirPath : dirPath.slice(slash + 1); + const parentId = await ensureFolder(parentPath); + const result = await createFolderAction(backend, parentId, name); + if (!result.ok || !result.data) { + throw new Error(result.error ?? 'Failed to create folder.'); + } + const created = result.data; + folderIdByPath.set(dirPath, created.id); + if (parentPath === '') { + runtime.rootFolderIds.push(created.id); + } + return created.id; + }; + + for (const { taskId, item, dirPath } of queue) { + if (signal.aborted) { + break; + } + updateTask(batchId, taskId, { status: 'uploading' }); + try { + // eslint-disable-next-line no-await-in-loop -- uploads run one at a time (progress + folder dedup) + const uploadFolderId = await ensureFolder(dirPath); + // eslint-disable-next-line no-await-in-loop -- sequential upload queue + const result = await uploadFile( + backend, + uploadFolderId, + item.file, + ({ percent, rate }) => { + updateTask(batchId, taskId, { + percent, + rate, + status: percent >= 100 ? 'processing' : 'uploading', + }); + }, + signal, + item.uploadName, + ); + if (dirPath === '') { + runtime.looseFileIds.push(result.fileId); + } + updateTask(batchId, taskId, { status: 'done', percent: 100, rate: null }); + } catch (error) { + if (isCanceledError(error)) { + break; + } + updateTask(batchId, taskId, { status: 'error', rate: null }); + toast.error(`${item.uploadName ?? item.file.name}: ${extractApiErrorMessage(error)}`); + } + } + + if (signal.aborted) { + await rollbackBatch(batchId, backend); + } else { + try { + // Replacements are in — now remove the originals they superseded. + if (replaceIds.length > 0) { + await Promise.all(replaceIds.map((id) => deleteItemAction(backend, id))); + } + await reloadPanes(); + } finally { + batchRuntime.current.delete(batchId); + setBatchStatus(batchId, 'done'); + scheduleRemoveBatch(batchId); + } + } + }, + [ + activeBackend, + getPaneTarget, + reloadPanes, + toast, + askDuplicate, + updateTask, + setBatchStatus, + scheduleRemoveBatch, + rollbackBatch, + ], + ); + + return { duplicate, resolveDuplicate, handleUpload }; +} diff --git a/apps/web/src/components/layouts/WorkspaceClient.tsx b/apps/web/src/components/layouts/WorkspaceClient.tsx index e5dc96b..e4c423a 100644 --- a/apps/web/src/components/layouts/WorkspaceClient.tsx +++ b/apps/web/src/components/layouts/WorkspaceClient.tsx @@ -14,7 +14,6 @@ import { import { createFolderAction, deleteItemAction, - listContentsAction, moveItemAction, renameItemAction, resolvePathAction, @@ -23,31 +22,23 @@ import { import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; import { useBrowsePane } from '@/common/hooks/useBrowsePane'; import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; +import { useEntryListing } from '@/common/hooks/useEntryListing'; +import { useEntrySelection } from '@/common/hooks/useEntrySelection'; import { useMediaQuery } from '@/common/hooks/useMediaQuery'; +import { useUploadQueue } from '@/common/hooks/useUploadQueue'; import { fileDownloadUrl, folderDownloadUrl, - uploadFile, } from '@/common/services/api/storage.client'; import { type PickedFolder, usePicker } from '@/common/services/picker/usePicker'; import type { - BatchRuntime, Crumb, SortDir, SortKey, - UploadBatch, - UploadItem, ViewEntry, } from '@/common/types/workspace.types'; -import { extractApiErrorMessage, isCanceledError } from '@/common/utils/error.functions'; +import { extractApiErrorMessage } from '@/common/utils/error.functions'; import { buildWorkspaceUrl, slugify } from '@/common/utils/storage-url'; -import { - isIgnoredUploadName, - isTopLevelFolder, - topLevelName, - uniqueName, -} from '@/common/utils/upload.functions'; -import { toViewEntries } from '@/common/utils/view-entry.functions'; import Button from '@/components/common/Button'; import ConfirmDialog from '@/components/common/ConfirmDialog'; import Icon from '@/components/common/Icon'; @@ -126,31 +117,18 @@ function WorkspaceInner({ const pathname = usePathname(); const toast = useToast(); const { openPicker } = usePicker(); - const { - setBatches, - setDownloads, - updateTask, - setBatchStatus, - scheduleRemoveBatch, - batchRuntime, - } = useUploadActions(); + const { setDownloads } = useUploadActions(); const [statuses, setStatuses] = useState(initialStatuses); const [loadingStatus, setLoadingStatus] = useState(false); const [activeBackend, setActiveBackend] = useState(initialBackend ?? null); const [path, setPath] = useState(initialPath ?? []); - const [entries, setEntries] = useState([]); - const [loadingEntries, setLoadingEntries] = useState(false); const [search, setSearch] = useState(''); - const [nextPageToken, setNextPageToken] = useState(null); - const [loadingMore, setLoadingMore] = useState(false); const [selected, setSelected] = useState(null); - const [duplicate, setDuplicate] = useState(null); const [confirmTarget, setConfirmTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [unselectTarget, setUnselectTarget] = useState(null); const [unselecting, setUnselecting] = useState(false); - const [selectedIds, setSelectedIds] = useState>(new Set()); const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); const [bulkDeleting, setBulkDeleting] = useState(false); const canSplit = useMediaQuery(DESKTOP_QUERY); @@ -171,10 +149,6 @@ function WorkspaceInner({ const [loggingOut, setLoggingOut] = useState(false); const handledParams = useRef(false); - const duplicateResolve = useRef<((choice: 'replace' | 'keep' | 'cancel') => void) | null>(null); - // Bumped on every listing read; a resolved request whose id no longer matches is stale (the - // operator navigated away mid-flight) and must not paint over the current folder. - const listSeq = useRef(0); // id → display name, so navigating (and rebuilding a deep-linked breadcrumb) shows real names. const nameCache = useRef>( new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.name])), @@ -240,6 +214,39 @@ function WorkspaceInner({ [toast, loadStatus], ); + // The main pane's listing and selection — the same primitives the split pane runs on. Only the + // location differs: this pane reads it from the URL, the split pane from local state. + const { + entries, + loading: loadingEntries, + hasMore, + loadingMore, + loadMore: loadMoreEntries, + reload: loadEntries, + } = useEntryListing({ + backend: activeBackend, + currentFolderId, + roots, + search: debouncedSearch, + sortKey, + sortDir, + onError: onPaneError, + }); + + const { + selectedIds, + toggleSelect, + selectAll: selectIds, + clearSelection, + setSelection, + pruneSelection, + } = useEntrySelection(currentFolderId, activeBackend); + + const selectAll = useCallback( + () => selectIds(entries.map((entry) => entry.id)), + [entries, selectIds], + ); + // The second (split) pane: an independent browser over the same backend. const paneB = useBrowsePane(activeBackend, roots, onPaneError); @@ -325,11 +332,6 @@ function WorkspaceInner({ }; }, [pathname, statuses, router]); - // Drop any multi-select whenever the folder or storage changes. - useEffect(() => { - setSelectedIds(new Set()); - }, [currentFolderId, activeBackend]); - useEffect(() => { if (handledParams.current) { return; @@ -360,90 +362,6 @@ function WorkspaceInner({ setSearch(''); }, [currentFolderId, activeBackend]); - const loadEntries = useCallback(async () => { - const seq = ++listSeq.current; - - if (currentFolderId === null || activeBackend === null) { - setEntries(roots); - setNextPageToken(null); - setLoadingEntries(false); - return; - } - - setLoadingEntries(true); - const result = await listContentsAction(activeBackend, currentFolderId, { - // Drive filters server-side; S3 lists the whole level and is filtered client-side. - search: activeBackend === 'drive' && debouncedSearch ? debouncedSearch : undefined, - sortKey, - sortDir, - }); - - // Superseded while in flight — a newer read owns the view now. - if (seq !== listSeq.current) { - return; - } - - if (result.ok) { - setEntries(toViewEntries(result.data?.entries ?? [])); - setNextPageToken(result.data?.nextPageToken ?? null); - } else { - toast.error(result.error ?? 'Failed to open the folder.'); - // Storage disconnected, or the folder was deleted in Drive — refresh so the sidebar reflects it. - if (result.status === 424 || result.status === 404) { - void loadStatus(); - } - } - setLoadingEntries(false); - }, [activeBackend, currentFolderId, roots, toast, loadStatus, debouncedSearch, sortKey, sortDir]); - - useEffect(() => { - void loadEntries(); - }, [loadEntries]); - - const loadMoreEntries = useCallback(async () => { - if ( - nextPageToken === null || - activeBackend === null || - currentFolderId === null || - loadingMore - ) { - return; - } - - const seq = listSeq.current; - setLoadingMore(true); - const result = await listContentsAction(activeBackend, currentFolderId, { - pageToken: nextPageToken, - search: activeBackend === 'drive' && debouncedSearch ? debouncedSearch : undefined, - sortKey, - sortDir, - }); - - // The folder changed under us — appending this page would mix two listings. - if (seq !== listSeq.current) { - setLoadingMore(false); - return; - } - - if (result.ok) { - // Append (never replace) so the scroll position is preserved during infinite scroll. - setEntries((current) => [...current, ...toViewEntries(result.data?.entries ?? [])]); - setNextPageToken(result.data?.nextPageToken ?? null); - } else if (result.status === 424 || result.status === 404) { - void loadStatus(); - } - setLoadingMore(false); - }, [ - nextPageToken, - activeBackend, - currentFolderId, - loadingMore, - debouncedSearch, - sortKey, - sortDir, - loadStatus, - ]); - // Reload both panes after a mutation so the source and destination both refresh. const reloadPanes = useCallback(async () => { await Promise.all([loadEntries(), split ? paneB.reload() : Promise.resolve()]); @@ -618,7 +536,7 @@ function WorkspaceInner({ const failed = results.filter((result) => !result.ok).length; await reloadPanes(); - setSelectedIds(new Set()); + clearSelection(); paneB.clearSelection(); if (selected && moveIds.includes(selected.id)) { setSelected(null); @@ -664,7 +582,7 @@ function WorkspaceInner({ const failed = results.filter((result) => !result.ok).length; await reloadPanes(); - setSelectedIds(new Set()); + clearSelection(); paneB.clearSelection(); if (selected && ids.includes(selected.id)) { setSelected(null); @@ -682,238 +600,19 @@ function WorkspaceInner({ [dragMove, activeBackend, currentFolderId, paneB, reloadPanes, selected, toast], ); - // --- Upload orchestration ----------------------------------------------------- - - // Undo a batch: delete everything it created so a cancel leaves nothing behind. - const rollbackBatch = useCallback( - async (batchId: string, backend: StorageBackend) => { - // Flip the batch and its rows to cancelled (the dock turns them red). - setBatches((current) => - current.map((batch) => - batch.id === batchId - ? { - ...batch, - status: 'canceling', - tasks: batch.tasks.map((task) => ({ ...task, status: 'canceled', rate: null })), - } - : batch, - ), - ); - const runtime = batchRuntime.current.get(batchId); - if (runtime) { - await Promise.all( - [...runtime.rootFolderIds, ...runtime.looseFileIds].map((id) => - deleteItemAction(backend, id), - ), - ); - batchRuntime.current.delete(batchId); - } - await reloadPanes(); - setBatchStatus(batchId, 'canceled'); - scheduleRemoveBatch(batchId); - }, - [reloadPanes, setBatchStatus, scheduleRemoveBatch, setBatches, batchRuntime], + // Uploads (duplicate prompt, folder creation, progress, rollback) live in their own hook. + const getPaneTarget = useCallback( + (pane: 0 | 1) => + pane === 0 + ? { folderId: currentFolderId, entries } + : { folderId: paneB.currentFolderId, entries: paneB.entries }, + [currentFolderId, entries, paneB], ); - const askDuplicate = useCallback((names: string[]): Promise<'replace' | 'keep' | 'cancel'> => { - setDuplicate(names); - return new Promise((resolve) => { - duplicateResolve.current = resolve; - }); - }, []); - - const resolveDuplicate = useCallback((choice: 'replace' | 'keep' | 'cancel') => { - setDuplicate(null); - duplicateResolve.current?.(choice); - duplicateResolve.current = null; - }, []); - - const handleUpload = useCallback( - async (droppedItems: UploadItem[], pane: 0 | 1 = 0) => { - const items = droppedItems.filter((item) => !isIgnoredUploadName(item.relativePath)); - const targetFolderId = pane === 0 ? currentFolderId : paneB.currentFolderId; - const existing = pane === 0 ? entries : paneB.entries; - if (targetFolderId === null || activeBackend === null || items.length === 0) { - return; - } - const backend = activeBackend; - - // Detect name conflicts among the top-level items being added here. - const existingNames = new Set(existing.map((entry) => entry.name)); - const topLevel = new Map(); - for (const item of items) { - const top = topLevelName(item.relativePath); - topLevel.set(top, topLevel.get(top) === true || isTopLevelFolder(item.relativePath)); - } - const conflicts = [...topLevel.keys()].filter((name) => existingNames.has(name)); - - const renameMap = new Map(); - // "Replace": upload first, delete originals only once they're safely in. - let replaceIds: string[] = []; - if (conflicts.length > 0) { - const choice = await askDuplicate(conflicts); - if (choice === 'cancel') { - return; - } - if (choice === 'replace') { - replaceIds = existing - .filter((entry) => conflicts.includes(entry.name)) - .map((entry) => entry.id); - } else { - const taken = new Set(existingNames); - for (const name of conflicts) { - const renamed = uniqueName(name, taken); - taken.add(renamed); - renameMap.set(name, renamed); - } - } - } - - // Apply any renames (folders via their path, loose files via an upload name). - const finalItems: UploadItem[] = items.map((item) => { - const top = topLevelName(item.relativePath); - const renamed = renameMap.get(top); - if (!renamed) { - return item; - } - return isTopLevelFolder(item.relativePath) - ? { ...item, relativePath: `${renamed}${item.relativePath.slice(top.length)}` } - : { ...item, relativePath: renamed, uploadName: renamed }; - }); - - const folderTops = new Set( - finalItems - .filter((item) => isTopLevelFolder(item.relativePath)) - .map((item) => topLevelName(item.relativePath)), - ); - const looseCount = finalItems.filter((item) => !isTopLevelFolder(item.relativePath)).length; - const kind: UploadBatch['kind'] = - folderTops.size === 1 && looseCount === 0 ? 'folder' : 'files'; - const folderName = kind === 'folder' ? [...folderTops][0] : null; - - const batchId = crypto.randomUUID(); - const runtime: BatchRuntime = { - controller: new AbortController(), - looseFileIds: [], - rootFolderIds: [], - }; - batchRuntime.current.set(batchId, runtime); - const { signal } = runtime.controller; - - const queue = finalItems.map((item) => { - const slash = item.relativePath.lastIndexOf('/'); - return { - taskId: crypto.randomUUID(), - item, - dirPath: slash === -1 ? '' : item.relativePath.slice(0, slash), - }; - }); - setBatches((current) => [ - ...current, - { - id: batchId, - kind, - folderName, - status: 'uploading', - tasks: queue.map(({ taskId, item }) => ({ - id: taskId, - name: item.uploadName ?? item.file.name, - size: item.file.size, - percent: 0, - rate: null, - status: 'pending' as const, - })), - }, - ]); - - // Recreate the nested folder structure, tracking top-level folders for cancel. - const folderIdByPath = new Map([['', targetFolderId]]); - const ensureFolder = async (dirPath: string): Promise => { - const cached = folderIdByPath.get(dirPath); - if (cached) { - return cached; - } - const slash = dirPath.lastIndexOf('/'); - const parentPath = slash === -1 ? '' : dirPath.slice(0, slash); - const name = slash === -1 ? dirPath : dirPath.slice(slash + 1); - const parentId = await ensureFolder(parentPath); - const result = await createFolderAction(backend, parentId, name); - if (!result.ok || !result.data) { - throw new Error(result.error ?? 'Failed to create folder.'); - } - const created = result.data; - folderIdByPath.set(dirPath, created.id); - if (parentPath === '') { - runtime.rootFolderIds.push(created.id); - } - return created.id; - }; - - for (const { taskId, item, dirPath } of queue) { - if (signal.aborted) { - break; - } - updateTask(batchId, taskId, { status: 'uploading' }); - try { - // eslint-disable-next-line no-await-in-loop -- uploads run one at a time (progress + folder dedup) - const uploadFolderId = await ensureFolder(dirPath); - // eslint-disable-next-line no-await-in-loop -- sequential upload queue - const result = await uploadFile( - backend, - uploadFolderId, - item.file, - ({ percent, rate }) => { - updateTask(batchId, taskId, { - percent, - rate, - status: percent >= 100 ? 'processing' : 'uploading', - }); - }, - signal, - item.uploadName, - ); - if (dirPath === '') { - runtime.looseFileIds.push(result.fileId); - } - updateTask(batchId, taskId, { status: 'done', percent: 100, rate: null }); - } catch (error) { - if (isCanceledError(error)) { - break; - } - updateTask(batchId, taskId, { status: 'error', rate: null }); - toast.error(`${item.uploadName ?? item.file.name}: ${extractApiErrorMessage(error)}`); - } - } - - if (signal.aborted) { - await rollbackBatch(batchId, backend); - } else { - try { - // Replacements are in — now remove the originals they superseded. - if (replaceIds.length > 0) { - await Promise.all(replaceIds.map((id) => deleteItemAction(backend, id))); - } - await reloadPanes(); - } finally { - batchRuntime.current.delete(batchId); - setBatchStatus(batchId, 'done'); - scheduleRemoveBatch(batchId); - } - } - }, - [ - activeBackend, - currentFolderId, - paneB, - entries, - reloadPanes, - toast, - askDuplicate, - updateTask, - setBatchStatus, - scheduleRemoveBatch, - rollbackBatch, - ], + const { duplicate, resolveDuplicate, handleUpload } = useUploadQueue( + activeBackend, + getPaneTarget, + reloadPanes, ); const handleDownload = useCallback( @@ -991,17 +690,10 @@ function WorkspaceInner({ const forgetEntry = useCallback( (id: string) => { setSelected((current) => (current?.id === id ? null : current)); - setSelectedIds((current) => { - if (!current.has(id)) { - return current; - } - const next = new Set(current); - next.delete(id); - return next; - }); + pruneSelection(id); paneB.pruneSelection(id); }, - [paneB], + [paneB, pruneSelection], ); const confirmDelete = useCallback(async () => { @@ -1028,26 +720,6 @@ function WorkspaceInner({ } }, [activeBackend, confirmTarget, selected, reloadPanes, paneB, toast]); - const toggleSelect = useCallback((id: string) => { - setSelectedIds((current) => { - const next = new Set(current); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }, []); - - const selectAll = useCallback(() => { - setSelectedIds(new Set(entries.map((entry) => entry.id))); - }, [entries]); - - const clearSelection = useCallback(() => setSelectedIds(new Set()), []); - - const setSelection = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); - const confirmBulkDelete = useCallback(async () => { const ids = bulkPane === 0 ? [...selectedIds] : [...paneB.selectedIds]; if (activeBackend === null || ids.length === 0) { @@ -1065,7 +737,7 @@ function WorkspaceInner({ setSelected(null); } if (bulkPane === 0) { - setSelectedIds(new Set()); + clearSelection(); } else { paneB.clearSelection(); } @@ -1310,7 +982,7 @@ function WorkspaceInner({ onCopyEntryLink={handleCopyEntryLink} searchQuery={search} onSearchChange={setSearch} - hasMore={nextPageToken !== null} + hasMore={hasMore} loadingMore={loadingMore} onLoadMore={loadMoreEntries} onMoveIntoFolder={handleMoveIntoFolder} From a8d99e159698b4d1062a59cd2d02b1173ef3cba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 15:28:37 +0200 Subject: [PATCH 08/26] refactor(web): move entry mutations and their dialogs out of the workspace --- apps/web/prettier.config.ts | 2 +- apps/web/src/common/hooks/useEntryListing.ts | 11 +- .../src/common/hooks/useEntryOperations.ts | 353 ++++++++++++ .../components/layouts/WorkspaceClient.tsx | 510 ++---------------- .../components/layouts/WorkspaceDialogs.tsx | 200 +++++++ package.json | 1 + pnpm-lock.yaml | 3 + 7 files changed, 613 insertions(+), 467 deletions(-) create mode 100644 apps/web/src/common/hooks/useEntryOperations.ts create mode 100644 apps/web/src/components/layouts/WorkspaceDialogs.tsx diff --git a/apps/web/prettier.config.ts b/apps/web/prettier.config.ts index eaf6664..bae8e33 100644 --- a/apps/web/prettier.config.ts +++ b/apps/web/prettier.config.ts @@ -1,6 +1,6 @@ import type { Config } from 'prettier'; -import baseConfig from '../../prettier.config'; +import baseConfig from '../../prettier.config.ts'; // The workspace config plus the Tailwind class-sorting plugin (web-only). const config: Config = { diff --git a/apps/web/src/common/hooks/useEntryListing.ts b/apps/web/src/common/hooks/useEntryListing.ts index e22a779..a3ca381 100644 --- a/apps/web/src/common/hooks/useEntryListing.ts +++ b/apps/web/src/common/hooks/useEntryListing.ts @@ -130,16 +130,7 @@ export function useEntryListing({ onError({ error: result.error, status: result.status }); } setLoadingMore(false); - }, [ - nextPageToken, - backend, - currentFolderId, - loadingMore, - search, - sortKey, - sortDir, - onError, - ]); + }, [nextPageToken, backend, currentFolderId, loadingMore, search, sortKey, sortDir, onError]); return { entries, diff --git a/apps/web/src/common/hooks/useEntryOperations.ts b/apps/web/src/common/hooks/useEntryOperations.ts new file mode 100644 index 0000000..96da0ab --- /dev/null +++ b/apps/web/src/common/hooks/useEntryOperations.ts @@ -0,0 +1,353 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { type FormEvent, useCallback, useState } from 'react'; + +import { removeFolderAction } from '@/actions/auth/auth.actions'; +import { + createFolderAction, + deleteItemAction, + renameItemAction, +} from '@/actions/storage/storage.actions'; +import type { ViewEntry } from '@/common/types/workspace.types'; +import { extractApiErrorMessage } from '@/common/utils/error.functions'; +import { useToast } from '@/components/providers/ToastProvider'; + +/** Which pane an operation targets. */ +type Pane = 0 | 1; + +/** A pending extension change awaiting confirmation (the "modal on a modal"). */ +export interface ExtensionWarning { + /** The name exactly as typed (keeps the new extension). */ + use: string; + /** The typed base name but with the original extension kept. */ + keep: string; + /** The original extension (`''` when the file had none). */ + fromExt: string; + /** The new extension (`''` when the new name has none). */ + toExt: string; +} + +/** How the operations reach whichever panes the workspace is showing. */ +interface PaneAccess { + folderId: (pane: Pane) => string | null; + selectedIds: (pane: Pane) => string[]; + clearSelection: (pane: Pane) => void; +} + +interface Options { + backend: StorageBackend | null; + panes: PaneAccess; + /** Refreshes both panes after a mutation. */ + reloadPanes: () => Promise; + /** Refreshes the sidebar (a parent folder or root may have disappeared). */ + loadStatus: () => Promise; + /** Drops ids from the preview and both panes' selection after they are gone or renamed. */ + forgetEntries: (ids: string[]) => void; +} + +/** + * Splits a name into base + extension (leading/trailing dot = no extension). + **/ +const splitExtension = (name: string): { base: string; ext: string } => { + const dot = name.lastIndexOf('.'); + + if (dot <= 0 || dot === name.length - 1) { + return { base: name, ext: '' }; + } + + return { base: name.slice(0, dot), ext: name.slice(dot) }; +}; + +/** + * Every mutation on an entry, together with the dialog state that confirms it. + **/ +// Lifted out of WorkspaceClient: creating, renaming, deleting and unauthorizing all follow the same +// shape (guard → act → reload both panes → forget the id → close), so they belong together. +export function useEntryOperations({ + backend, + panes, + reloadPanes, + loadStatus, + forgetEntries, +}: Options) { + const toast = useToast(); + + const [newFolderOpen, setNewFolderOpen] = useState(false); + const [newFolderName, setNewFolderName] = useState(''); + const [newFolderPane, setNewFolderPane] = useState(0); + const [creating, setCreating] = useState(false); + + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleting, setDeleting] = useState(false); + + const [bulkOpen, setBulkOpen] = useState(false); + const [bulkPane, setBulkPane] = useState(0); + const [bulkDeleting, setBulkDeleting] = useState(false); + + const [renameTarget, setRenameTarget] = useState(null); + const [renameName, setRenameName] = useState(''); + const [renaming, setRenaming] = useState(false); + const [pendingRename, setPendingRename] = useState(null); + const [extWarning, setExtWarning] = useState(null); + + const [unselectTarget, setUnselectTarget] = useState(null); + const [unselecting, setUnselecting] = useState(false); + + // --- New folder --------------------------------------------------------------- + + const openNewFolder = useCallback((pane: Pane) => { + setNewFolderPane(pane); + setNewFolderOpen(true); + }, []); + + const submitNewFolder = useCallback( + async (event: FormEvent) => { + event.preventDefault(); + const name = newFolderName.trim(); + const targetFolderId = panes.folderId(newFolderPane); + + if (targetFolderId === null || backend === null || !name || creating) { + return; + } + + setCreating(true); + try { + const result = await createFolderAction(backend, targetFolderId, name); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + // The parent is gone (deleted outside the app) — drop it from the sidebar roots. + if (result.status === 424 || result.status === 404) { + void loadStatus(); + } + return; + } + + await reloadPanes(); + toast.success(`Folder "${name}" created.`); + setNewFolderOpen(false); + setNewFolderName(''); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setCreating(false); + } + }, + [backend, panes, newFolderPane, newFolderName, creating, reloadPanes, toast, loadStatus], + ); + + // --- Delete ------------------------------------------------------------------- + + const confirmDelete = useCallback(async () => { + if (!deleteTarget || backend === null) { + return; + } + const entry = deleteTarget; + + setDeleting(true); + try { + const result = await deleteItemAction(backend, entry.id); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + return; + } + + await reloadPanes(); + forgetEntries([entry.id]); + toast.success(`${entry.isFolder ? 'Folder' : 'File'} "${entry.name}" deleted.`); + setDeleteTarget(null); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setDeleting(false); + } + }, [backend, deleteTarget, reloadPanes, forgetEntries, toast]); + + const openBulkDelete = useCallback((pane: Pane) => { + setBulkPane(pane); + setBulkOpen(true); + }, []); + + const confirmBulkDelete = useCallback(async () => { + const ids = panes.selectedIds(bulkPane); + + if (backend === null || ids.length === 0) { + return; + } + + setBulkDeleting(true); + try { + const results = await Promise.all(ids.map((id) => deleteItemAction(backend, id))); + const failed = results.filter((result) => !result.ok).length; + + await reloadPanes(); + forgetEntries(ids); + panes.clearSelection(bulkPane); + setBulkOpen(false); + + if (failed > 0) { + toast.error(`Failed to delete ${failed} item${failed === 1 ? '' : 's'}.`); + } else { + toast.success(`Deleted ${ids.length} item${ids.length === 1 ? '' : 's'}.`); + } + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setBulkDeleting(false); + } + }, [backend, bulkPane, panes, reloadPanes, forgetEntries, toast]); + + // --- Rename ------------------------------------------------------------------- + + const openRename = useCallback((entry: ViewEntry) => { + setRenameTarget(entry); + setRenameName(entry.name); + }, []); + + const runRename = useCallback( + async (name: string) => { + if (!renameTarget || backend === null) { + return; + } + + // Renaming to the same name is a no-op (and an S3 self-delete) — guard it. + if (name === renameTarget.name) { + setRenameTarget(null); + setExtWarning(null); + return; + } + + setRenaming(true); + setPendingRename(name); + try { + const result = await renameItemAction(backend, renameTarget.id, name); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + return; + } + + await reloadPanes(); + // The id can change on rename (S3 keys), so drop the old id from selection/preview. + forgetEntries([renameTarget.id]); + toast.success(`Renamed to "${name}".`); + setRenameTarget(null); + setExtWarning(null); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setRenaming(false); + setPendingRename(null); + } + }, + [backend, renameTarget, reloadPanes, forgetEntries, toast], + ); + + const submitRename = useCallback( + (event: FormEvent) => { + event.preventDefault(); + const name = renameName.trim(); + + if (!renameTarget || backend === null || !name || renaming) { + return; + } + if (name === renameTarget.name) { + setRenameTarget(null); + return; + } + + // Warn (Finder-style) before changing a file's extension. Folders have none. + if (!renameTarget.isFolder) { + const fromExt = splitExtension(renameTarget.name).ext; + const { base, ext: toExt } = splitExtension(name); + + if (fromExt.toLowerCase() !== toExt.toLowerCase()) { + setExtWarning({ use: name, keep: `${base}${fromExt}`, fromExt, toExt }); + return; + } + } + + void runRename(name); + }, + [renameName, renameTarget, backend, renaming, runRename], + ); + + // --- Unauthorize a Drive root ------------------------------------------------- + + const confirmUnselect = useCallback(async () => { + if (!unselectTarget) { + return; + } + + setUnselecting(true); + try { + const result = await removeFolderAction(unselectTarget.id); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + return; + } + + setUnselectTarget(null); + await loadStatus(); + toast.success('Folder removed from the app.'); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setUnselecting(false); + } + }, [unselectTarget, loadStatus, toast]); + + return { + newFolder: { + open: newFolderOpen, + name: newFolderName, + creating, + setName: setNewFolderName, + openFor: openNewFolder, + close: () => setNewFolderOpen(false), + submit: submitNewFolder, + }, + remove: { + target: deleteTarget, + deleting, + request: setDeleteTarget, + cancel: () => setDeleteTarget(null), + confirm: confirmDelete, + }, + bulkRemove: { + open: bulkOpen, + pane: bulkPane, + deleting: bulkDeleting, + count: panes.selectedIds(bulkPane).length, + openFor: openBulkDelete, + close: () => setBulkOpen(false), + confirm: confirmBulkDelete, + }, + rename: { + target: renameTarget, + name: renameName, + renaming, + pending: pendingRename, + extWarning, + setName: setRenameName, + open: openRename, + close: () => setRenameTarget(null), + submit: submitRename, + run: runRename, + dismissExtWarning: () => setExtWarning(null), + }, + unselectRoot: { + target: unselectTarget, + busy: unselecting, + request: setUnselectTarget, + cancel: () => setUnselectTarget(null), + confirm: confirmUnselect, + }, + }; +} + +/** Everything the workspace dialogs need to render and drive themselves. */ +export type EntryOperations = ReturnType; diff --git a/apps/web/src/components/layouts/WorkspaceClient.tsx b/apps/web/src/components/layouts/WorkspaceClient.tsx index e4c423a..a3451b4 100644 --- a/apps/web/src/components/layouts/WorkspaceClient.tsx +++ b/apps/web/src/components/layouts/WorkspaceClient.tsx @@ -2,20 +2,16 @@ import type { StorageBackend, StorageStatus } from '@dropto/types'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { claimDriveOwnerAction, disconnectAction, - removeFolderAction, revokeDriveOwnerAction, saveFoldersAction, } from '@/actions/auth/auth.actions'; import { - createFolderAction, - deleteItemAction, moveItemAction, - renameItemAction, resolvePathAction, statusesAction, } from '@/actions/storage/storage.actions'; @@ -23,27 +19,15 @@ import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; import { useBrowsePane } from '@/common/hooks/useBrowsePane'; import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; import { useEntryListing } from '@/common/hooks/useEntryListing'; +import { useEntryOperations } from '@/common/hooks/useEntryOperations'; import { useEntrySelection } from '@/common/hooks/useEntrySelection'; import { useMediaQuery } from '@/common/hooks/useMediaQuery'; import { useUploadQueue } from '@/common/hooks/useUploadQueue'; -import { - fileDownloadUrl, - folderDownloadUrl, -} from '@/common/services/api/storage.client'; +import { fileDownloadUrl, folderDownloadUrl } from '@/common/services/api/storage.client'; import { type PickedFolder, usePicker } from '@/common/services/picker/usePicker'; -import type { - Crumb, - SortDir, - SortKey, - ViewEntry, -} from '@/common/types/workspace.types'; +import type { Crumb, SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; import { extractApiErrorMessage } from '@/common/utils/error.functions'; import { buildWorkspaceUrl, slugify } from '@/common/utils/storage-url'; -import Button from '@/components/common/Button'; -import ConfirmDialog from '@/components/common/ConfirmDialog'; -import Icon from '@/components/common/Icon'; -import Input from '@/components/common/Input'; -import Modal from '@/components/common/Modal'; import { useToast } from '@/components/providers/ToastProvider'; import { UploadProvider, useUploadActions } from '@/components/providers/UploadProvider'; import { STORAGE_ICON } from '@/configs/storage.config'; @@ -54,6 +38,7 @@ import FileBrowser from './FileBrowser'; import MobileMenu from './mobile/MobileMenu'; import PreviewPanel from './PreviewPanel'; import UploadDock from './UploadDock'; +import WorkspaceDialogs from './WorkspaceDialogs'; const FOLDER_MIME = 'application/vnd.google-apps.folder'; const DOWNLOAD_PREPARING_MS = 6000; @@ -78,29 +63,6 @@ interface Props { const pickDefaultBackend = (statuses: StorageStatus[]): StorageBackend | null => statuses.find((status) => status.connected)?.backend ?? null; -/** - * Splits a name into base + extension (leading/trailing dot = no extension). - **/ -const splitExtension = (name: string): { base: string; ext: string } => { - const dot = name.lastIndexOf('.'); - if (dot <= 0 || dot === name.length - 1) { - return { base: name, ext: '' }; - } - return { base: name.slice(0, dot), ext: name.slice(dot) }; -}; - -/** A pending extension change awaiting confirmation (the "modal on a modal"). */ -interface ExtensionWarning { - /** The name exactly as typed (keeps the new extension). */ - use: string; - /** The typed base name but with the original extension kept. */ - keep: string; - /** The original extension (`''` when the file had none). */ - fromExt: string; - /** The new extension (`''` when the new name has none). */ - toExt: string; -} - /** * Main workspace: sidebar + file browser + preview, with uploads and account management. **/ @@ -125,26 +87,10 @@ function WorkspaceInner({ const [path, setPath] = useState(initialPath ?? []); const [search, setSearch] = useState(''); const [selected, setSelected] = useState(null); - const [confirmTarget, setConfirmTarget] = useState(null); - const [deleting, setDeleting] = useState(false); - const [unselectTarget, setUnselectTarget] = useState(null); - const [unselecting, setUnselecting] = useState(false); - const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); - const [bulkDeleting, setBulkDeleting] = useState(false); const canSplit = useMediaQuery(DESKTOP_QUERY); const [split, setSplit] = useState(false); const [activePane, setActivePane] = useState<0 | 1>(0); - const [bulkPane, setBulkPane] = useState<0 | 1>(0); const [dragMove, setDragMove] = useState<{ ids: string[]; sourcePane: 0 | 1 } | null>(null); - const [renameTarget, setRenameTarget] = useState(null); - const [renameName, setRenameName] = useState(''); - const [renaming, setRenaming] = useState(false); - const [pendingRename, setPendingRename] = useState(null); - const [extWarning, setExtWarning] = useState(null); - const [newFolderOpen, setNewFolderOpen] = useState(false); - const [newFolderName, setNewFolderName] = useState(''); - const [newFolderPane, setNewFolderPane] = useState<0 | 1>(0); - const [creating, setCreating] = useState(false); const [saving, setSaving] = useState(false); const [loggingOut, setLoggingOut] = useState(false); @@ -304,7 +250,9 @@ function WorkspaceInner({ ]); setSelected(null); - const missing = restIds.filter((id) => !nameCache.current.has(id) || !linkCache.current.has(id)); + const missing = restIds.filter( + (id) => !nameCache.current.has(id) || !linkCache.current.has(id), + ); if (missing.length === 0) { return; } @@ -601,6 +549,15 @@ function WorkspaceInner({ ); // Uploads (duplicate prompt, folder creation, progress, rollback) live in their own hook. + const paneAccess = useMemo( + () => ({ + folderId: (pane: 0 | 1) => (pane === 0 ? currentFolderId : paneB.currentFolderId), + selectedIds: (pane: 0 | 1) => [...(pane === 0 ? selectedIds : paneB.selectedIds)], + clearSelection: (pane: 0 | 1) => (pane === 0 ? clearSelection() : paneB.clearSelection()), + }), + [currentFolderId, paneB, selectedIds, clearSelection], + ); + const getPaneTarget = useCallback( (pane: 0 | 1) => pane === 0 @@ -609,12 +566,31 @@ function WorkspaceInner({ [currentFolderId, entries, paneB], ); - const { duplicate, resolveDuplicate, handleUpload } = useUploadQueue( - activeBackend, - getPaneTarget, - reloadPanes, + const uploads = useUploadQueue(activeBackend, getPaneTarget, reloadPanes); + + /** + * Drops ids from the preview and both panes' selection (after a delete or a changed id). + **/ + const forgetEntries = useCallback( + (ids: string[]) => { + setSelected((current) => (current && ids.includes(current.id) ? null : current)); + for (const id of ids) { + pruneSelection(id); + paneB.pruneSelection(id); + } + }, + [paneB, pruneSelection], ); + // Entry mutations (new folder, rename, delete, bulk delete, unauthorize) + their dialog state. + const ops = useEntryOperations({ + backend: activeBackend, + panes: paneAccess, + reloadPanes, + loadStatus, + forgetEntries, + }); + const handleDownload = useCallback( (entry: ViewEntry) => { if (activeBackend === null) { @@ -641,188 +617,6 @@ function WorkspaceInner({ [activeBackend], ); - const handleCreateFolder = useCallback( - async (event: FormEvent) => { - event.preventDefault(); - const name = newFolderName.trim(); - const targetFolderId = newFolderPane === 0 ? currentFolderId : paneB.currentFolderId; - if (targetFolderId === null || activeBackend === null || !name || creating) { - return; - } - - setCreating(true); - try { - const result = await createFolderAction(activeBackend, targetFolderId, name); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - // The parent is gone (deleted outside the app) — drop it from the sidebar roots. - if (result.status === 424 || result.status === 404) { - void loadStatus(); - } - return; - } - await reloadPanes(); - toast.success(`Folder "${name}" created.`); - setNewFolderOpen(false); - setNewFolderName(''); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setCreating(false); - } - }, - [ - activeBackend, - currentFolderId, - paneB, - newFolderPane, - newFolderName, - creating, - reloadPanes, - toast, - loadStatus, - ], - ); - - /** - * Drops an id from the preview and both panes' selection (after it's deleted or its id changed). - **/ - const forgetEntry = useCallback( - (id: string) => { - setSelected((current) => (current?.id === id ? null : current)); - pruneSelection(id); - paneB.pruneSelection(id); - }, - [paneB, pruneSelection], - ); - - const confirmDelete = useCallback(async () => { - if (!confirmTarget || activeBackend === null) { - return; - } - const entry = confirmTarget; - - setDeleting(true); - try { - const result = await deleteItemAction(activeBackend, entry.id); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - return; - } - await reloadPanes(); - forgetEntry(entry.id); - toast.success(`${entry.isFolder ? 'Folder' : 'File'} "${entry.name}" deleted.`); - setConfirmTarget(null); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setDeleting(false); - } - }, [activeBackend, confirmTarget, selected, reloadPanes, paneB, toast]); - - const confirmBulkDelete = useCallback(async () => { - const ids = bulkPane === 0 ? [...selectedIds] : [...paneB.selectedIds]; - if (activeBackend === null || ids.length === 0) { - return; - } - const backend = activeBackend; - - setBulkDeleting(true); - try { - const results = await Promise.all(ids.map((id) => deleteItemAction(backend, id))); - const failed = results.filter((result) => !result.ok).length; - - await reloadPanes(); - if (selected && ids.includes(selected.id)) { - setSelected(null); - } - if (bulkPane === 0) { - clearSelection(); - } else { - paneB.clearSelection(); - } - setBulkDeleteOpen(false); - - if (failed > 0) { - toast.error(`Failed to delete ${failed} item${failed === 1 ? '' : 's'}.`); - } else { - toast.success(`Deleted ${ids.length} item${ids.length === 1 ? '' : 's'}.`); - } - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setBulkDeleting(false); - } - }, [activeBackend, bulkPane, selectedIds, paneB, selected, reloadPanes, toast]); - - const openRename = useCallback((entry: ViewEntry) => { - setRenameTarget(entry); - setRenameName(entry.name); - }, []); - - const runRename = useCallback( - async (name: string) => { - if (!renameTarget || activeBackend === null) { - return; - } - // Renaming to the same name is a no-op (and an S3 self-delete) — guard it. - if (name === renameTarget.name) { - setRenameTarget(null); - setExtWarning(null); - return; - } - - setRenaming(true); - setPendingRename(name); - try { - const result = await renameItemAction(activeBackend, renameTarget.id, name); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - return; - } - await reloadPanes(); - // The id can change on rename (S3 keys), so drop the old id from selection/preview. - forgetEntry(renameTarget.id); - toast.success(`Renamed to "${name}".`); - setRenameTarget(null); - setExtWarning(null); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setRenaming(false); - setPendingRename(null); - } - }, - [activeBackend, renameTarget, selected, reloadPanes, paneB, toast], - ); - - const handleRename = useCallback( - (event: FormEvent) => { - event.preventDefault(); - const name = renameName.trim(); - if (!renameTarget || activeBackend === null || !name || renaming) { - return; - } - if (name === renameTarget.name) { - setRenameTarget(null); - return; - } - - // Warn (Finder-style) before changing a file's extension. Folders have none. - if (!renameTarget.isFolder) { - const fromExt = splitExtension(renameTarget.name).ext; - const { base, ext: toExt } = splitExtension(name); - if (fromExt.toLowerCase() !== toExt.toLowerCase()) { - setExtWarning({ use: name, keep: `${base}${fromExt}`, fromExt, toExt }); - return; - } - } - - void runRename(name); - }, - [renameName, renameTarget, activeBackend, renaming, runRename], - ); - const handleManageFolders = useCallback(async () => { try { await openPicker(async (folders: PickedFolder[]) => { @@ -864,27 +658,6 @@ function WorkspaceInner({ } }, [loadStatus, toast]); - const confirmUnselect = useCallback(async () => { - if (!unselectTarget) { - return; - } - setUnselecting(true); - try { - const result = await removeFolderAction(unselectTarget.id); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - return; - } - setUnselectTarget(null); - await loadStatus(); - toast.success('Folder removed from the app.'); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setUnselecting(false); - } - }, [unselectTarget, loadStatus, toast]); - const handleLogout = useCallback(() => { setLoggingOut(true); // /logout revokes the session, clears cookies and redirects (full navigation). @@ -893,7 +666,6 @@ function WorkspaceInner({ const rootLabel = activeStatus?.label ?? 'Home'; const rootIcon = activeBackend ? STORAGE_ICON[activeBackend] : 'Home'; - const bulkCount = bulkPane === 0 ? selectedIds.size : paneB.selectedIds.size; // One set of props for both sidebar shells: the desktop rail and the mobile swipe-open drawer. const sidebarProps: AccountSidebarProps = { @@ -953,22 +725,16 @@ function WorkspaceInner({ setSelected(null); clearSelection(); }} - onUpload={(items) => handleUpload(items, 0)} - onNewFolder={() => { - setNewFolderPane(0); - setNewFolderOpen(true); - }} + onUpload={(items) => uploads.handleUpload(items, 0)} + onNewFolder={() => ops.newFolder.openFor(0)} onToggleSelect={toggleSelect} onSelectAll={selectAll} onClearSelection={clearSelection} onSetSelectedIds={setSelection} - onBulkDelete={() => { - setBulkPane(0); - setBulkDeleteOpen(true); - }} + onBulkDelete={() => ops.bulkRemove.openFor(0)} onDownload={handleDownload} - onRename={openRename} - onDelete={setConfirmTarget} + onRename={ops.rename.open} + onDelete={ops.remove.request} storagePicker={{ storages: statuses, activeBackend, @@ -988,7 +754,7 @@ function WorkspaceInner({ onMoveIntoFolder={handleMoveIntoFolder} onUnselectRoot={ activeBackend === 'drive' && (driveStatus?.isOwner ?? false) - ? setUnselectTarget + ? ops.unselectRoot.request : undefined } /> @@ -1025,22 +791,16 @@ function WorkspaceInner({ setSelected(null); paneB.clearSelection(); }} - onUpload={(items) => handleUpload(items, 1)} - onNewFolder={() => { - setNewFolderPane(1); - setNewFolderOpen(true); - }} + onUpload={(items) => uploads.handleUpload(items, 1)} + onNewFolder={() => ops.newFolder.openFor(1)} onToggleSelect={paneB.toggleSelect} onSelectAll={paneB.selectAll} onClearSelection={paneB.clearSelection} onSetSelectedIds={paneB.setSelection} - onBulkDelete={() => { - setBulkPane(1); - setBulkDeleteOpen(true); - }} + onBulkDelete={() => ops.bulkRemove.openFor(1)} onDownload={handleDownload} - onRename={openRename} - onDelete={setConfirmTarget} + onRename={ops.rename.open} + onDelete={ops.remove.request} sortKey={paneB.sortKey} sortDir={paneB.sortDir} onToggleSort={paneB.toggleSort} @@ -1063,178 +823,16 @@ function WorkspaceInner({ isRoot={activePane === 0 ? atRoots : paneB.atRoots} backend={activeBackend} onClose={() => setSelected(null)} - onDelete={setConfirmTarget} + onDelete={ops.remove.request} onDownload={handleDownload} - onRename={openRename} + onRename={ops.rename.open} onCopyLink={handleCopyEntryLink} />
- resolveDuplicate('cancel')} - title='Items already exist'> -
-

- {duplicate?.length === 1 - ? 'An item with this name already exists here:' - : `${duplicate?.length} items with these names already exist here:`} -

-
    - {duplicate?.map((name) => ( -
  • - {name} -
  • - ))} -
-
- - - -
-
-
- - setNewFolderOpen(false)} title='New folder'> -
- setNewFolderName(event.target.value)} - autoFocus - /> -
- - -
-
-
- - { - setRenameTarget(null); - setExtWarning(null); - }} - title={renameTarget?.isFolder ? 'Rename folder' : 'Rename file'}> -
- setRenameName(event.target.value)} - autoFocus - /> -
- - -
-
-
- - {/* Extension-change confirmation (a modal on top of the rename modal) */} - setExtWarning(null)} - title='Change extension?'> -
-
-
- -
-

- {extWarning?.toExt === '' - ? `Removing the "${extWarning?.fromExt}" extension may change how this file opens.` - : extWarning?.fromExt === '' - ? `Adding the "${extWarning?.toExt}" extension may change this file's type.` - : `Changing the extension from "${extWarning?.fromExt}" to "${extWarning?.toExt}" may change this file's type.`} -

-
-
- - - -
-
-
- - setBulkDeleteOpen(false)} - title='Delete items' - message={`Delete the ${bulkCount} selected item${bulkCount === 1 ? '' : 's'}, including everything inside any selected folders? This cannot be undone.`} - confirmLabel='Delete' - loading={bulkDeleting} - onConfirm={confirmBulkDelete} - /> - - setConfirmTarget(null)} - title={confirmTarget?.isFolder ? 'Delete folder' : 'Delete file'} - message={ - confirmTarget?.isFolder - ? `Delete the folder "${confirmTarget?.name}" and everything inside it? This cannot be undone.` - : `Delete the file "${confirmTarget?.name}"? This cannot be undone.` - } - confirmLabel='Delete' - loading={deleting} - onConfirm={confirmDelete} - /> - - setUnselectTarget(null)} - title='Remove folder' - message={`Remove "${unselectTarget?.name}" from the app? It stays in Google Drive — only its authorization here is revoked.`} - confirmLabel='Remove' - loading={unselecting} - onConfirm={confirmUnselect} - /> +
); } diff --git a/apps/web/src/components/layouts/WorkspaceDialogs.tsx b/apps/web/src/components/layouts/WorkspaceDialogs.tsx new file mode 100644 index 0000000..8c415d1 --- /dev/null +++ b/apps/web/src/components/layouts/WorkspaceDialogs.tsx @@ -0,0 +1,200 @@ +'use client'; + +import type { EntryOperations } from '@/common/hooks/useEntryOperations'; +import type { UploadQueue } from '@/common/hooks/useUploadQueue'; +import Button from '@/components/common/Button'; +import ConfirmDialog from '@/components/common/ConfirmDialog'; +import Icon from '@/components/common/Icon'; +import Input from '@/components/common/Input'; +import Modal from '@/components/common/Modal'; + +interface Props { + ops: EntryOperations; + uploads: UploadQueue; +} + +/** + * Every workspace dialog: duplicate uploads, new folder, rename (plus its extension warning), + * delete, bulk delete and removing an authorized root. + **/ +// Purely presentational — all the state and handlers live in useEntryOperations / useUploadQueue, +// so this file stays a readable list of dialogs instead of thirty drilled props. +export default function WorkspaceDialogs({ ops, uploads }: Props) { + return ( + <> + uploads.resolveDuplicate('cancel')} + title='Items already exist'> +
+

+ {uploads.duplicate?.length === 1 + ? 'An item with this name already exists here:' + : `${uploads.duplicate?.length} items with these names already exist here:`} +

+
    + {uploads.duplicate?.map((name) => ( +
  • + {name} +
  • + ))} +
+
+ + + +
+
+
+ + ops.newFolder.close()} title='New folder'> +
+ ops.newFolder.setName(event.target.value)} + autoFocus + /> +
+ + +
+
+
+ + { + ops.rename.close(); + ops.rename.dismissExtWarning(); + }} + title={ops.rename.target?.isFolder ? 'Rename folder' : 'Rename file'}> +
+ ops.rename.setName(event.target.value)} + autoFocus + /> +
+ + +
+
+
+ + {/* Extension-change confirmation (a modal on top of the rename modal) */} + ops.rename.dismissExtWarning()} + title='Change extension?'> +
+
+
+ +
+

+ {ops.rename.extWarning?.toExt === '' + ? `Removing the "${ops.rename.extWarning?.fromExt}" extension may change how this file opens.` + : ops.rename.extWarning?.fromExt === '' + ? `Adding the "${ops.rename.extWarning?.toExt}" extension may change this file's type.` + : `Changing the extension from "${ops.rename.extWarning?.fromExt}" to "${ops.rename.extWarning?.toExt}" may change this file's type.`} +

+
+
+ + + +
+
+
+ + ops.bulkRemove.close()} + title='Delete items' + message={`Delete the ${ops.bulkRemove.count} selected item${ops.bulkRemove.count === 1 ? '' : 's'}, including everything inside any selected folders? This cannot be undone.`} + confirmLabel='Delete' + loading={ops.bulkRemove.deleting} + onConfirm={ops.bulkRemove.confirm} + /> + + ops.remove.cancel()} + title={ops.remove.target?.isFolder ? 'Delete folder' : 'Delete file'} + message={ + ops.remove.target?.isFolder + ? `Delete the folder "${ops.remove.target?.name}" and everything inside it? This cannot be undone.` + : `Delete the file "${ops.remove.target?.name}"? This cannot be undone.` + } + confirmLabel='Delete' + loading={ops.remove.deleting} + onConfirm={ops.remove.confirm} + /> + + ops.unselectRoot.cancel()} + title='Remove folder' + message={`Remove "${ops.unselectRoot.target?.name}" from the app? It stays in Google Drive — only its authorization here is revoked.`} + confirmLabel='Remove' + loading={ops.unselectRoot.busy} + onConfirm={ops.unselectRoot.confirm} + /> + + ); +} diff --git a/package.json b/package.json index 95a263b..2059ee7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "prettier": "^3.9.6", + "prettier-plugin-tailwindcss": "^0.8.1", "turbo": "^2.10.12", "typescript": "^6.0.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6660eaa..e3a989b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: prettier: specifier: ^3.9.6 version: 3.9.6 + prettier-plugin-tailwindcss: + specifier: ^0.8.1 + version: 0.8.1(prettier@3.9.6) turbo: specifier: ^2.10.12 version: 2.10.12 From 3c9ee526bc244a1f681f0fb375b65873d9fce09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 15:34:57 +0200 Subject: [PATCH 09/26] refactor(web): extract the file row and the drop-item helpers --- .../src/common/utils/drop-items.functions.ts | 71 ++++ .../src/components/layouts/FileBrowser.tsx | 331 ++++++------------ apps/web/src/components/layouts/FileRow.tsx | 130 +++++++ 3 files changed, 313 insertions(+), 219 deletions(-) create mode 100644 apps/web/src/common/utils/drop-items.functions.ts create mode 100644 apps/web/src/components/layouts/FileRow.tsx diff --git a/apps/web/src/common/utils/drop-items.functions.ts b/apps/web/src/common/utils/drop-items.functions.ts new file mode 100644 index 0000000..3cb180a --- /dev/null +++ b/apps/web/src/common/utils/drop-items.functions.ts @@ -0,0 +1,71 @@ +import type { UploadItem } from '@/common/types/workspace.types'; + +/** + * Reads all entries from a directory reader (≤100 per call). + **/ +const readAllEntries = (reader: FileSystemDirectoryReader): Promise => + new Promise((resolve, reject) => reader.readEntries(resolve, reject)); + +/** + * Resolves a file entry into a File. + **/ +const entryToFile = (entry: FileSystemFileEntry): Promise => + new Promise((resolve, reject) => entry.file(resolve, reject)); + +/** + * Recursively walks a dropped entry into flat upload items. + **/ +const walkEntry = async ( + entry: FileSystemEntry, + prefix: string, + out: UploadItem[], +): Promise => { + if (entry.isFile) { + const file = await entryToFile(entry as FileSystemFileEntry); + out.push({ file, relativePath: `${prefix}${entry.name}` }); + return; + } + + const reader = (entry as FileSystemDirectoryEntry).createReader(); + let batch = await readAllEntries(reader); + while (batch.length > 0) { + for (const child of batch) { + // eslint-disable-next-line no-await-in-loop -- sequential walk into a shared, ordered list + await walkEntry(child, `${prefix}${entry.name}/`, out); + } + // eslint-disable-next-line no-await-in-loop -- the directory reader is stateful; read in sequence + batch = await readAllEntries(reader); + } +}; + +/** + * Dropped items → flat upload items (falls back to the plain file list). + **/ +export const resolveDropItems = async ( + entries: FileSystemEntry[], + flatFiles: File[], +): Promise => { + if (entries.length === 0) { + return flatFiles.map((file) => ({ file, relativePath: file.name })); + } + + const out: UploadItem[] = []; + for (const entry of entries) { + // eslint-disable-next-line no-await-in-loop -- entries walked sequentially into an ordered list + await walkEntry(entry, '', out); + } + return out; +}; + +/** + * Picks the icon name for a file's MIME type. + **/ +export const fileIcon = (mimeType: string | null): string => { + if (!mimeType) return 'Document'; + if (mimeType.startsWith('image/')) return 'Photo'; + if (mimeType.startsWith('video/')) return 'Film'; + if (mimeType.startsWith('audio/')) return 'MusicalNote'; + if (mimeType === 'application/pdf') return 'DocumentText'; + if (mimeType.includes('zip') || mimeType.includes('compressed')) return 'ArchiveBox'; + return 'Document'; +}; diff --git a/apps/web/src/components/layouts/FileBrowser.tsx b/apps/web/src/components/layouts/FileBrowser.tsx index 1edea10..22a9e0a 100644 --- a/apps/web/src/components/layouts/FileBrowser.tsx +++ b/apps/web/src/components/layouts/FileBrowser.tsx @@ -4,6 +4,7 @@ import { AnimatePresence, motion } from 'motion/react'; import { type ChangeEvent, type DragEvent, + type KeyboardEvent as ReactKeyboardEvent, type MouseEvent, useCallback, useEffect, @@ -22,11 +23,12 @@ import type { UploadItem, ViewEntry, } from '@/common/types/workspace.types'; -import { formatBytes, formatDate } from '@/common/utils/format.functions'; +import { resolveDropItems } from '@/common/utils/drop-items.functions'; import Icon from '@/components/common/Icon'; import LoadingIndicator from '@/components/loadings/LoadingIndicator'; import Breadcrumb, { type BreadcrumbStoragePicker } from './Breadcrumb'; +import FileRow from './FileRow'; /** Custom DataTransfer type marking an internal drag-to-move (vs an OS file drop). */ const MOVE_MIME = 'application/x-dropto-move'; @@ -114,76 +116,6 @@ interface RowMenu { rect: DOMRect; } -/** - * Icon name for a MIME type. - **/ -const fileIcon = (mimeType: string | null): string => { - if (!mimeType) return 'Document'; - if (mimeType.startsWith('image/')) return 'Photo'; - if (mimeType.startsWith('video/')) return 'Film'; - if (mimeType.startsWith('audio/')) return 'MusicalNote'; - if (mimeType === 'application/pdf') return 'DocumentText'; - if (mimeType.includes('zip') || mimeType.includes('compressed')) return 'ArchiveBox'; - return 'Document'; -}; - -/** - * Reads all entries from a directory reader (≤100 per call). - **/ -const readAllEntries = (reader: FileSystemDirectoryReader): Promise => - new Promise((resolve, reject) => reader.readEntries(resolve, reject)); - -/** - * Resolves a file entry into a File. - **/ -const entryToFile = (entry: FileSystemFileEntry): Promise => - new Promise((resolve, reject) => entry.file(resolve, reject)); - -/** - * Recursively walks a dropped entry into flat upload items. - **/ -const walkEntry = async ( - entry: FileSystemEntry, - prefix: string, - out: UploadItem[], -): Promise => { - if (entry.isFile) { - const file = await entryToFile(entry as FileSystemFileEntry); - out.push({ file, relativePath: `${prefix}${entry.name}` }); - return; - } - - const reader = (entry as FileSystemDirectoryEntry).createReader(); - let batch = await readAllEntries(reader); - while (batch.length > 0) { - for (const child of batch) { - // eslint-disable-next-line no-await-in-loop -- sequential walk into a shared, ordered list - await walkEntry(child, `${prefix}${entry.name}/`, out); - } - // eslint-disable-next-line no-await-in-loop -- the directory reader is stateful; read in sequence - batch = await readAllEntries(reader); - } -}; - -/** - * Dropped items → flat upload items (falls back to the plain file list). - **/ -const resolveDropItems = async ( - entries: FileSystemEntry[], - flatFiles: File[], -): Promise => { - if (entries.length === 0) { - return flatFiles.map((file) => ({ file, relativePath: file.name })); - } - - const out: UploadItem[] = []; - for (const entry of entries) { - // eslint-disable-next-line no-await-in-loop -- entries walked sequentially into an ordered list - await walkEntry(entry, '', out); - } - return out; -}; - /** * Middle pane: Finder-like sortable list + drop target for uploads. **/ @@ -457,6 +389,92 @@ export default function FileBrowser({ onMoveDragStart?.(ids); }; + const handleRowDragOver = (event: DragEvent, entry: ViewEntry) => { + // A folder row accepts a move drop (unless it's part of the dragged selection). + if ( + canModify && + onMoveIntoFolder && + entry.isFolder && + !selectedIds.has(entry.id) && + event.dataTransfer.types.includes(MOVE_MIME) + ) { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; + if (dropFolderId !== entry.id) setDropFolderId(entry.id); + } + }; + + const handleRowDragLeave = (event: DragEvent, entry: ViewEntry) => { + if (event.currentTarget.contains(event.relatedTarget as Node | null)) { + return; + } + setDropFolderId((current) => (current === entry.id ? null : current)); + }; + + const handleRowDrop = (event: DragEvent, entry: ViewEntry) => { + if ( + !canModify || + !onMoveIntoFolder || + !entry.isFolder || + !event.dataTransfer.types.includes(MOVE_MIME) + ) { + return; + } + event.preventDefault(); + event.stopPropagation(); + setDropFolderId(null); + const ids = event.dataTransfer.getData(MOVE_MIME).split(',').filter(Boolean); + if (ids.length > 0 && !ids.includes(entry.id)) { + onMoveIntoFolder(entry.id, ids); + } + }; + + const handleRowClick = (event: MouseEvent, entry: ViewEntry) => { + // Don't let the click reach the finder's deselect handler. + event.stopPropagation(); + const ids = sorted.map((row) => row.id); + // Shift-click: select the range from the anchor to this row. + if (event.shiftKey && onSetSelectedIds && selectionAnchor.current) { + const from = ids.indexOf(selectionAnchor.current); + const to = ids.indexOf(entry.id); + if (from !== -1 && to !== -1) { + const [lo, hi] = from < to ? [from, to] : [to, from]; + onSetSelectedIds(ids.slice(lo, hi + 1)); + return; + } + } + // Cmd/Ctrl-click: toggle this row in the multi-selection. + if ((event.metaKey || event.ctrlKey) && canModify) { + onToggleSelect(entry.id); + selectionAnchor.current = entry.id; + return; + } + // Plain click: single select (preview) and drop any multi-selection. + onSelect(entry); + if (canModify) { + onClearSelection(); + } + selectionAnchor.current = entry.id; + }; + + const handleRowOpen = (entry: ViewEntry) => { + if (entry.isFolder) onOpenFolder(entry); + else if (entry.webViewLink) window.open(entry.webViewLink, '_blank'); + }; + + const handleRowKeyDown = (event: ReactKeyboardEvent, entry: ViewEntry) => { + // Enter opens a folder / selects a file; Space selects. + if (event.key === 'Enter') { + event.preventDefault(); + if (entry.isFolder) onOpenFolder(entry); + else onSelect(entry); + } else if (event.key === ' ') { + event.preventDefault(); + onSelect(entry); + } + }; + const handleInput = (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []); if (files.length > 0) { @@ -669,154 +687,29 @@ export default function FileBrowser({
    - {sorted.map((entry) => { - const selected = entry.id === selectedId; - const checked = selectedIds.has(entry.id); - return ( -
  • -
    handleRowDragStart(event, entry)} - onDragEnd={() => onMoveDragEnd?.()} - onDragOver={(event) => { - // A folder row accepts a move drop (unless it's part of the dragged selection). - if ( - canModify && - onMoveIntoFolder && - entry.isFolder && - !selectedIds.has(entry.id) && - event.dataTransfer.types.includes(MOVE_MIME) - ) { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; - if (dropFolderId !== entry.id) setDropFolderId(entry.id); - } - }} - onDragLeave={(event) => { - if (event.currentTarget.contains(event.relatedTarget as Node | null)) { - return; - } - setDropFolderId((current) => (current === entry.id ? null : current)); - }} - onDrop={(event) => { - if ( - !canModify || - !onMoveIntoFolder || - !entry.isFolder || - !event.dataTransfer.types.includes(MOVE_MIME) - ) { - return; - } - event.preventDefault(); - event.stopPropagation(); - setDropFolderId(null); - const ids = event.dataTransfer.getData(MOVE_MIME).split(',').filter(Boolean); - if (ids.length > 0 && !ids.includes(entry.id)) { - onMoveIntoFolder(entry.id, ids); - } - }} - onClick={(event) => { - // Don't let the click reach the finder's deselect handler. - event.stopPropagation(); - const ids = sorted.map((row) => row.id); - // Shift-click: select the range from the anchor to this row. - if (event.shiftKey && onSetSelectedIds && selectionAnchor.current) { - const from = ids.indexOf(selectionAnchor.current); - const to = ids.indexOf(entry.id); - if (from !== -1 && to !== -1) { - const [lo, hi] = from < to ? [from, to] : [to, from]; - onSetSelectedIds(ids.slice(lo, hi + 1)); - return; - } - } - // Cmd/Ctrl-click: toggle this row in the multi-selection. - if ((event.metaKey || event.ctrlKey) && canModify) { - onToggleSelect(entry.id); - selectionAnchor.current = entry.id; - return; - } - // Plain click: single select (preview) and drop any multi-selection. - onSelect(entry); - if (canModify) { - onClearSelection(); - } - selectionAnchor.current = entry.id; - }} - onDoubleClick={() => { - if (entry.isFolder) onOpenFolder(entry); - else if (entry.webViewLink) window.open(entry.webViewLink, '_blank'); - }} - onKeyDown={(event) => { - // Enter opens a folder / selects a file; Space selects. - if (event.key === 'Enter') { - event.preventDefault(); - if (entry.isFolder) onOpenFolder(entry); - else onSelect(entry); - } else if (event.key === ' ') { - event.preventDefault(); - onSelect(entry); - } - }} - className={`grid ${gridCols} w-full cursor-pointer items-center gap-x-3 rounded-lg px-3 py-2 text-left transition ${ - dropFolderId === entry.id - ? 'bg-green-600/15 ring-2 ring-green-600 ring-inset' - : checked || selected - ? 'bg-green-600/10' - : 'hover:bg-zinc-200 dark:hover:bg-zinc-800' - }`}> - {canModify && ( - // Wrapper keeps the grid cell aligned on desktop; the checkbox shows only on mobile. - - event.stopPropagation()} - onChange={() => onToggleSelect(entry.id)} - className='hidden h-4 w-4 max-sm:block' - /> - {/* Desktop: a check indicator (no checkbox) for selected rows. */} - {checked && ( - - )} - - )} - - {entry.isFolder ? ( - - ) : ( - - )} - {entry.name} - - - {formatDate(entry.modifiedTime)} - - - {entry.isFolder ? '—' : formatBytes(entry.size)} - - {(canModify || (rootMenu && entry.isFolder)) && ( -
    - -
    - )} -
    -
  • - ); - })} + {sorted.map((entry) => ( + onMoveDragEnd?.()} + onDragOver={handleRowDragOver} + onDragLeave={handleRowDragLeave} + onDrop={handleRowDrop} + onClick={handleRowClick} + onDoubleClick={handleRowOpen} + onKeyDown={handleRowKeyDown} + onToggleCheck={(row) => onToggleSelect(row.id)} + onOpenMenu={openMenu} + /> + ))}
{loadingMore && ( diff --git a/apps/web/src/components/layouts/FileRow.tsx b/apps/web/src/components/layouts/FileRow.tsx new file mode 100644 index 0000000..e09bea1 --- /dev/null +++ b/apps/web/src/components/layouts/FileRow.tsx @@ -0,0 +1,130 @@ +'use client'; + +import type { DragEvent, KeyboardEvent, MouseEvent } from 'react'; + +import type { ViewEntry } from '@/common/types/workspace.types'; +import { fileIcon } from '@/common/utils/drop-items.functions'; +import { formatBytes, formatDate } from '@/common/utils/format.functions'; +import Icon from '@/components/common/Icon'; + +interface Props { + entry: ViewEntry; + /** Single-selected (the one shown in the preview panel). */ + selected: boolean; + /** Part of the multi-selection. */ + checked: boolean; + /** A move is hovering this folder row. */ + isDropTarget: boolean; + /** This row's action menu is open. */ + menuOpen: boolean; + canModify: boolean; + /** Whether the row shows the action menu button at all. */ + showMenu: boolean; + /** The grid template shared with the header, so columns line up. */ + gridCols: string; + onDragStart: (event: DragEvent, entry: ViewEntry) => void; + onDragEnd: () => void; + onDragOver: (event: DragEvent, entry: ViewEntry) => void; + onDragLeave: (event: DragEvent, entry: ViewEntry) => void; + onDrop: (event: DragEvent, entry: ViewEntry) => void; + onClick: (event: MouseEvent, entry: ViewEntry) => void; + onDoubleClick: (entry: ViewEntry) => void; + onKeyDown: (event: KeyboardEvent, entry: ViewEntry) => void; + onToggleCheck: (entry: ViewEntry) => void; + onOpenMenu: (event: MouseEvent, entry: ViewEntry) => void; +} + +/** + * One row of the file list: icon, name, modified date, size and the action menu. + **/ +// Every interaction is decided by the browser above — this only reports events and paints state, +// which keeps the list markup readable on its own. +export default function FileRow({ + entry, + selected, + checked, + isDropTarget, + menuOpen, + canModify, + showMenu, + gridCols, + onDragStart, + onDragEnd, + onDragOver, + onDragLeave, + onDrop, + onClick, + onDoubleClick, + onKeyDown, + onToggleCheck, + onOpenMenu, +}: Props) { + return ( +
  • +
    onDragStart(event, entry)} + onDragEnd={onDragEnd} + onDragOver={(event) => onDragOver(event, entry)} + onDragLeave={(event) => onDragLeave(event, entry)} + onDrop={(event) => onDrop(event, entry)} + onClick={(event) => onClick(event, entry)} + onDoubleClick={() => onDoubleClick(entry)} + onKeyDown={(event) => onKeyDown(event, entry)} + className={`grid ${gridCols} w-full cursor-pointer items-center gap-x-3 rounded-lg px-3 py-2 text-left transition ${ + isDropTarget + ? 'bg-green-600/15 ring-2 ring-green-600 ring-inset' + : checked || selected + ? 'bg-green-600/10' + : 'hover:bg-zinc-200 dark:hover:bg-zinc-800' + }`}> + {canModify && ( + // Wrapper keeps the grid cell aligned on desktop; the checkbox shows only on mobile. + + event.stopPropagation()} + onChange={() => onToggleCheck(entry)} + className='hidden h-4 w-4 max-sm:block' + /> + {/* Desktop: a check indicator (no checkbox) for selected rows. */} + {checked && } + + )} + + {entry.isFolder ? ( + + ) : ( + + )} + {entry.name} + + + {formatDate(entry.modifiedTime)} + + + {entry.isFolder ? '—' : formatBytes(entry.size)} + + {showMenu && ( +
    + +
    + )} +
    +
  • + ); +} From ccaf007074646f5c9cf8eacd5c20c4f479f7b120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 16:07:40 +0200 Subject: [PATCH 10/26] refactor(web): extract the URL location and split-pane state into hooks --- apps/web/src/common/hooks/useSplitPanes.ts | 191 ++++++++++ .../src/common/hooks/useWorkspaceLocation.ts | 209 ++++++++++ .../components/layouts/WorkspaceClient.tsx | 359 +++--------------- 3 files changed, 454 insertions(+), 305 deletions(-) create mode 100644 apps/web/src/common/hooks/useSplitPanes.ts create mode 100644 apps/web/src/common/hooks/useWorkspaceLocation.ts diff --git a/apps/web/src/common/hooks/useSplitPanes.ts b/apps/web/src/common/hooks/useSplitPanes.ts new file mode 100644 index 0000000..79f0475 --- /dev/null +++ b/apps/web/src/common/hooks/useSplitPanes.ts @@ -0,0 +1,191 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useEffect, useState } from 'react'; + +import { moveItemAction } from '@/actions/storage/storage.actions'; +import type { BrowsePane } from '@/common/hooks/useBrowsePane'; +import type { Crumb } from '@/common/types/workspace.types'; +import { useToast } from '@/components/providers/ToastProvider'; + +/** Which pane an interaction belongs to. */ +type Pane = 0 | 1; + +/** A drag in flight: what is being moved, and which pane it left. */ +interface DragMove { + ids: string[]; + sourcePane: Pane; +} + +interface Options { + /** False below the breakpoint where two panes fit. */ + canSplit: boolean; + backend: StorageBackend | null; + /** The main pane's folder and path. */ + currentFolderId: string | null; + path: Crumb[]; + paneB: BrowsePane; + reloadPanes: () => Promise; + /** Clears what the move invalidated (both selections and the preview), as the workspace sees fit. */ + onMoved: (ids: string[]) => void; +} + +/** + * The second pane: whether it is open, which pane is active, and moving items between them. + **/ +export function useSplitPanes({ + canSplit, + backend, + currentFolderId, + path, + paneB, + reloadPanes, + onMoved, +}: Options) { + const toast = useToast(); + + const [split, setSplit] = useState(false); + const [activePane, setActivePane] = useState(0); + const [dragMove, setDragMove] = useState(null); + + const toggleSplit = useCallback(() => { + setSplit((current) => { + const next = !current; + + if (next) { + // Open the second pane at the same location as the primary one. + paneB.goTo(path); + } else { + setDragMove(null); + setActivePane(0); + } + + return next; + }); + }, [paneB, path]); + + // Close the split (and any drag) if the storage disconnects entirely. + useEffect(() => { + if (backend === null) { + setSplit(false); + setDragMove(null); + } + }, [backend]); + + // Below `md` two panes have no usable width, and moving items between them is a drag & drop + // gesture touch never fires — so the split collapses back to one pane on a narrow viewport. + useEffect(() => { + if (!canSplit) { + setSplit(false); + setDragMove(null); + setActivePane(0); + } + }, [canSplit]); + + // Keyboard shortcut (Cmd/Ctrl + \) to toggle the split view. + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === '\\') { + const target = event.target as HTMLElement | null; + + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { + return; + } + // Only open the split from inside a folder, and only where it fits; closing is always allowed. + if (!split && (currentFolderId === null || !canSplit)) { + return; + } + + event.preventDefault(); + toggleSplit(); + } + }; + + window.addEventListener('keydown', handler); + + return () => window.removeEventListener('keydown', handler); + }, [split, currentFolderId, canSplit, toggleSplit]); + + /** + * Moves ids into a folder and reports the outcome in one live toast. + **/ + // A move can take a while (S3 copies whole folders), so the toast is opened before the requests. + const runMove = useCallback( + async (targetFolderId: string, ids: string[], activeBackend: StorageBackend) => { + const count = `${ids.length} item${ids.length === 1 ? '' : 's'}`; + const toastId = toast.loading(`Moving ${count}…`); + + const results = await Promise.all( + ids.map((id) => moveItemAction(activeBackend, id, targetFolderId)), + ); + const failed = results.filter((result) => !result.ok).length; + + await reloadPanes(); + onMoved(ids); + + toast.update(toastId, { + variant: failed > 0 ? 'error' : 'success', + message: + failed > 0 + ? `Failed to move ${failed} item${failed === 1 ? '' : 's'}.` + : `Moved ${count}.`, + }); + }, + [toast, reloadPanes, onMoved], + ); + + // Finder-style drop onto a folder row, within one pane. + const moveIntoFolder = useCallback( + async (targetFolderId: string, ids: string[]) => { + if (backend === null) { + return; + } + + // Can't move a folder into itself. + const moveIds = ids.filter((id) => id !== targetFolderId); + + if (moveIds.length === 0) { + return; + } + + await runMove(targetFolderId, moveIds, backend); + }, + [backend, runMove], + ); + + // Drop onto the other pane. + const moveDrop = useCallback( + async (targetPane: Pane) => { + if (!dragMove || backend === null || dragMove.sourcePane === targetPane) { + return; + } + + const { ids } = dragMove; + const sourceFolderId = dragMove.sourcePane === 0 ? currentFolderId : paneB.currentFolderId; + const targetFolderId = targetPane === 0 ? currentFolderId : paneB.currentFolderId; + setDragMove(null); + + if (targetFolderId === null) { + return; + } + if (sourceFolderId === targetFolderId) { + toast.error('Items are already in that folder.'); + return; + } + + await runMove(targetFolderId, ids, backend); + }, + [dragMove, backend, currentFolderId, paneB, runMove, toast], + ); + + return { + split, + activePane, + setActivePane, + dragMove, + setDragMove, + toggleSplit, + moveIntoFolder, + moveDrop, + }; +} diff --git a/apps/web/src/common/hooks/useWorkspaceLocation.ts b/apps/web/src/common/hooks/useWorkspaceLocation.ts new file mode 100644 index 0000000..a90fac5 --- /dev/null +++ b/apps/web/src/common/hooks/useWorkspaceLocation.ts @@ -0,0 +1,209 @@ +'use client'; + +import type { StorageBackend, StorageStatus } from '@dropto/types'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { resolvePathAction } from '@/actions/storage/storage.actions'; +import type { Crumb, SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; +import { buildWorkspaceUrl, slugify } from '@/common/utils/storage-url'; + +interface Options { + statuses: StorageStatus[]; + /** Backend from the URL (already validated connected), for first paint without a flash. */ + initialBackend?: StorageBackend | null; + /** Breadcrumb resolved server-side, so a deep link paints with real names. */ + initialPath?: Crumb[]; + /** The URL points at a folder that doesn't exist / isn't authorized (server-detected 404). */ + initialNotFound?: boolean; + /** Called whenever the location changes, so the workspace can drop the preview. */ + onLocationChange: () => void; +} + +/** + * First connected backend (or null) — used to auto-select a storage. + **/ +const pickDefaultBackend = (statuses: StorageStatus[]): StorageBackend | null => + statuses.find((status) => status.connected)?.backend ?? null; + +/** + * The browse location, derived from the URL: which backend, which folder path, and the sort. + **/ +// The URL is the source of truth. Navigation writes to window.history (no server round-trip) and +// this hook reads it back, so the back button and a shared deep link behave identically. +export function useWorkspaceLocation({ + statuses, + initialBackend, + initialPath, + initialNotFound, + onLocationChange, +}: Options) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [activeBackend, setActiveBackend] = useState(initialBackend ?? null); + const [path, setPath] = useState(initialPath ?? []); + + // id → display name, so navigating (and rebuilding a deep-linked breadcrumb) shows real names. + const nameCache = useRef>( + new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.name])), + ); + // id → Drive web-view link, so the breadcrumb / toolbar "Copy link" always points at the real folder. + const linkCache = useRef>( + new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.webViewLink])), + ); + + // Server-detected 404 (bad folder URL): shown in the file area until the operator navigates away. + const notFoundPathname = useRef(initialNotFound ? pathname : null); + const notFound = notFoundPathname.current !== null && notFoundPathname.current === pathname; + + const currentFolderId = path.length > 0 ? path[path.length - 1].id : null; + const atRoots = path.length === 0; + + // Sort lives in the URL query (?sort=&dir=) so it's shareable; the main pane is controlled by it. + const sortParam = searchParams.get('sort'); + const sortKey: SortKey = sortParam === 'modified' || sortParam === 'size' ? sortParam : 'name'; + const sortDir: SortDir = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'; + + // Derive the browse location from the URL; navigation updates it via window.history (no server round-trip). + useEffect(() => { + const segments = pathname.split('/').filter(Boolean).map(decodeURIComponent); + const paramBackend = segments[0] === 'drive' || segments[0] === 's3' ? segments[0] : null; + const connected = statuses.filter((status) => status.connected).map((status) => status.backend); + + // Server-detected 404: keep the sidebar (select the backend) but don't load the bad folder. + if (notFound) { + if (paramBackend && connected.includes(paramBackend)) { + setActiveBackend(paramBackend); + } + setPath([]); + return; + } + + // Only connected backends are browsable; redirect away from an unknown/disconnected URL. + if (paramBackend === null || !connected.includes(paramBackend)) { + const fallback = pickDefaultBackend(statuses); + if (fallback === null) { + setActiveBackend(null); + setPath([]); + onLocationChange(); + return; + } + router.replace(`/${fallback}`); + return; + } + + const backend = paramBackend; + const backendRoots = statuses.find((status) => status.backend === backend)?.roots ?? []; + const folderSegs = segments.slice(1); + const root = + folderSegs.length > 0 && backendRoots.find((r) => slugify(r.name) === folderSegs[0]); + + if (!root) { + setActiveBackend(backend); + setPath([]); + onLocationChange(); + return; + } + + // Names/links come from the cache so the breadcrumb paints instantly; any gap is filled once below. + const restIds = folderSegs.slice(1); + setActiveBackend(backend); + setPath([ + { id: root.id, name: root.name, webViewLink: null }, + ...restIds.map((id) => ({ + id, + name: nameCache.current.get(id) ?? '', + webViewLink: linkCache.current.get(id) ?? null, + })), + ]); + onLocationChange(); + + const missing = restIds.filter( + (id) => !nameCache.current.has(id) || !linkCache.current.has(id), + ); + if (missing.length === 0) { + return; + } + + let cancelled = false; + void (async () => { + const result = await resolvePathAction(backend, missing); + if (cancelled || !result.ok) { + return; + } + for (const pair of result.data ?? []) { + nameCache.current.set(pair.id, pair.name); + linkCache.current.set(pair.id, pair.webViewLink); + } + setPath((current) => + current.map((crumb) => ({ + ...crumb, + name: nameCache.current.get(crumb.id) ?? crumb.name, + webViewLink: linkCache.current.get(crumb.id) ?? crumb.webViewLink, + })), + ); + })(); + return () => { + cancelled = true; + }; + }, [pathname, statuses, router]); + const openFolder = useCallback( + (entry: ViewEntry) => { + if (activeBackend === null) { + return; + } + nameCache.current.set(entry.id, entry.name); + linkCache.current.set(entry.id, entry.webViewLink); + const url = buildWorkspaceUrl(activeBackend, [ + ...path, + { id: entry.id, name: entry.name, webViewLink: entry.webViewLink }, + ]); + window.history.pushState(null, '', url); + }, + [activeBackend, path], + ); + + const navigate = useCallback( + (index: number) => { + if (activeBackend === null) { + return; + } + const url = buildWorkspaceUrl(activeBackend, index < 0 ? [] : path.slice(0, index + 1)); + window.history.pushState(null, '', url); + }, + [activeBackend, path], + ); + + const selectStorage = useCallback((backend: StorageBackend) => { + window.history.pushState(null, '', `/${backend}`); + }, []); + + const handleToggleSort = useCallback( + (key: SortKey) => { + const nextDir: SortDir = key === sortKey ? (sortDir === 'asc' ? 'desc' : 'asc') : 'asc'; + const base = activeBackend + ? buildWorkspaceUrl(activeBackend, path) + : window.location.pathname; + // Default (name/asc) stays out of the URL to keep it clean. + const query = key === 'name' && nextDir === 'asc' ? '' : `?sort=${key}&dir=${nextDir}`; + window.history.replaceState(null, '', `${base}${query}`); + }, + [sortKey, sortDir, activeBackend, path], + ); + + return { + activeBackend, + path, + currentFolderId, + atRoots, + notFound, + sortKey, + sortDir, + openFolder, + navigate, + selectStorage, + toggleSort: handleToggleSort, + }; +} diff --git a/apps/web/src/components/layouts/WorkspaceClient.tsx b/apps/web/src/components/layouts/WorkspaceClient.tsx index a3451b4..dde6b6e 100644 --- a/apps/web/src/components/layouts/WorkspaceClient.tsx +++ b/apps/web/src/components/layouts/WorkspaceClient.tsx @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend, StorageStatus } from '@dropto/types'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { @@ -10,11 +10,7 @@ import { revokeDriveOwnerAction, saveFoldersAction, } from '@/actions/auth/auth.actions'; -import { - moveItemAction, - resolvePathAction, - statusesAction, -} from '@/actions/storage/storage.actions'; +import { statusesAction } from '@/actions/storage/storage.actions'; import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; import { useBrowsePane } from '@/common/hooks/useBrowsePane'; import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; @@ -22,12 +18,13 @@ import { useEntryListing } from '@/common/hooks/useEntryListing'; import { useEntryOperations } from '@/common/hooks/useEntryOperations'; import { useEntrySelection } from '@/common/hooks/useEntrySelection'; import { useMediaQuery } from '@/common/hooks/useMediaQuery'; +import { useSplitPanes } from '@/common/hooks/useSplitPanes'; import { useUploadQueue } from '@/common/hooks/useUploadQueue'; +import { useWorkspaceLocation } from '@/common/hooks/useWorkspaceLocation'; import { fileDownloadUrl, folderDownloadUrl } from '@/common/services/api/storage.client'; import { type PickedFolder, usePicker } from '@/common/services/picker/usePicker'; -import type { Crumb, SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; +import type { Crumb, ViewEntry } from '@/common/types/workspace.types'; import { extractApiErrorMessage } from '@/common/utils/error.functions'; -import { buildWorkspaceUrl, slugify } from '@/common/utils/storage-url'; import { useToast } from '@/components/providers/ToastProvider'; import { UploadProvider, useUploadActions } from '@/components/providers/UploadProvider'; import { STORAGE_ICON } from '@/configs/storage.config'; @@ -57,12 +54,6 @@ interface Props { initialNotFound?: boolean; } -/** - * First connected backend (or null) — used to auto-select a storage. - **/ -const pickDefaultBackend = (statuses: StorageStatus[]): StorageBackend | null => - statuses.find((status) => status.connected)?.backend ?? null; - /** * Main workspace: sidebar + file browser + preview, with uploads and account management. **/ @@ -74,54 +65,49 @@ function WorkspaceInner({ initialPath, initialNotFound, }: Props) { - const router = useRouter(); const searchParams = useSearchParams(); - const pathname = usePathname(); const toast = useToast(); const { openPicker } = usePicker(); const { setDownloads } = useUploadActions(); const [statuses, setStatuses] = useState(initialStatuses); const [loadingStatus, setLoadingStatus] = useState(false); - const [activeBackend, setActiveBackend] = useState(initialBackend ?? null); - const [path, setPath] = useState(initialPath ?? []); const [search, setSearch] = useState(''); const [selected, setSelected] = useState(null); const canSplit = useMediaQuery(DESKTOP_QUERY); - const [split, setSplit] = useState(false); - const [activePane, setActivePane] = useState<0 | 1>(0); - const [dragMove, setDragMove] = useState<{ ids: string[]; sourcePane: 0 | 1 } | null>(null); const [saving, setSaving] = useState(false); const [loggingOut, setLoggingOut] = useState(false); const handledParams = useRef(false); - // id → display name, so navigating (and rebuilding a deep-linked breadcrumb) shows real names. - const nameCache = useRef>( - new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.name])), - ); - // id → Drive web-view link, so the breadcrumb / toolbar "Copy link" always points at the real folder. - const linkCache = useRef>( - new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.webViewLink])), - ); + + // The browse location (backend, path, sort) is derived from the URL. + const { + activeBackend, + path, + currentFolderId, + atRoots, + notFound, + sortKey, + sortDir, + openFolder, + navigate, + selectStorage, + toggleSort: handleToggleSort, + } = useWorkspaceLocation({ + statuses, + initialBackend, + initialPath, + initialNotFound, + onLocationChange: () => setSelected(null), + }); const driveStatus = statuses.find((status) => status.backend === 'drive') ?? null; const activeStatus = activeBackend ? (statuses.find((status) => status.backend === activeBackend) ?? null) : null; - const currentFolderId = path.length > 0 ? path[path.length - 1].id : null; - const atRoots = path.length === 0; const debouncedSearch = useDebouncedValue(search.trim(), 250); - // Server-detected 404 (bad folder URL): shown in the file area until the operator navigates away. - const notFoundPathname = useRef(initialNotFound ? pathname : null); - const notFound = notFoundPathname.current !== null && notFoundPathname.current === pathname; - - // Sort lives in the URL query (?sort=&dir=) so it's shareable; the main pane is controlled by it. - const sortParam = searchParams.get('sort'); - const sortKey: SortKey = sortParam === 'modified' || sortParam === 'size' ? sortParam : 'name'; - const sortDir: SortDir = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'; - const roots = useMemo( () => (activeStatus?.roots ?? []).map((root) => ({ @@ -196,90 +182,6 @@ function WorkspaceInner({ // The second (split) pane: an independent browser over the same backend. const paneB = useBrowsePane(activeBackend, roots, onPaneError); - // Derive the browse location from the URL; navigation updates it via window.history (no server round-trip). - useEffect(() => { - const segments = pathname.split('/').filter(Boolean).map(decodeURIComponent); - const paramBackend = segments[0] === 'drive' || segments[0] === 's3' ? segments[0] : null; - const connected = statuses.filter((status) => status.connected).map((status) => status.backend); - - // Server-detected 404: keep the sidebar (select the backend) but don't load the bad folder. - if (notFound) { - if (paramBackend && connected.includes(paramBackend)) { - setActiveBackend(paramBackend); - } - setPath([]); - return; - } - - // Only connected backends are browsable; redirect away from an unknown/disconnected URL. - if (paramBackend === null || !connected.includes(paramBackend)) { - const fallback = pickDefaultBackend(statuses); - if (fallback === null) { - setActiveBackend(null); - setPath([]); - setSelected(null); - return; - } - router.replace(`/${fallback}`); - return; - } - - const backend = paramBackend; - const backendRoots = statuses.find((status) => status.backend === backend)?.roots ?? []; - const folderSegs = segments.slice(1); - const root = - folderSegs.length > 0 && backendRoots.find((r) => slugify(r.name) === folderSegs[0]); - - if (!root) { - setActiveBackend(backend); - setPath([]); - setSelected(null); - return; - } - - // Names/links come from the cache so the breadcrumb paints instantly; any gap is filled once below. - const restIds = folderSegs.slice(1); - setActiveBackend(backend); - setPath([ - { id: root.id, name: root.name, webViewLink: null }, - ...restIds.map((id) => ({ - id, - name: nameCache.current.get(id) ?? '', - webViewLink: linkCache.current.get(id) ?? null, - })), - ]); - setSelected(null); - - const missing = restIds.filter( - (id) => !nameCache.current.has(id) || !linkCache.current.has(id), - ); - if (missing.length === 0) { - return; - } - - let cancelled = false; - void (async () => { - const result = await resolvePathAction(backend, missing); - if (cancelled || !result.ok) { - return; - } - for (const pair of result.data ?? []) { - nameCache.current.set(pair.id, pair.name); - linkCache.current.set(pair.id, pair.webViewLink); - } - setPath((current) => - current.map((crumb) => ({ - ...crumb, - name: nameCache.current.get(crumb.id) ?? crumb.name, - webViewLink: linkCache.current.get(crumb.id) ?? crumb.webViewLink, - })), - ); - })(); - return () => { - cancelled = true; - }; - }, [pathname, statuses, router]); - useEffect(() => { if (handledParams.current) { return; @@ -310,41 +212,14 @@ function WorkspaceInner({ setSearch(''); }, [currentFolderId, activeBackend]); + // Read through a ref: `split` is owned by useSplitPanes, which in turn needs reloadPanes — the + // ref breaks that cycle without making the reload depend on the split's identity. + const splitRef = useRef(false); + // Reload both panes after a mutation so the source and destination both refresh. const reloadPanes = useCallback(async () => { - await Promise.all([loadEntries(), split ? paneB.reload() : Promise.resolve()]); - }, [loadEntries, split, paneB]); - - const openFolder = useCallback( - (entry: ViewEntry) => { - if (activeBackend === null) { - return; - } - nameCache.current.set(entry.id, entry.name); - linkCache.current.set(entry.id, entry.webViewLink); - const url = buildWorkspaceUrl(activeBackend, [ - ...path, - { id: entry.id, name: entry.name, webViewLink: entry.webViewLink }, - ]); - window.history.pushState(null, '', url); - }, - [activeBackend, path], - ); - - const navigate = useCallback( - (index: number) => { - if (activeBackend === null) { - return; - } - const url = buildWorkspaceUrl(activeBackend, index < 0 ? [] : path.slice(0, index + 1)); - window.history.pushState(null, '', url); - }, - [activeBackend, path], - ); - - const selectStorage = useCallback((backend: StorageBackend) => { - window.history.pushState(null, '', `/${backend}`); - }, []); + await Promise.all([loadEntries(), splitRef.current ? paneB.reload() : Promise.resolve()]); + }, [loadEntries, paneB]); // The Drive link of the folder currently open in the main pane (null at roots / for S3). const currentFolderLink = path.length > 0 ? path[path.length - 1].webViewLink : null; @@ -395,158 +270,32 @@ function WorkspaceInner({ [openDriveLink, paneBFolderLink], ); - const handleToggleSort = useCallback( - (key: SortKey) => { - const nextDir: SortDir = key === sortKey ? (sortDir === 'asc' ? 'desc' : 'asc') : 'asc'; - const base = activeBackend - ? buildWorkspaceUrl(activeBackend, path) - : window.location.pathname; - // Default (name/asc) stays out of the URL to keep it clean. - const query = key === 'name' && nextDir === 'asc' ? '' : `?sort=${key}&dir=${nextDir}`; - window.history.replaceState(null, '', `${base}${query}`); - }, - [sortKey, sortDir, activeBackend, path], - ); - - // --- Split view + drag-to-move ------------------------------------------------ - - const toggleSplit = useCallback(() => { - setSplit((current) => { - const next = !current; - if (next) { - // Open the second pane at the same location as the primary one. - paneB.goTo(path); - } else { - setDragMove(null); - setActivePane(0); - } - return next; - }); - }, [paneB, path]); - - // Close the split (and any drag) if the storage disconnects entirely. - useEffect(() => { - if (activeBackend === null) { - setSplit(false); - setDragMove(null); - } - }, [activeBackend]); - - // Below `md` two panes have no usable width, and moving items between them is a drag & drop - // gesture touch never fires — so the split collapses back to one pane on a narrow viewport. - useEffect(() => { - if (!canSplit) { - setSplit(false); - setDragMove(null); - setActivePane(0); - } - }, [canSplit]); - - // Keyboard shortcut (Cmd/Ctrl + \) to toggle the split view. - useEffect(() => { - const handler = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === '\\') { - const target = event.target as HTMLElement | null; - if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { - return; - } - // Only open the split from inside a folder, and only where it fits; closing is always allowed. - if (!split && (currentFolderId === null || !canSplit)) { - return; - } - event.preventDefault(); - toggleSplit(); - } - }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [split, currentFolderId, canSplit, toggleSplit]); - - // Move the given ids into a target folder (Finder-style drop onto a folder row, same pane). - const handleMoveIntoFolder = useCallback( - async (targetFolderId: string, ids: string[]) => { - if (activeBackend === null) { - return; - } - // Can't move a folder into itself. - const moveIds = ids.filter((id) => id !== targetFolderId); - if (moveIds.length === 0) { - return; - } - const backend = activeBackend; - - const count = `${moveIds.length} item${moveIds.length === 1 ? '' : 's'}`; - const toastId = toast.loading(`Moving ${count}…`); - - const results = await Promise.all( - moveIds.map((id) => moveItemAction(backend, id, targetFolderId)), - ); - const failed = results.filter((result) => !result.ok).length; - - await reloadPanes(); + // The second pane and moving items between panes. + const { + split, + activePane, + setActivePane, + dragMove, + setDragMove, + toggleSplit, + moveIntoFolder: handleMoveIntoFolder, + moveDrop: handleMoveDrop, + } = useSplitPanes({ + canSplit, + backend: activeBackend, + currentFolderId, + path, + paneB, + reloadPanes, + // A move empties both selections and drops the preview if it was one of the moved items. + onMoved: (ids) => { clearSelection(); paneB.clearSelection(); - if (selected && moveIds.includes(selected.id)) { - setSelected(null); - } - - toast.update(toastId, { - variant: failed > 0 ? 'error' : 'success', - message: - failed > 0 - ? `Failed to move ${failed} item${failed === 1 ? '' : 's'}.` - : `Moved ${count}.`, - }); + setSelected((current) => (current && ids.includes(current.id) ? null : current)); }, - [activeBackend, toast, reloadPanes, paneB, selected], - ); - - const handleMoveDrop = useCallback( - async (targetPane: 0 | 1) => { - if (!dragMove || activeBackend === null || dragMove.sourcePane === targetPane) { - return; - } - const backend = activeBackend; - const { ids } = dragMove; - const sourceFolderId = dragMove.sourcePane === 0 ? currentFolderId : paneB.currentFolderId; - const targetFolderId = targetPane === 0 ? currentFolderId : paneB.currentFolderId; - setDragMove(null); - - if (targetFolderId === null) { - return; - } - if (sourceFolderId === targetFolderId) { - toast.error('Items are already in that folder.'); - return; - } - - const count = `${ids.length} item${ids.length === 1 ? '' : 's'}`; - // A move can take a while (S3 copies whole folders), so show a live toast. - const toastId = toast.loading(`Moving ${count}…`); - - const results = await Promise.all( - ids.map((id) => moveItemAction(backend, id, targetFolderId)), - ); - const failed = results.filter((result) => !result.ok).length; - - await reloadPanes(); - clearSelection(); - paneB.clearSelection(); - if (selected && ids.includes(selected.id)) { - setSelected(null); - } + }); - if (failed > 0) { - toast.update(toastId, { - variant: 'error', - message: `Failed to move ${failed} item${failed === 1 ? '' : 's'}.`, - }); - } else { - toast.update(toastId, { variant: 'success', message: `Moved ${count}.` }); - } - }, - [dragMove, activeBackend, currentFolderId, paneB, reloadPanes, selected, toast], - ); + splitRef.current = split; // Uploads (duplicate prompt, folder creation, progress, rollback) live in their own hook. const paneAccess = useMemo( From 83ce1caa8e66f4380351c3bd673bf46567f51a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 16:26:00 +0200 Subject: [PATCH 11/26] perf(web): memoize the file row and stabilise what feeds it --- apps/web/src/common/hooks/useEntryListing.ts | 23 +- .../web/src/common/hooks/useEntrySelection.ts | 7 +- .../src/components/layouts/FileBrowser.tsx | 210 ++++++++++-------- apps/web/src/components/layouts/FileRow.tsx | 9 +- .../components/layouts/WorkspaceClient.tsx | 53 +++-- 5 files changed, 182 insertions(+), 120 deletions(-) diff --git a/apps/web/src/common/hooks/useEntryListing.ts b/apps/web/src/common/hooks/useEntryListing.ts index a3ca381..5580a9b 100644 --- a/apps/web/src/common/hooks/useEntryListing.ts +++ b/apps/web/src/common/hooks/useEntryListing.ts @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend } from '@dropto/types'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { listContentsAction } from '@/actions/storage/storage.actions'; import type { SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; @@ -132,13 +132,16 @@ export function useEntryListing({ setLoadingMore(false); }, [nextPageToken, backend, currentFolderId, loadingMore, search, sortKey, sortDir, onError]); - return { - entries, - setEntries, - loading, - hasMore: nextPageToken !== null, - loadingMore, - loadMore, - reload, - }; + return useMemo( + () => ({ + entries, + setEntries, + loading, + hasMore: nextPageToken !== null, + loadingMore, + loadMore, + reload, + }), + [entries, loading, nextPageToken, loadingMore, loadMore, reload], + ); } diff --git a/apps/web/src/common/hooks/useEntrySelection.ts b/apps/web/src/common/hooks/useEntrySelection.ts index e321cf7..b1f8ce3 100644 --- a/apps/web/src/common/hooks/useEntrySelection.ts +++ b/apps/web/src/common/hooks/useEntrySelection.ts @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend } from '@dropto/types'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; /** Multi-selection over the entries of one pane. */ export interface EntrySelection { @@ -62,5 +62,8 @@ export function useEntrySelection( }); }, []); - return { selectedIds, toggleSelect, selectAll, clearSelection, setSelection, pruneSelection }; + return useMemo( + () => ({ selectedIds, toggleSelect, selectAll, clearSelection, setSelection, pruneSelection }), + [selectedIds, toggleSelect, selectAll, clearSelection, setSelection, pruneSelection], + ); } diff --git a/apps/web/src/components/layouts/FileBrowser.tsx b/apps/web/src/components/layouts/FileBrowser.tsx index 22a9e0a..1cef4be 100644 --- a/apps/web/src/components/layouts/FileBrowser.tsx +++ b/apps/web/src/components/layouts/FileBrowser.tsx @@ -186,6 +186,13 @@ export default function FileBrowser({ // Folder row currently hovered as a move drop target (Finder-style same-pane move). const [dropFolderId, setDropFolderId] = useState(null); + // The row handlers below must keep a stable identity for `FileRow`'s memo to hold, so the values + // that change on every selection or drag tick are read through refs instead of closed over. + const selectedIdsRef = useRef(selectedIds); + selectedIdsRef.current = selectedIds; + const dropFolderIdRef = useRef(dropFolderId); + dropFolderIdRef.current = dropFolderId; + // Close the search box and drop its filter. const closeSearch = useCallback(() => { setShowSearch(false); @@ -253,6 +260,9 @@ export default function FileBrowser({ }); }, [entries, sortKey, sortDir, query]); + const sortedRef = useRef(sorted); + sortedRef.current = sorted; + // Identity of the current view — changes only on folder / sort / search, i.e. a "fresh load". const viewKey = `${path.map((crumb) => crumb.id).join('/')}|${sortKey}|${sortDir}|${query}`; @@ -306,13 +316,13 @@ export default function FileBrowser({ ? 'grid-cols-[minmax(0,1fr)_4.5rem_2rem] sm:grid-cols-[minmax(0,1fr)_8rem_6rem_2rem]' : 'grid-cols-[minmax(0,1fr)_4.5rem] sm:grid-cols-[minmax(0,1fr)_8rem_6rem]'; - const openMenu = (event: MouseEvent, entry: ViewEntry) => { + const openMenu = useCallback((event: MouseEvent, entry: ViewEntry) => { event.stopPropagation(); const rect = event.currentTarget.getBoundingClientRect(); // Opening a row menu closes the toolbar menu (they must never overlap). setToolbarMenu(null); setMenu((current) => (current?.entry.id === entry.id ? null : { entry, rect })); - }; + }, []); const openToolbarMenu = (event: MouseEvent) => { event.stopPropagation(); @@ -378,102 +388,128 @@ export default function FileBrowser({ }); }; - const handleRowDragStart = (event: DragEvent, entry: ViewEntry) => { - if (!canModify) { - return; - } - // Dragging a selected row moves the whole selection; otherwise just that row. - const ids = selectedIds.has(entry.id) ? [...selectedIds] : [entry.id]; - event.dataTransfer.setData(MOVE_MIME, ids.join(',')); - event.dataTransfer.effectAllowed = 'move'; - onMoveDragStart?.(ids); - }; + const handleRowDragStart = useCallback( + (event: DragEvent, entry: ViewEntry) => { + if (!canModify) { + return; + } + // Dragging a selected row moves the whole selection; otherwise just that row. + const current = selectedIdsRef.current; + const ids = current.has(entry.id) ? [...current] : [entry.id]; + event.dataTransfer.setData(MOVE_MIME, ids.join(',')); + event.dataTransfer.effectAllowed = 'move'; + onMoveDragStart?.(ids); + }, + [canModify, onMoveDragStart], + ); - const handleRowDragOver = (event: DragEvent, entry: ViewEntry) => { - // A folder row accepts a move drop (unless it's part of the dragged selection). - if ( - canModify && - onMoveIntoFolder && - entry.isFolder && - !selectedIds.has(entry.id) && - event.dataTransfer.types.includes(MOVE_MIME) - ) { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; - if (dropFolderId !== entry.id) setDropFolderId(entry.id); - } - }; + const handleRowDragOver = useCallback( + (event: DragEvent, entry: ViewEntry) => { + // A folder row accepts a move drop (unless it's part of the dragged selection). + if ( + canModify && + onMoveIntoFolder && + entry.isFolder && + !selectedIdsRef.current.has(entry.id) && + event.dataTransfer.types.includes(MOVE_MIME) + ) { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; + if (dropFolderIdRef.current !== entry.id) setDropFolderId(entry.id); + } + }, + [canModify, onMoveIntoFolder], + ); - const handleRowDragLeave = (event: DragEvent, entry: ViewEntry) => { + const handleRowDragLeave = useCallback((event: DragEvent, entry: ViewEntry) => { if (event.currentTarget.contains(event.relatedTarget as Node | null)) { return; } setDropFolderId((current) => (current === entry.id ? null : current)); - }; + }, []); - const handleRowDrop = (event: DragEvent, entry: ViewEntry) => { - if ( - !canModify || - !onMoveIntoFolder || - !entry.isFolder || - !event.dataTransfer.types.includes(MOVE_MIME) - ) { - return; - } - event.preventDefault(); - event.stopPropagation(); - setDropFolderId(null); - const ids = event.dataTransfer.getData(MOVE_MIME).split(',').filter(Boolean); - if (ids.length > 0 && !ids.includes(entry.id)) { - onMoveIntoFolder(entry.id, ids); - } - }; + const handleRowDrop = useCallback( + (event: DragEvent, entry: ViewEntry) => { + if ( + !canModify || + !onMoveIntoFolder || + !entry.isFolder || + !event.dataTransfer.types.includes(MOVE_MIME) + ) { + return; + } + event.preventDefault(); + event.stopPropagation(); + setDropFolderId(null); + const ids = event.dataTransfer.getData(MOVE_MIME).split(',').filter(Boolean); + if (ids.length > 0 && !ids.includes(entry.id)) { + onMoveIntoFolder(entry.id, ids); + } + }, + [canModify, onMoveIntoFolder], + ); - const handleRowClick = (event: MouseEvent, entry: ViewEntry) => { - // Don't let the click reach the finder's deselect handler. - event.stopPropagation(); - const ids = sorted.map((row) => row.id); - // Shift-click: select the range from the anchor to this row. - if (event.shiftKey && onSetSelectedIds && selectionAnchor.current) { - const from = ids.indexOf(selectionAnchor.current); - const to = ids.indexOf(entry.id); - if (from !== -1 && to !== -1) { - const [lo, hi] = from < to ? [from, to] : [to, from]; - onSetSelectedIds(ids.slice(lo, hi + 1)); + const handleRowClick = useCallback( + (event: MouseEvent, entry: ViewEntry) => { + // Don't let the click reach the finder's deselect handler. + event.stopPropagation(); + const ids = sortedRef.current.map((row) => row.id); + // Shift-click: select the range from the anchor to this row. + if (event.shiftKey && onSetSelectedIds && selectionAnchor.current) { + const from = ids.indexOf(selectionAnchor.current); + const to = ids.indexOf(entry.id); + if (from !== -1 && to !== -1) { + const [lo, hi] = from < to ? [from, to] : [to, from]; + onSetSelectedIds(ids.slice(lo, hi + 1)); + return; + } + } + // Cmd/Ctrl-click: toggle this row in the multi-selection. + if ((event.metaKey || event.ctrlKey) && canModify) { + onToggleSelect(entry.id); + selectionAnchor.current = entry.id; return; } - } - // Cmd/Ctrl-click: toggle this row in the multi-selection. - if ((event.metaKey || event.ctrlKey) && canModify) { - onToggleSelect(entry.id); + // Plain click: single select (preview) and drop any multi-selection. + onSelect(entry); + if (canModify) { + onClearSelection(); + } selectionAnchor.current = entry.id; - return; - } - // Plain click: single select (preview) and drop any multi-selection. - onSelect(entry); - if (canModify) { - onClearSelection(); - } - selectionAnchor.current = entry.id; - }; - - const handleRowOpen = (entry: ViewEntry) => { - if (entry.isFolder) onOpenFolder(entry); - else if (entry.webViewLink) window.open(entry.webViewLink, '_blank'); - }; + }, + [canModify, onSetSelectedIds, onToggleSelect, onSelect, onClearSelection], + ); - const handleRowKeyDown = (event: ReactKeyboardEvent, entry: ViewEntry) => { - // Enter opens a folder / selects a file; Space selects. - if (event.key === 'Enter') { - event.preventDefault(); + const handleRowOpen = useCallback( + (entry: ViewEntry) => { if (entry.isFolder) onOpenFolder(entry); - else onSelect(entry); - } else if (event.key === ' ') { - event.preventDefault(); - onSelect(entry); - } - }; + else if (entry.webViewLink) window.open(entry.webViewLink, '_blank'); + }, + [onOpenFolder], + ); + + const handleRowKeyDown = useCallback( + (event: ReactKeyboardEvent, entry: ViewEntry) => { + // Enter opens a folder / selects a file; Space selects. + if (event.key === 'Enter') { + event.preventDefault(); + if (entry.isFolder) onOpenFolder(entry); + else onSelect(entry); + } else if (event.key === ' ') { + event.preventDefault(); + onSelect(entry); + } + }, + [onOpenFolder, onSelect], + ); + + const handleRowDragEnd = useCallback(() => onMoveDragEnd?.(), [onMoveDragEnd]); + + const handleRowToggleCheck = useCallback( + (entry: ViewEntry) => onToggleSelect(entry.id), + [onToggleSelect], + ); const handleInput = (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []); @@ -699,14 +735,14 @@ export default function FileBrowser({ showMenu={canModify || (rootMenu && entry.isFolder)} gridCols={gridCols} onDragStart={handleRowDragStart} - onDragEnd={() => onMoveDragEnd?.()} + onDragEnd={handleRowDragEnd} onDragOver={handleRowDragOver} onDragLeave={handleRowDragLeave} onDrop={handleRowDrop} onClick={handleRowClick} onDoubleClick={handleRowOpen} onKeyDown={handleRowKeyDown} - onToggleCheck={(row) => onToggleSelect(row.id)} + onToggleCheck={handleRowToggleCheck} onOpenMenu={openMenu} /> ))} diff --git a/apps/web/src/components/layouts/FileRow.tsx b/apps/web/src/components/layouts/FileRow.tsx index e09bea1..d6fbf22 100644 --- a/apps/web/src/components/layouts/FileRow.tsx +++ b/apps/web/src/components/layouts/FileRow.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { DragEvent, KeyboardEvent, MouseEvent } from 'react'; +import { type DragEvent, type KeyboardEvent, memo, type MouseEvent } from 'react'; import type { ViewEntry } from '@/common/types/workspace.types'; import { fileIcon } from '@/common/utils/drop-items.functions'; @@ -38,8 +38,9 @@ interface Props { * One row of the file list: icon, name, modified date, size and the action menu. **/ // Every interaction is decided by the browser above — this only reports events and paints state, -// which keeps the list markup readable on its own. -export default function FileRow({ +// which keeps the list markup readable on its own. Memoized: every prop is either a scalar or a +// callback the browser keeps stable, so selecting one row re-renders that row, not the whole list. +function FileRow({ entry, selected, checked, @@ -128,3 +129,5 @@ export default function FileRow({ ); } + +export default memo(FileRow); diff --git a/apps/web/src/components/layouts/WorkspaceClient.tsx b/apps/web/src/components/layouts/WorkspaceClient.tsx index dde6b6e..5621d20 100644 --- a/apps/web/src/components/layouts/WorkspaceClient.tsx +++ b/apps/web/src/components/layouts/WorkspaceClient.tsx @@ -182,6 +182,16 @@ function WorkspaceInner({ // The second (split) pane: an independent browser over the same backend. const paneB = useBrowsePane(activeBackend, roots, onPaneError); + // A move empties both selections and drops the preview if it was one of the moved items. + const handleMoved = useCallback( + (ids: string[]) => { + clearSelection(); + paneB.clearSelection(); + setSelected((current) => (current && ids.includes(current.id) ? null : current)); + }, + [clearSelection, paneB], + ); + useEffect(() => { if (handledParams.current) { return; @@ -287,14 +297,27 @@ function WorkspaceInner({ path, paneB, reloadPanes, - // A move empties both selections and drops the preview if it was one of the moved items. - onMoved: (ids) => { - clearSelection(); - paneB.clearSelection(); - setSelected((current) => (current && ids.includes(current.id) ? null : current)); - }, + onMoved: handleMoved, }); + // Stable identities: FileRow is memoized, and its handlers close over these. Inline arrows here + // would change on every render and make the memo a no-op. + const selectInPane = useMemo( + () => + [0, 1].map((pane) => (entry: ViewEntry) => { + setSelected(entry); + setActivePane(pane as 0 | 1); + }), + [], + ); + + const startMoveDrag = useMemo( + () => [0, 1].map((pane) => (ids: string[]) => setDragMove({ ids, sourcePane: pane as 0 | 1 })), + [], + ); + + const endMoveDrag = useCallback(() => setDragMove(null), []); + splitRef.current = split; // Uploads (duplicate prompt, folder creation, progress, rollback) live in their own hook. @@ -461,15 +484,12 @@ function WorkspaceInner({ split={split} onToggleSplit={currentFolderId !== null || split ? toggleSplit : undefined} acceptMove={split && dragMove?.sourcePane === 1 && currentFolderId !== null} - onMoveDragStart={(ids) => setDragMove({ ids, sourcePane: 0 })} - onMoveDragEnd={() => setDragMove(null)} + onMoveDragStart={startMoveDrag[0]} + onMoveDragEnd={endMoveDrag} onMoveDrop={() => void handleMoveDrop(0)} onNavigate={navigate} onOpenFolder={openFolder} - onSelect={(entry) => { - setSelected(entry); - setActivePane(0); - }} + onSelect={selectInPane[0]} onDeselect={() => { setSelected(null); clearSelection(); @@ -527,15 +547,12 @@ function WorkspaceInner({ split={split} onToggleSplit={toggleSplit} acceptMove={split && dragMove?.sourcePane === 0 && paneB.currentFolderId !== null} - onMoveDragStart={(ids) => setDragMove({ ids, sourcePane: 1 })} - onMoveDragEnd={() => setDragMove(null)} + onMoveDragStart={startMoveDrag[1]} + onMoveDragEnd={endMoveDrag} onMoveDrop={() => void handleMoveDrop(1)} onNavigate={paneB.navigate} onOpenFolder={paneB.openFolder} - onSelect={(entry) => { - setSelected(entry); - setActivePane(1); - }} + onSelect={selectInPane[1]} onDeselect={() => { setSelected(null); paneB.clearSelection(); From 72602bd70162df3c63fd9dc21c48c792e5821132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ka=C5=A1par?= Date: Mon, 31 Aug 2026 16:26:11 +0200 Subject: [PATCH 12/26] style(web): apply Prettier to the files it could not reach before --- .../src/common/services/api/storage.client.ts | 38 +++++++++++-------- .../web/src/components/layouts/Breadcrumb.tsx | 11 +++--- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/apps/web/src/common/services/api/storage.client.ts b/apps/web/src/common/services/api/storage.client.ts index f6509b7..611f26e 100644 --- a/apps/web/src/common/services/api/storage.client.ts +++ b/apps/web/src/common/services/api/storage.client.ts @@ -141,23 +141,29 @@ const uploadToDriveDirect = async ( try { // PUT the remaining bytes to the Google session URL (the URL is the capability — no auth header/cookies); progress offset by confirmed bytes. // eslint-disable-next-line no-await-in-loop -- deliberate: one (resumable) attempt per loop turn - const res = await axios.put<{ id?: string }>(uploadUrl, offset > 0 ? file.slice(offset) : file, { - signal: attempt.signal, - withCredentials: false, - headers: { - 'Content-Type': mimeType, - ...(offset > 0 ? { 'Content-Range': `bytes ${offset}-${size - 1}/${size}` } : {}), + const res = await axios.put<{ id?: string }>( + uploadUrl, + offset > 0 ? file.slice(offset) : file, + { + signal: attempt.signal, + withCredentials: false, + headers: { + 'Content-Type': mimeType, + ...(offset > 0 ? { 'Content-Range': `bytes ${offset}-${size - 1}/${size}` } : {}), + }, + validateStatus: (statusCode) => + (statusCode >= 200 && statusCode < 300) || statusCode === 308, + onUploadProgress: (event) => { + if (!onProgress) return; + const loaded = offset + event.loaded; + onProgress({ + percent: Math.min(100, Math.round((loaded / size) * 100)), + rate: + typeof event.rate === 'number' && Number.isFinite(event.rate) ? event.rate : null, + }); + }, }, - validateStatus: (statusCode) => (statusCode >= 200 && statusCode < 300) || statusCode === 308, - onUploadProgress: (event) => { - if (!onProgress) return; - const loaded = offset + event.loaded; - onProgress({ - percent: Math.min(100, Math.round((loaded / size) * 100)), - rate: typeof event.rate === 'number' && Number.isFinite(event.rate) ? event.rate : null, - }); - }, - }); + ); if (res.status === 308) { recheck = true; // Google wants more — re-query the confirmed offset and continue continue; diff --git a/apps/web/src/components/layouts/Breadcrumb.tsx b/apps/web/src/components/layouts/Breadcrumb.tsx index 80f4dde..c26a53f 100644 --- a/apps/web/src/components/layouts/Breadcrumb.tsx +++ b/apps/web/src/components/layouts/Breadcrumb.tsx @@ -103,10 +103,7 @@ export default function Breadcrumb({ const renderCrumb = (crumb: Crumb, index: number) => ( - +