From 9f86fe80d7098be733d806a1c113433234fbffee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Sun, 22 Feb 2026 20:37:30 +0100 Subject: [PATCH] extract auth to lib --- .gitignore | 50 ++ .npmrc | 2 + package.json | 38 ++ src/api/createApiClient.ts | 134 +++++ src/api/query.ts | 29 + src/auth/createAuthContext.tsx | 90 ++++ src/auth/jwt.ts | 35 ++ src/contexts/LeftMenuContext.tsx | 217 ++++++++ src/contexts/RightSidebarContext.tsx | 139 +++++ src/errors/createErrorResolver.ts | 132 +++++ src/hooks/useCooldownTimer.ts | 28 + src/hooks/useEditableForm.ts | 68 +++ src/hooks/usePaginatedResource.ts | 102 ++++ src/hooks/useSorting.ts | 78 +++ src/hooks/useSubmitState.ts | 31 ++ src/hooks/useValidatedFields.ts | 166 ++++++ src/index.ts | 29 + src/panels/useSidePanelMachine.ts | 174 ++++++ src/utils/formatting.ts | 64 +++ src/utils/verifiedEmail.ts | 20 + tsconfig.build.json | 12 + tsconfig.json | 17 + vite.config.ts | 18 + yarn.lock | 769 +++++++++++++++++++++++++++ 24 files changed, 2442 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 package.json create mode 100644 src/api/createApiClient.ts create mode 100644 src/api/query.ts create mode 100644 src/auth/createAuthContext.tsx create mode 100644 src/auth/jwt.ts create mode 100644 src/contexts/LeftMenuContext.tsx create mode 100644 src/contexts/RightSidebarContext.tsx create mode 100644 src/errors/createErrorResolver.ts create mode 100644 src/hooks/useCooldownTimer.ts create mode 100644 src/hooks/useEditableForm.ts create mode 100644 src/hooks/usePaginatedResource.ts create mode 100644 src/hooks/useSorting.ts create mode 100644 src/hooks/useSubmitState.ts create mode 100644 src/hooks/useValidatedFields.ts create mode 100644 src/index.ts create mode 100644 src/panels/useSidePanelMachine.ts create mode 100644 src/utils/formatting.ts create mode 100644 src/utils/verifiedEmail.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3259942 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules/ + +# Builds +dist/ +build/ +coverage/ + +# Vite / tooling caches +.vite/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +bun-debug.log* + +# Env files (keep .env.example committed) +.env +.env.* +!.env.example + +# TypeScript incremental build info +*.tsbuildinfo + +# OS / editor cruft +.DS_Store +Thumbs.db + +# JetBrains (either ignore all, or see "optional" note below) +.idea/ +*.iml + +# CMake +cmake-build-*/ + +# IntelliJ +out/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based HTTP Client +http-client.private.env.json \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..a06c222 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +registry=https://nexus.beatrice.wtf/repository/npm-group/ +@panic:registry=https://nexus.beatrice.wtf/repository/npm-hosted/ diff --git a/package.json b/package.json new file mode 100644 index 0000000..b74ce54 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@panic/web-core", + "version": "0.1.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "build": "yarn clean && vite build && tsc -p tsconfig.build.json", + "prepublishOnly": "yarn build", + "publish:nexus": "npm publish --registry ${NEXUS_NPM_REGISTRY:-https://nexus.beatrice.wtf/repository/npm-hosted/}" + }, + "publishConfig": { + "registry": "https://nexus.beatrice.wtf/repository/npm-hosted/", + "access": "restricted" + }, + "peerDependencies": { + "react": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@vitejs/plugin-react": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.6.2", + "vite": "^7.0.0" + } +} diff --git a/src/api/createApiClient.ts b/src/api/createApiClient.ts new file mode 100644 index 0000000..8dd9c28 --- /dev/null +++ b/src/api/createApiClient.ts @@ -0,0 +1,134 @@ +export type RequestOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + token?: string | null; + body?: unknown; +}; + +export type ResolveErrorInput = { + code?: string; + status?: number; + fallbackMessage?: string; +}; + +export type CreateApiClientConfig = { + baseUrl: string; + resolveError?: (input: ResolveErrorInput) => string; + inferErrorCodeFromStatus?: (status?: number | null) => string | undefined; + fetchImpl?: typeof fetch; +}; + +export class ApiError extends Error { + status: number; + code?: string; + requestId?: string; + details?: unknown; + rawMessage?: string; + + constructor({ + message, + status, + code, + requestId, + details, + rawMessage + }: { + message: string; + status: number; + code?: string; + requestId?: string; + details?: unknown; + rawMessage?: string; + }) { + super(message); + this.name = 'ApiError'; + this.status = status; + this.code = code; + this.requestId = requestId; + this.details = details; + this.rawMessage = rawMessage; + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value != null; +} + +function parseErrorPayload(data: unknown) { + if (!isRecord(data)) { + return { + code: undefined as string | undefined, + rawMessage: undefined as string | undefined, + requestId: undefined as string | undefined, + details: undefined as unknown + }; + } + + const code = typeof data.code === 'string' ? data.code : undefined; + const rawMessage = typeof data.error === 'string' ? data.error : undefined; + const requestId = typeof data.requestId === 'string' ? data.requestId : undefined; + const details = data.details; + + return { code, rawMessage, requestId, details }; +} + +function defaultResolveError({ status, fallbackMessage }: ResolveErrorInput): string { + if (fallbackMessage) { + return fallbackMessage; + } + + if (status != null) { + return `Request failed (${status}).`; + } + + return 'Request failed. Please try again.'; +} + +export function createApiClient(config: CreateApiClientConfig) { + const { + baseUrl, + resolveError = defaultResolveError, + inferErrorCodeFromStatus, + fetchImpl + } = config; + + async function request(path: string, options: RequestOptions = {}): Promise { + const { method = 'GET', token, body } = options; + const runFetch = fetchImpl ?? fetch; + + const response = await runFetch(`${baseUrl}${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}) + }, + body: body ? JSON.stringify(body) : undefined + }); + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const parsed = parseErrorPayload(data); + const code = parsed.code ?? inferErrorCodeFromStatus?.(response.status); + const message = resolveError({ + code, + status: response.status, + fallbackMessage: parsed.rawMessage + }); + + throw new ApiError({ + message, + status: response.status, + code, + requestId: parsed.requestId, + details: parsed.details, + rawMessage: parsed.rawMessage + }); + } + + return data as T; + } + + return { + request + }; +} diff --git a/src/api/query.ts b/src/api/query.ts new file mode 100644 index 0000000..2574a1e --- /dev/null +++ b/src/api/query.ts @@ -0,0 +1,29 @@ +type BuildListQueryOptions = { + q?: string; + page?: number; + pageSize?: number; + sort?: string; + defaultSort: string; +}; + +export function buildListQuery({ + q, + page = 1, + pageSize = 10, + sort, + defaultSort +}: BuildListQueryOptions): string { + const query = new URLSearchParams(); + const normalizedQuery = q?.trim(); + const normalizedSort = sort?.trim(); + + if (normalizedQuery) { + query.set('q', normalizedQuery); + } + + query.set('page', String(page)); + query.set('pageSize', String(pageSize)); + query.set('sort', normalizedSort || defaultSort); + + return query.toString(); +} diff --git a/src/auth/createAuthContext.tsx b/src/auth/createAuthContext.tsx new file mode 100644 index 0000000..4a2bc99 --- /dev/null +++ b/src/auth/createAuthContext.tsx @@ -0,0 +1,90 @@ +import { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; + +const DEFAULT_AUTH_TOKEN_KEY = 'authToken'; +const DEFAULT_REFRESH_TOKEN_KEY = 'refreshToken'; +const DEFAULT_LEGACY_KEYS = ['auth_token', 'auth_user', 'token']; + +export type AuthState = { + authToken: string | null; + refreshToken: string | null; + currentUser: TUser | null; +}; + +export type AuthContextValue = AuthState & { + setSession: (authToken: string, refreshToken: string, currentUser: TUser) => void; + setCurrentUser: (currentUser: TUser | null) => void; + clearSession: () => void; +}; + +export type CreateAuthContextOptions = { + authTokenKey?: string; + refreshTokenKey?: string; + legacyKeys?: string[]; +}; + +export function createAuthContext(options: CreateAuthContextOptions = {}) { + const authTokenKey = options.authTokenKey ?? DEFAULT_AUTH_TOKEN_KEY; + const refreshTokenKey = options.refreshTokenKey ?? DEFAULT_REFRESH_TOKEN_KEY; + const legacyKeys = options.legacyKeys ?? DEFAULT_LEGACY_KEYS; + + const AuthContext = createContext | undefined>(undefined); + + function readStoredSession(): AuthState { + for (const key of legacyKeys) { + localStorage.removeItem(key); + } + + const authToken = localStorage.getItem(authTokenKey); + const refreshToken = localStorage.getItem(refreshTokenKey); + + return { + authToken, + refreshToken, + currentUser: null + }; + } + + function AuthProvider({ children }: Readonly<{ children: ReactNode }>) { + const [state, setState] = useState>(readStoredSession); + + const setSession = useCallback((authToken: string, refreshToken: string, currentUser: TUser) => { + localStorage.setItem(authTokenKey, authToken); + localStorage.setItem(refreshTokenKey, refreshToken); + setState({ authToken, refreshToken, currentUser }); + }, []); + + const clearSession = useCallback(() => { + localStorage.removeItem(authTokenKey); + localStorage.removeItem(refreshTokenKey); + setState({ authToken: null, refreshToken: null, currentUser: null }); + }, []); + + const setCurrentUser = useCallback((currentUser: TUser | null) => { + setState((prev) => ({ ...prev, currentUser })); + }, []); + + const value = useMemo>(() => ({ + ...state, + setSession, + setCurrentUser, + clearSession + }), [state, setSession, setCurrentUser, clearSession]); + + return {children}; + } + + function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error('useAuth must be used within AuthProvider'); + } + return ctx; + } + + return { + AuthProvider, + useAuth, + AuthContext + }; +} diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts new file mode 100644 index 0000000..34602be --- /dev/null +++ b/src/auth/jwt.ts @@ -0,0 +1,35 @@ +const MILLISECONDS_PER_SECOND = 1000; + +export function decodeJwtPayload(token: string): Record | null { + const parts = token.split('.'); + if (parts.length !== 3) { + return null; + } + + const base64Url = parts[1]; + const base64 = base64Url.replaceAll('-', '+').replaceAll('_', '/'); + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '='); + + try { + const payload = JSON.parse(atob(padded)); + if (payload && typeof payload === 'object') { + return payload as Record; + } + } catch { + return null; + } + + return null; +} + +export function isJwtExpired(token: string, skewSeconds = 0) { + const payload = decodeJwtPayload(token); + const exp = payload?.exp; + if (typeof exp !== 'number' || !Number.isFinite(exp)) { + return false; + } + + const expiresAt = exp * MILLISECONDS_PER_SECOND; + const now = Date.now(); + return expiresAt <= now + skewSeconds * MILLISECONDS_PER_SECOND; +} diff --git a/src/contexts/LeftMenuContext.tsx b/src/contexts/LeftMenuContext.tsx new file mode 100644 index 0000000..ba1e405 --- /dev/null +++ b/src/contexts/LeftMenuContext.tsx @@ -0,0 +1,217 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type CSSProperties, + type ReactNode +} from 'react'; +import { isDesktopViewport, useSidePanelMachine } from '../panels/useSidePanelMachine'; + +const SIDEBAR_WIDTH_KEY = 'authSidebarWidth'; +const SIDEBAR_COLLAPSED_KEY = 'authSidebarCollapsed'; +const SIDEBAR_DEFAULT_WIDTH = 280; +const SIDEBAR_MIN_WIDTH = 220; +const SIDEBAR_MAX_WIDTH = 420; +const SIDEBAR_COLLAPSED_WIDTH = 56; + +export type LeftMenuRenderState = { + collapsed: boolean; + mobileOpen: boolean; + isDesktop: boolean; + closeMenu: () => void; +}; + +export type LeftMenuContent = { + ariaLabel?: string; + render: (state: LeftMenuRenderState) => ReactNode; +}; + +export type LeftMenuStyle = CSSProperties & { + '--auth-sidebar-width': string; +}; + +type LeftMenuContextValue = { + collapsed: boolean; + mobileOpen: boolean; + content: LeftMenuContent; + desktopMenuStyle: LeftMenuStyle; + openMenu: (content?: LeftMenuContent) => void; + closeMenu: () => void; + toggleMenu: (content?: LeftMenuContent) => void; + expandMenu: () => void; + collapseMenu: () => void; + toggleCollapsed: () => void; + setMenuContent: (content: LeftMenuContent | null) => void; + startResize: ReturnType['startResize']; +}; + +type LeftMenuProviderProps = { + children: ReactNode; + defaultContent: LeftMenuContent; + closeOnPathname?: string; +}; + +const LeftMenuContext = createContext(undefined); + +function readStoredCollapsed(): boolean { + if (!globalThis.window) { + return false; + } + + return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1'; +} + +export function LeftMenuProvider({ + children, + defaultContent, + closeOnPathname +}: Readonly) { + const [collapsed, setCollapsed] = useState(() => readStoredCollapsed()); + const [mobileOpen, setMobileOpen] = useState(false); + const [content, setContent] = useState(defaultContent); + const defaultContentRef = useRef(defaultContent); + + useEffect(() => { + const previousDefaultContent = defaultContentRef.current; + defaultContentRef.current = defaultContent; + setContent((currentContent) => { + if (currentContent === previousDefaultContent) { + return defaultContent; + } + return currentContent; + }); + }, [defaultContent]); + + useEffect(() => { + if (!globalThis.window) { + return; + } + + localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed ? '1' : '0'); + }, [collapsed]); + + const expandMenu = useCallback(() => { + setCollapsed(false); + }, []); + + const collapseMenu = useCallback(() => { + setCollapsed(true); + }, []); + + const toggleCollapsed = useCallback(() => { + setCollapsed((previous) => !previous); + }, []); + + const closeMobile = useCallback(() => { + setMobileOpen(false); + }, []); + + const setMenuContent = useCallback((nextContent: LeftMenuContent | null) => { + setContent(nextContent ?? defaultContentRef.current); + }, []); + + const closeMenu = useCallback(() => { + if (isDesktopViewport()) { + collapseMenu(); + return; + } + + closeMobile(); + }, [collapseMenu, closeMobile]); + + const openMenu = useCallback((nextContent?: LeftMenuContent) => { + if (nextContent) { + setContent(nextContent); + } + + if (isDesktopViewport()) { + expandMenu(); + return; + } + + setMobileOpen(true); + }, [expandMenu]); + + const toggleMenu = useCallback((nextContent?: LeftMenuContent) => { + if (nextContent) { + setContent(nextContent); + } + + if (isDesktopViewport()) { + toggleCollapsed(); + return; + } + + setMobileOpen((previous) => !previous); + }, [toggleCollapsed]); + + const handleCloseOnPathname = useCallback(() => { + setMobileOpen(false); + setContent(defaultContentRef.current); + }, []); + + const { width, startResize } = useSidePanelMachine({ + storageKey: SIDEBAR_WIDTH_KEY, + defaultWidth: SIDEBAR_DEFAULT_WIDTH, + minWidth: SIDEBAR_MIN_WIDTH, + maxWidth: SIDEBAR_MAX_WIDTH, + resizeAxis: 'from-left', + resizingBodyClass: 'auth-sidebar-resizing', + isOpen: mobileOpen, + canResize: !collapsed, + shouldPersistWidth: !collapsed, + closeOnPathname, + onCloseOnPathname: handleCloseOnPathname, + onEscape: closeMobile + }); + + const desktopMenuStyle = useMemo(() => ({ + '--auth-sidebar-width': `${collapsed ? SIDEBAR_COLLAPSED_WIDTH : width}px` + }), [collapsed, width]); + + const value = useMemo(() => ({ + collapsed, + mobileOpen, + content, + desktopMenuStyle, + openMenu, + closeMenu, + toggleMenu, + expandMenu, + collapseMenu, + toggleCollapsed, + setMenuContent, + startResize + }), [ + collapsed, + mobileOpen, + content, + desktopMenuStyle, + openMenu, + closeMenu, + toggleMenu, + expandMenu, + collapseMenu, + toggleCollapsed, + setMenuContent, + startResize + ]); + + return ( + + {children} + + ); +} + +export function useLeftMenu() { + const ctx = useContext(LeftMenuContext); + if (!ctx) { + throw new Error('useLeftMenu must be used within LeftMenuProvider'); + } + return ctx; +} diff --git a/src/contexts/RightSidebarContext.tsx b/src/contexts/RightSidebarContext.tsx new file mode 100644 index 0000000..79b7c32 --- /dev/null +++ b/src/contexts/RightSidebarContext.tsx @@ -0,0 +1,139 @@ +import { + createContext, + useCallback, + useContext, + useMemo, + useState, + type CSSProperties, + type ReactNode +} from 'react'; +import { isDesktopViewport, useSidePanelMachine } from '../panels/useSidePanelMachine'; + +const RIGHT_SIDEBAR_WIDTH_KEY = 'authRightSidebarWidth'; +const RIGHT_SIDEBAR_DEFAULT_WIDTH = 320; +const RIGHT_SIDEBAR_MIN_WIDTH = 260; +const RIGHT_SIDEBAR_MAX_WIDTH = 480; + +export type RightSidebarContent = { + title: string; + content: ReactNode; + ariaLabel?: string; +}; + +export type RightSidebarStyle = CSSProperties & { + '--auth-right-sidebar-width': string; +}; + +type RightSidebarContextValue = { + isOpen: boolean; + content: RightSidebarContent | null; + openSidebar: (content?: RightSidebarContent) => void; + closeSidebar: () => void; + toggleSidebar: (content?: RightSidebarContent) => void; + setSidebarContent: (content: RightSidebarContent | null) => void; + desktopSidebarStyle: RightSidebarStyle; + startResize: ReturnType['startResize']; +}; + +type RightSidebarProviderProps = { + children: ReactNode; + closeOnPathname?: string; + onMobileOpenRequest?: () => void; +}; + +const RightSidebarContext = createContext(undefined); + +export function RightSidebarProvider({ + children, + closeOnPathname, + onMobileOpenRequest +}: Readonly) { + const [isOpen, setIsOpen] = useState(false); + const [content, setContent] = useState(null); + + const closeSidebar = useCallback(() => { + setIsOpen(false); + setContent(null); + }, []); + + const setSidebarContent = useCallback((nextContent: RightSidebarContent | null) => { + setContent(nextContent); + }, []); + + const openSidebar = useCallback((nextContent?: RightSidebarContent) => { + const resolvedContent = nextContent ?? content; + if (!resolvedContent) { + return; + } + + if (nextContent) { + setContent(nextContent); + } + if (!isDesktopViewport()) { + onMobileOpenRequest?.(); + } + setIsOpen(true); + }, [content, onMobileOpenRequest]); + + const toggleSidebar = useCallback((nextContent?: RightSidebarContent) => { + if (isOpen) { + closeSidebar(); + return; + } + + openSidebar(nextContent); + }, [isOpen, closeSidebar, openSidebar]); + + const { width, startResize } = useSidePanelMachine({ + storageKey: RIGHT_SIDEBAR_WIDTH_KEY, + defaultWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH, + minWidth: RIGHT_SIDEBAR_MIN_WIDTH, + maxWidth: RIGHT_SIDEBAR_MAX_WIDTH, + resizeAxis: 'from-right', + resizingBodyClass: 'auth-right-sidebar-resizing', + isOpen, + canResize: isOpen, + shouldPersistWidth: true, + closeOnPathname, + onCloseOnPathname: closeSidebar, + onEscape: closeSidebar + }); + + const desktopSidebarStyle = useMemo(() => ({ + '--auth-right-sidebar-width': `${width}px` + }), [width]); + + const value = useMemo(() => ({ + isOpen, + content, + openSidebar, + closeSidebar, + toggleSidebar, + setSidebarContent, + desktopSidebarStyle, + startResize + }), [ + isOpen, + content, + openSidebar, + closeSidebar, + toggleSidebar, + setSidebarContent, + desktopSidebarStyle, + startResize + ]); + + return ( + + {children} + + ); +} + +export function useRightSidebar() { + const ctx = useContext(RightSidebarContext); + if (!ctx) { + throw new Error('useRightSidebar must be used within RightSidebarProvider'); + } + return ctx; +} diff --git a/src/errors/createErrorResolver.ts b/src/errors/createErrorResolver.ts new file mode 100644 index 0000000..c06bd6b --- /dev/null +++ b/src/errors/createErrorResolver.ts @@ -0,0 +1,132 @@ +export type ErrorCatalog = Record; + +type ErrorLike = { + code?: unknown; + status?: unknown; + message?: unknown; + rawMessage?: unknown; +}; + +export type ResolveErrorMessageOptions = { + code?: string | null; + status?: number | null; + context?: string; + fallbackMessage?: string | null; +}; + +export type CreateErrorResolverConfig = { + catalog: ErrorCatalog; + fallbackCode?: string; + defaultContext?: string; + contextOverrides?: Record>>; + inferCodeFromStatus?: (status?: number | null) => string | undefined; + inferCodeFromLegacyMessage?: (message?: string | null) => string | undefined; +}; + +export function createErrorResolver(config: CreateErrorResolverConfig) { + const { + catalog, + fallbackCode, + defaultContext = 'default', + contextOverrides = {}, + inferCodeFromStatus, + inferCodeFromLegacyMessage + } = config; + + const knownCodes = new Set(Object.keys(catalog)); + + function isKnownErrorCode(value: string): boolean { + return knownCodes.has(value); + } + + function normalizeErrorCode(code?: string | null): string | undefined { + if (!code) { + return undefined; + } + return isKnownErrorCode(code) ? code : undefined; + } + + function inferErrorCodeFromStatus(status?: number | null): string | undefined { + return inferCodeFromStatus?.(status); + } + + function resolveErrorMessage(options: ResolveErrorMessageOptions): string { + const { + code, + status, + context = defaultContext, + fallbackMessage + } = options; + + const resolvedCode = normalizeErrorCode(code) + ?? inferCodeFromLegacyMessage?.(fallbackMessage) + ?? inferErrorCodeFromStatus(status); + + if (resolvedCode) { + const contextMessage = contextOverrides[context]?.[resolvedCode]; + if (contextMessage) { + return contextMessage; + } + const catalogMessage = catalog[resolvedCode]; + if (catalogMessage) { + return catalogMessage; + } + } + + const statusCode = inferErrorCodeFromStatus(status); + if (statusCode) { + const contextMessage = contextOverrides[context]?.[statusCode]; + if (contextMessage) { + return contextMessage; + } + const catalogMessage = catalog[statusCode]; + if (catalogMessage) { + return catalogMessage; + } + } + + if (fallbackCode && catalog[fallbackCode]) { + return catalog[fallbackCode]; + } + + if (fallbackMessage) { + return fallbackMessage; + } + + return 'Request failed. Please try again.'; + } + + function resolveOptionalErrorMessage(code?: string | null, context: string = defaultContext): string | undefined { + if (!code) { + return undefined; + } + return resolveErrorMessage({ code, context }); + } + + function toErrorMessage(err: unknown, context: string = defaultContext): string { + if (err && typeof err === 'object') { + const errorLike = err as ErrorLike; + const code = typeof errorLike.code === 'string' ? errorLike.code : undefined; + const status = typeof errorLike.status === 'number' ? errorLike.status : undefined; + const rawMessage = typeof errorLike.rawMessage === 'string' ? errorLike.rawMessage : undefined; + const message = typeof errorLike.message === 'string' ? errorLike.message : undefined; + + return resolveErrorMessage({ + code, + status, + context, + fallbackMessage: rawMessage ?? message + }); + } + + return resolveErrorMessage({ context }); + } + + return { + isKnownErrorCode, + inferErrorCodeFromStatus, + resolveErrorMessage, + resolveOptionalErrorMessage, + toErrorMessage + }; +} diff --git a/src/hooks/useCooldownTimer.ts b/src/hooks/useCooldownTimer.ts new file mode 100644 index 0000000..1cc0934 --- /dev/null +++ b/src/hooks/useCooldownTimer.ts @@ -0,0 +1,28 @@ +import { useCallback, useEffect, useState } from 'react'; + +export function useCooldownTimer(seconds = 0, enabled = true) { + const [cooldown, setCooldown] = useState(seconds); + + useEffect(() => { + if (!enabled || cooldown <= 0) { + return; + } + + const timer = globalThis.setInterval(() => { + setCooldown((prev) => (prev > 0 ? prev - 1 : 0)); + }, 1000); + + return () => { + globalThis.clearInterval(timer); + }; + }, [enabled, cooldown]); + + const startCooldown = useCallback((seconds: number) => { + setCooldown(Math.max(0, Math.floor(seconds))); + }, []); + + return { + cooldown, + startCooldown + }; +} diff --git a/src/hooks/useEditableForm.ts b/src/hooks/useEditableForm.ts new file mode 100644 index 0000000..affb8ee --- /dev/null +++ b/src/hooks/useEditableForm.ts @@ -0,0 +1,68 @@ +import { useCallback, useState } from 'react'; +import { useValidatedFields } from './useValidatedFields'; + +type FieldErrors = Partial>; + +type UseEditableFormOptions = { + initialValues: TValues; + validate: (values: TValues) => FieldErrors; +}; + +export function useEditableForm>({ + initialValues, + validate +}: UseEditableFormOptions) { + const [isEditing, setIsEditing] = useState(false); + + const { + values, + errors, + isValid, + setValues, + setFieldValue, + validateAll, + setFieldError, + setErrors, + clearErrors + } = useValidatedFields({ + initialValues, + validate + }); + + const startEditing = useCallback((sourceValues: TValues) => { + setValues(sourceValues, { validate: true }); + setIsEditing(true); + }, [setValues]); + + const discardChanges = useCallback((sourceValues: TValues) => { + setValues(sourceValues, { clearErrors: true }); + setIsEditing(false); + }, [setValues]); + + const loadFromSource = useCallback((sourceValues: TValues) => { + setValues(sourceValues, { clearErrors: true }); + }, [setValues]); + + const commitSaved = useCallback((sourceValues: TValues) => { + setValues(sourceValues, { clearErrors: true }); + setIsEditing(false); + }, [setValues]); + + return { + values, + errors, + isValid, + setValues, + setFieldValue, + validateAll, + setFieldError, + setErrors, + clearErrors, + isEditing, + startEditing, + discardChanges, + loadFromSource, + commitSaved, + setIsEditing + }; +} diff --git a/src/hooks/usePaginatedResource.ts b/src/hooks/usePaginatedResource.ts new file mode 100644 index 0000000..891bfb3 --- /dev/null +++ b/src/hooks/usePaginatedResource.ts @@ -0,0 +1,102 @@ +import { useCallback, useEffect, useState } from 'react'; + +type PaginatedResourceResponse = { + items: TItem[]; + page: number; + pageSize: number; + total: number; + totalPages: number; +}; + +type UsePaginatedResourceOptions = { + load: (params: { q: string; page: number; pageSize: number; sort?: string }) => Promise>; + sort?: string; + debounceMs?: number; + initialQuery?: string; + initialPage?: number; + initialPageSize?: number; +}; + +export function usePaginatedResource({ + load, + sort, + debounceMs = 250, + initialQuery = '', + initialPage = 1, + initialPageSize = 10 +}: UsePaginatedResourceOptions) { + const [items, setItems] = useState([]); + const [q, setQ] = useState(initialQuery); + const [page, setPage] = useState(initialPage); + const [pageSize, setPageSize] = useState(initialPageSize); + const [total, setTotal] = useState(0); + const [totalPages, setTotalPages] = useState(0); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + setIsLoading(true); + setError(null); + + const timer = setTimeout(() => { + void (async () => { + try { + const response = await load({ + q, + page, + pageSize, + sort + }); + + if (cancelled) { + return; + } + + setItems(response.items); + setTotal(response.total); + setTotalPages(response.totalPages); + setPage(response.page); + setPageSize(response.pageSize); + } catch (err) { + if (!cancelled) { + setError(err instanceof Error ? err.message : 'Request failed. Please try again.'); + } + } finally { + if (!cancelled) { + setIsLoading(false); + } + } + })(); + }, debounceMs); + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [q, page, pageSize, sort, load, debounceMs]); + + const setQuery = useCallback((value: string) => { + setQ(value); + setPage(1); + }, []); + + const setPageSizeAndResetPage = useCallback((value: number) => { + setPageSize(value); + setPage(1); + }, []); + + return { + items, + q, + page, + pageSize, + total, + totalPages, + error, + isLoading, + setQuery, + setPage, + setPageSize: setPageSizeAndResetPage + }; +} diff --git a/src/hooks/useSorting.ts b/src/hooks/useSorting.ts new file mode 100644 index 0000000..51d92d4 --- /dev/null +++ b/src/hooks/useSorting.ts @@ -0,0 +1,78 @@ +import { useCallback, useMemo, useState } from 'react'; + +export type SortDirection = 'asc' | 'desc'; + +export type SortState = { + field: string; + direction: SortDirection; +}; + +function invertDirection(direction: SortDirection): SortDirection { + return direction === 'asc' ? 'desc' : 'asc'; +} + +export function formatSortParam(sort: SortState | null | undefined): string | undefined { + if (!sort) { + return undefined; + } + return sort.direction === 'desc' ? `-${sort.field}` : sort.field; +} + +type UseSortingResult = { + activeSort: SortState | null; + sortParam: string | undefined; + toggleSort: (field: string) => void; + setSort: (next: SortState | null) => void; + resetSort: () => void; +}; + +export function useSorting(defaultSort?: SortState | null): UseSortingResult { + const [overrideSort, setOverrideSort] = useState(null); + + const activeSort = overrideSort ?? defaultSort ?? null; + + const toggleSort = useCallback((field: string) => { + setOverrideSort((previousOverride) => { + const baselineSort = defaultSort ?? null; + const currentSort = previousOverride ?? baselineSort; + + if (!currentSort || currentSort.field !== field) { + return { field, direction: 'asc' }; + } + + if (baselineSort && baselineSort.field === field) { + if (previousOverride == null) { + return { field, direction: invertDirection(baselineSort.direction) }; + } + if (previousOverride.direction === baselineSort.direction) { + return { field, direction: invertDirection(baselineSort.direction) }; + } + return null; + } + + if (previousOverride == null || previousOverride.direction === 'desc') { + return null; + } + + return { field, direction: 'desc' }; + }); + }, [defaultSort]); + + const setSort = useCallback((next: SortState | null) => { + setOverrideSort(next); + }, []); + + const resetSort = useCallback(() => { + setOverrideSort(null); + }, []); + + const sortParam = useMemo(() => formatSortParam(activeSort), [activeSort]); + + return { + activeSort, + sortParam, + toggleSort, + setSort, + resetSort + }; +} diff --git a/src/hooks/useSubmitState.ts b/src/hooks/useSubmitState.ts new file mode 100644 index 0000000..d441db6 --- /dev/null +++ b/src/hooks/useSubmitState.ts @@ -0,0 +1,31 @@ +import { useCallback, useState } from 'react'; + +export function useSubmitState(initialStatus: TStatus) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + const [status, setStatus] = useState(initialStatus); + + const startSubmitting = useCallback(() => { + setIsSubmitting(true); + }, []); + + const finishSubmitting = useCallback(() => { + setIsSubmitting(false); + }, []); + + const clearFeedback = useCallback(() => { + setSubmitError(null); + setStatus(initialStatus); + }, [initialStatus]); + + return { + isSubmitting, + submitError, + status, + startSubmitting, + finishSubmitting, + setSubmitError, + setStatus, + clearFeedback + }; +} diff --git a/src/hooks/useValidatedFields.ts b/src/hooks/useValidatedFields.ts new file mode 100644 index 0000000..1cab7a8 --- /dev/null +++ b/src/hooks/useValidatedFields.ts @@ -0,0 +1,166 @@ +import { useCallback, useMemo, useState } from 'react'; + +type FieldErrors = Partial>; +type TouchedFields = Partial>; + +type SetValuesOptions = { + validate?: boolean; + clearErrors?: boolean; +}; + +type SetFieldValueOptions = { + validate?: boolean; + touch?: boolean; +}; + +type ValidateAllOptions = { + touchAll?: boolean; +}; + +type UseValidatedFieldsOptions = { + initialValues: TValues; + validate: (values: TValues) => FieldErrors; +}; + +function hasErrors(errors: FieldErrors): boolean { + return Object.values(errors).some(Boolean); +} + +function pickTouchedErrors( + errors: FieldErrors, + touched: TouchedFields +): FieldErrors { + const next: FieldErrors = {}; + + for (const key of Object.keys(errors) as Array) { + if (touched[key]) { + next[key] = errors[key]; + } + } + + return next; +} + +function touchAll>(values: TValues): TouchedFields { + const touched: TouchedFields = {}; + + for (const key of Object.keys(values) as Array) { + touched[key] = true; + } + + return touched; +} + +export function useValidatedFields>({ + initialValues, + validate +}: UseValidatedFieldsOptions) { + const [values, setValues] = useState(initialValues); + const [allErrors, setAllErrors] = useState>(() => validate(initialValues)); + const [touched, setTouched] = useState>({}); + + const updateValues = useCallback((nextValues: TValues, options: SetValuesOptions = {}) => { + const { validate: shouldValidate = false, clearErrors = false } = options; + setValues(nextValues); + + if (shouldValidate || clearErrors) { + setAllErrors(validate(nextValues)); + } + + if (clearErrors) { + setTouched({}); + } + }, [validate]); + + const setFieldValue = useCallback(( + key: K, + value: TValues[K], + options: SetFieldValueOptions = {} + ) => { + const { validate: shouldValidate = true, touch = true } = options; + + if (touch) { + setTouched((current) => ({ + ...current, + [key]: true + })); + } + + setValues((current) => { + const nextValues = { + ...current, + [key]: value + }; + + if (shouldValidate) { + setAllErrors(validate(nextValues)); + } + + return nextValues; + }); + }, [validate]); + + const validateAll = useCallback((options: ValidateAllOptions = {}) => { + const { touchAll: shouldTouchAll = true } = options; + const nextErrors = validate(values); + + setAllErrors(nextErrors); + + if (shouldTouchAll) { + setTouched(touchAll(values)); + } + + return nextErrors; + }, [validate, values]); + + const setFieldError = useCallback((key: K, message?: string) => { + setTouched((current) => ({ + ...current, + [key]: true + })); + + setAllErrors((current) => ({ + ...current, + [key]: message + })); + }, []); + + const updateErrors = useCallback((nextErrors: FieldErrors) => { + const nextTouched: TouchedFields = {}; + + for (const key of Object.keys(nextErrors) as Array) { + if (nextErrors[key]) { + nextTouched[key] = true; + } + } + + setTouched((current) => ({ + ...current, + ...nextTouched + })); + setAllErrors(nextErrors); + }, []); + + const clearErrors = useCallback(() => { + setAllErrors(validate(values)); + setTouched({}); + }, [validate, values]); + + const errors = useMemo(() => pickTouchedErrors(allErrors, touched), [allErrors, touched]); + + const isValid = useMemo(() => { + return !hasErrors(validate(values)); + }, [validate, values]); + + return { + values, + errors, + isValid, + setValues: updateValues, + setFieldValue, + validateAll, + setFieldError, + setErrors: updateErrors, + clearErrors + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0557c0d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,29 @@ +export { createAuthContext } from './auth/createAuthContext'; +export type { AuthContextValue, AuthState, CreateAuthContextOptions } from './auth/createAuthContext'; + +export { decodeJwtPayload, isJwtExpired } from './auth/jwt'; + +export { createApiClient, ApiError } from './api/createApiClient'; +export type { CreateApiClientConfig, RequestOptions, ResolveErrorInput } from './api/createApiClient'; + +export { buildListQuery } from './api/query'; + +export { createErrorResolver } from './errors/createErrorResolver'; +export type { CreateErrorResolverConfig, ErrorCatalog, ResolveErrorMessageOptions } from './errors/createErrorResolver'; + +export { useValidatedFields } from './hooks/useValidatedFields'; +export { useEditableForm } from './hooks/useEditableForm'; +export { useSubmitState } from './hooks/useSubmitState'; +export { usePaginatedResource } from './hooks/usePaginatedResource'; +export { useSorting, formatSortParam } from './hooks/useSorting'; +export type { SortDirection, SortState } from './hooks/useSorting'; +export { useCooldownTimer } from './hooks/useCooldownTimer'; + +export { LeftMenuProvider, useLeftMenu } from './contexts/LeftMenuContext'; +export type { LeftMenuContent, LeftMenuRenderState, LeftMenuStyle } from './contexts/LeftMenuContext'; +export { RightSidebarProvider, useRightSidebar } from './contexts/RightSidebarContext'; +export type { RightSidebarContent, RightSidebarStyle } from './contexts/RightSidebarContext'; + +export { formatDate, capitalize, splitAndCapitalize } from './utils/formatting'; +export type { SplitMode } from './utils/formatting'; +export { shouldShowVerifiedEmailBadge } from './utils/verifiedEmail'; diff --git a/src/panels/useSidePanelMachine.ts b/src/panels/useSidePanelMachine.ts new file mode 100644 index 0000000..7fa0bea --- /dev/null +++ b/src/panels/useSidePanelMachine.ts @@ -0,0 +1,174 @@ +import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'; + +const DEFAULT_DESKTOP_BREAKPOINT = 1024; + +type ResizeAxis = 'from-left' | 'from-right'; + +export type SidePanelMachineOptions = { + storageKey: string; + defaultWidth: number; + minWidth: number; + maxWidth: number; + resizeAxis: ResizeAxis; + resizingBodyClass: string; + isOpen: boolean; + canResize: boolean; + shouldPersistWidth: boolean; + closeOnPathname?: string; + onCloseOnPathname?: () => void; + onEscape?: () => void; + desktopBreakpoint?: number; +}; + +export type SidePanelMachineResult = { + width: number; + isDesktop: boolean; + startResize: (event: ReactPointerEvent) => void; +}; + +export function isDesktopViewport(breakpoint = DEFAULT_DESKTOP_BREAKPOINT): boolean { + if (!globalThis.window) { + return true; + } + + if (typeof globalThis.window.matchMedia === 'function') { + return globalThis.window.matchMedia(`(min-width: ${breakpoint}px)`).matches; + } + + return window.innerWidth >= breakpoint; +} + +export function useSidePanelMachine({ + storageKey, + defaultWidth, + minWidth, + maxWidth, + resizeAxis, + resizingBodyClass, + isOpen, + canResize, + shouldPersistWidth, + closeOnPathname, + onCloseOnPathname, + onEscape, + desktopBreakpoint = DEFAULT_DESKTOP_BREAKPOINT +}: SidePanelMachineOptions): SidePanelMachineResult { + const isResizingRef = useRef(false); + const resizeStartXRef = useRef(0); + const resizeStartWidthRef = useRef(0); + + const clampWidth = useCallback((value: number) => { + return Math.min(maxWidth, Math.max(minWidth, value)); + }, [maxWidth, minWidth]); + + const readStoredWidth = useCallback(() => { + if (!globalThis.window) { + return defaultWidth; + } + + const storedValue = localStorage.getItem(storageKey); + const parsed = Number(storedValue); + if (!Number.isFinite(parsed)) { + return defaultWidth; + } + + return clampWidth(parsed); + }, [defaultWidth, storageKey, clampWidth]); + + const [width, setWidth] = useState(() => readStoredWidth()); + + useEffect(() => { + if (closeOnPathname == null || !onCloseOnPathname) { + return; + } + + onCloseOnPathname(); + }, [closeOnPathname, onCloseOnPathname]); + + useEffect(() => { + if (!shouldPersistWidth || !globalThis.window) { + return; + } + + localStorage.setItem(storageKey, String(width)); + }, [shouldPersistWidth, storageKey, width]); + + useEffect(() => { + function handlePointerMove(event: PointerEvent) { + if (!isResizingRef.current || !canResize) { + return; + } + + const deltaX = event.clientX - resizeStartXRef.current; + const delta = resizeAxis === 'from-left' ? deltaX : -deltaX; + const nextWidth = clampWidth(resizeStartWidthRef.current + delta); + setWidth(nextWidth); + } + + function stopResizing() { + if (!isResizingRef.current) { + return; + } + + isResizingRef.current = false; + document.body.classList.remove(resizingBodyClass); + } + + globalThis.addEventListener('pointermove', handlePointerMove); + globalThis.addEventListener('pointerup', stopResizing); + + return () => { + globalThis.removeEventListener('pointermove', handlePointerMove); + globalThis.removeEventListener('pointerup', stopResizing); + document.body.classList.remove(resizingBodyClass); + }; + }, [canResize, clampWidth, resizeAxis, resizingBodyClass]); + + useEffect(() => { + if (!isOpen || isDesktopViewport(desktopBreakpoint)) { + return; + } + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + + return () => { + document.body.style.overflow = previousOverflow; + }; + }, [isOpen, desktopBreakpoint]); + + useEffect(() => { + if (!isOpen || !onEscape) { + return; + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onEscape(); + } + }; + + globalThis.addEventListener('keydown', handleEscape); + return () => { + globalThis.removeEventListener('keydown', handleEscape); + }; + }, [isOpen, onEscape]); + + const startResize = useCallback((event: ReactPointerEvent) => { + if (!canResize || !isDesktopViewport(desktopBreakpoint)) { + return; + } + + isResizingRef.current = true; + resizeStartXRef.current = event.clientX; + resizeStartWidthRef.current = width; + document.body.classList.add(resizingBodyClass); + event.preventDefault(); + }, [canResize, desktopBreakpoint, resizingBodyClass, width]); + + return { + width, + isDesktop: isDesktopViewport(desktopBreakpoint), + startResize + }; +} diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts new file mode 100644 index 0000000..a73b40b --- /dev/null +++ b/src/utils/formatting.ts @@ -0,0 +1,64 @@ + +export function formatDate(value: string, seconds = false): string { + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + ...(seconds ? { second: "2-digit" } : {}), + }; + + return new Date(value).toLocaleString("it-IT", options); +} + +export const capitalize = (str: string) => + str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + +export type SplitMode = "underscore" | "camel" | "auto"; + +/** Title-case a string while preserving short all-caps acronyms (e.g., XML) */ +const toTitleCase = (s: string) => + s + .trim() + .toLowerCase() + .split(/\s+/) + .map(w => + /^[A-Z]{2,4}$/.test(w) ? w : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() + ) + .join(" "); + +const splitUnderscoreHyphen = (s: string) => s.replaceAll(/[_-]+/g, " "); + +/** Insert spaces at camelCase boundaries and around digit/letter edges */ +const splitCamel = (s: string) => + s + // fooBar -> foo Bar ; foo2D -> foo 2D + .replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2") + // XMLHttp -> XML Http (acronym + word) + .replaceAll(/([A-Z])([A-Z][a-z])/g, "$1 $2") + // letter<->digit boundaries + .replaceAll(/([a-zA-Z])([0-9])/g, "$1 $2") + .replaceAll(/([0-9])([a-zA-Z])/g, "$1 $2"); + +/** + * Split and capitalize either by underscores/hyphens or camelCase. + * mode: + * - "underscore": split on _ or - + * - "camel": split on camelCase boundaries + * - "auto": pick underscore if present, otherwise camel + */ +export function splitAndCapitalize(str?: string, mode: SplitMode = "auto"): string { + if (!str) return ""; + + // normalize underscores/hyphens first for auto decision + const hasUnderscoreLike = /[_-]/.test(str); + const chosen: SplitMode = + mode === "auto" ? (hasUnderscoreLike ? "underscore" : "camel") : mode; + + const spaced = + chosen === "underscore" ? splitUnderscoreHyphen(str) : splitCamel(str); + + // collapse extra spaces, then title-case + return toTitleCase(spaced.replaceAll(/\s+/g, " ").trim()); +} diff --git a/src/utils/verifiedEmail.ts b/src/utils/verifiedEmail.ts new file mode 100644 index 0000000..3da105e --- /dev/null +++ b/src/utils/verifiedEmail.ts @@ -0,0 +1,20 @@ +type VerifiedEmailVisibilityOptions = { + verifiedAt: string | null; + persistedEmail: string; + currentEmail: string; + isEditing: boolean; +}; + +export function shouldShowVerifiedEmailBadge(options: VerifiedEmailVisibilityOptions): boolean { + const { verifiedAt, persistedEmail, currentEmail, isEditing } = options; + + if (!verifiedAt) { + return false; + } + + if (!isEditing) { + return true; + } + + return persistedEmail.trim() === currentEmail.trim(); +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..31cff07 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist", + "declarationMap": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d04ea12 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": false, + "noEmit": true, + "types": ["react"] + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..794cb65 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'node:path'; + +export default defineConfig({ + plugins: [react()], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'PanicCore', + formats: ['es'], + fileName: () => 'index.js' + }, + rollupOptions: { + external: ['react'] + } + } +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d426e2a --- /dev/null +++ b/yarn.lock @@ -0,0 +1,769 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.29.0": + version "7.29.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-plugin-utils@^7.27.1": + version "7.28.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.27.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.27.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0": + version "7.29.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@esbuild/aix-ppc64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" + integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== + +"@esbuild/android-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8" + integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== + +"@esbuild/android-arm@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b" + integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== + +"@esbuild/android-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac" + integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== + +"@esbuild/darwin-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" + integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== + +"@esbuild/darwin-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a" + integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== + +"@esbuild/freebsd-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b" + integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== + +"@esbuild/freebsd-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead" + integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== + +"@esbuild/linux-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6" + integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== + +"@esbuild/linux-arm@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11" + integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== + +"@esbuild/linux-ia32@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29" + integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== + +"@esbuild/linux-loong64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed" + integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== + +"@esbuild/linux-mips64el@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1" + integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== + +"@esbuild/linux-ppc64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78" + integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== + +"@esbuild/linux-riscv64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d" + integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== + +"@esbuild/linux-s390x@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d" + integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== + +"@esbuild/linux-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5" + integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== + +"@esbuild/netbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7" + integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== + +"@esbuild/netbsd-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b" + integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== + +"@esbuild/openbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5" + integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== + +"@esbuild/openbsd-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b" + integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== + +"@esbuild/openharmony-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e" + integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== + +"@esbuild/sunos-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537" + integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== + +"@esbuild/win32-arm64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e" + integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== + +"@esbuild/win32-ia32@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c" + integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== + +"@esbuild/win32-x64@0.27.3": + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17" + integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@rolldown/pluginutils@1.0.0-rc.3": + version "1.0.0-rc.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz#8a88cc92a0f741befc7bc109cb1a4c6b9408e1c5" + integrity sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q== + +"@rollup/rollup-android-arm-eabi@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz#a6742c74c7d9d6d604ef8a48f99326b4ecda3d82" + integrity sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg== + +"@rollup/rollup-android-arm64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz#97247be098de4df0c11971089fd2edf80a5da8cf" + integrity sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q== + +"@rollup/rollup-darwin-arm64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz#674852cf14cf11b8056e0b1a2f4e872b523576cf" + integrity sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg== + +"@rollup/rollup-darwin-x64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz#36dfd7ed0aaf4d9d89d9ef983af72632455b0246" + integrity sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w== + +"@rollup/rollup-freebsd-arm64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz#2f87c2074b4220260fdb52a9996246edfc633c22" + integrity sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA== + +"@rollup/rollup-freebsd-x64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz#9b5a26522a38a95dc06616d1939d4d9a76937803" + integrity sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg== + +"@rollup/rollup-linux-arm-gnueabihf@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz#86aa4859385a8734235b5e40a48e52d770758c3a" + integrity sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw== + +"@rollup/rollup-linux-arm-musleabihf@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz#cbe70e56e6ece8dac83eb773b624fc9e5a460976" + integrity sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA== + +"@rollup/rollup-linux-arm64-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz#d14992a2e653bc3263d284bc6579b7a2890e1c45" + integrity sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA== + +"@rollup/rollup-linux-arm64-musl@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz#2fdd1ddc434ea90aeaa0851d2044789b4d07f6da" + integrity sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA== + +"@rollup/rollup-linux-loong64-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz#8a181e6f89f969f21666a743cd411416c80099e7" + integrity sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg== + +"@rollup/rollup-linux-loong64-musl@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz#904125af2babc395f8061daa27b5af1f4e3f2f78" + integrity sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q== + +"@rollup/rollup-linux-ppc64-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz#a57970ac6864c9a3447411a658224bdcf948be22" + integrity sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA== + +"@rollup/rollup-linux-ppc64-musl@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz#bb84de5b26870567a4267666e08891e80bb56a63" + integrity sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA== + +"@rollup/rollup-linux-riscv64-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz#72d00d2c7fb375ce3564e759db33f17a35bffab9" + integrity sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg== + +"@rollup/rollup-linux-riscv64-musl@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz#4c166ef58e718f9245bd31873384ba15a5c1a883" + integrity sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg== + +"@rollup/rollup-linux-s390x-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz#bb5025cde9a61db478c2ca7215808ad3bce73a09" + integrity sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w== + +"@rollup/rollup-linux-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz#9b66b1f9cd95c6624c788f021c756269ffed1552" + integrity sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg== + +"@rollup/rollup-linux-x64-musl@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz#b007ca255dc7166017d57d7d2451963f0bd23fd9" + integrity sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg== + +"@rollup/rollup-openbsd-x64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz#e8b357b2d1aa2c8d76a98f5f0d889eabe93f4ef9" + integrity sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ== + +"@rollup/rollup-openharmony-arm64@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz#96c2e3f4aacd3d921981329831ff8dde492204dc" + integrity sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA== + +"@rollup/rollup-win32-arm64-msvc@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz#2d865149d706d938df8b4b8f117e69a77646d581" + integrity sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A== + +"@rollup/rollup-win32-ia32-msvc@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz#abe1593be0fa92325e9971c8da429c5e05b92c36" + integrity sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA== + +"@rollup/rollup-win32-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz#c4af3e9518c9a5cd4b1c163dc81d0ad4d82e7eab" + integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA== + +"@rollup/rollup-win32-x64-msvc@4.59.0": + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c" + integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/estree@1.0.8": + version "1.0.8" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/react@^19.0.0": + version "19.2.14" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" + integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== + dependencies: + csstype "^3.2.2" + +"@vitejs/plugin-react@^5.0.0": + version "5.1.4" + resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz#5b477e060bf612a7394c4febacc5de33a219b0e4" + integrity sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA== + dependencies: + "@babel/core" "^7.29.0" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-rc.3" + "@types/babel__core" "^7.20.5" + react-refresh "^0.18.0" + +baseline-browser-mapping@^2.9.0: + version "2.10.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" + integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== + +browserslist@^4.24.0: + version "4.28.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" + integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== + dependencies: + baseline-browser-mapping "^2.9.0" + caniuse-lite "^1.0.30001759" + electron-to-chromium "^1.5.263" + node-releases "^2.0.27" + update-browserslist-db "^1.2.0" + +caniuse-lite@^1.0.30001759: + version "1.0.30001772" + resolved "https://nexus.beatrice.wtf/repository/npm-group/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz#aa8a176eba0006e78c965a8215c7a1ceb030122d" + integrity sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +csstype@^3.2.2: + version "3.2.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +debug@^4.1.0, debug@^4.3.1: + version "4.4.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +electron-to-chromium@^1.5.263: + version "1.5.302" + resolved "https://nexus.beatrice.wtf/repository/npm-group/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz#032a5802b31f7119269959c69fe2015d8dad5edb" + integrity sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg== + +esbuild@^0.27.0: + version "0.27.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" + integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://nexus.beatrice.wtf/repository/npm-group/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json5@^2.2.3: + version "2.2.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +ms@^2.1.3: + version "2.1.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://nexus.beatrice.wtf/repository/npm-group/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +node-releases@^2.0.27: + version "2.0.27" + resolved "https://nexus.beatrice.wtf/repository/npm-group/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.3: + version "4.0.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +postcss@^8.5.6: + version "8.5.6" + resolved "https://nexus.beatrice.wtf/repository/npm-group/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +react-dom@^19.0.0: + version "19.2.4" + resolved "https://nexus.beatrice.wtf/repository/npm-group/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" + integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== + dependencies: + scheduler "^0.27.0" + +react-refresh@^0.18.0: + version "0.18.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" + integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== + +react@^19.0.0: + version "19.2.4" + resolved "https://nexus.beatrice.wtf/repository/npm-group/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" + integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== + +rollup@^4.43.0: + version "4.59.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f" + integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.59.0" + "@rollup/rollup-android-arm64" "4.59.0" + "@rollup/rollup-darwin-arm64" "4.59.0" + "@rollup/rollup-darwin-x64" "4.59.0" + "@rollup/rollup-freebsd-arm64" "4.59.0" + "@rollup/rollup-freebsd-x64" "4.59.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" + "@rollup/rollup-linux-arm-musleabihf" "4.59.0" + "@rollup/rollup-linux-arm64-gnu" "4.59.0" + "@rollup/rollup-linux-arm64-musl" "4.59.0" + "@rollup/rollup-linux-loong64-gnu" "4.59.0" + "@rollup/rollup-linux-loong64-musl" "4.59.0" + "@rollup/rollup-linux-ppc64-gnu" "4.59.0" + "@rollup/rollup-linux-ppc64-musl" "4.59.0" + "@rollup/rollup-linux-riscv64-gnu" "4.59.0" + "@rollup/rollup-linux-riscv64-musl" "4.59.0" + "@rollup/rollup-linux-s390x-gnu" "4.59.0" + "@rollup/rollup-linux-x64-gnu" "4.59.0" + "@rollup/rollup-linux-x64-musl" "4.59.0" + "@rollup/rollup-openbsd-x64" "4.59.0" + "@rollup/rollup-openharmony-arm64" "4.59.0" + "@rollup/rollup-win32-arm64-msvc" "4.59.0" + "@rollup/rollup-win32-ia32-msvc" "4.59.0" + "@rollup/rollup-win32-x64-gnu" "4.59.0" + "@rollup/rollup-win32-x64-msvc" "4.59.0" + fsevents "~2.3.2" + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://nexus.beatrice.wtf/repository/npm-group/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== + +semver@^6.3.1: + version "6.3.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + +typescript@^5.6.2: + version "5.9.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +update-browserslist-db@^1.2.0: + version "1.2.3" + resolved "https://nexus.beatrice.wtf/repository/npm-group/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +vite@^7.0.0: + version "7.3.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" + integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== + dependencies: + esbuild "^0.27.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" + optionalDependencies: + fsevents "~2.3.3" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://nexus.beatrice.wtf/repository/npm-group/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==