diff --git a/apps/web/package.json b/apps/web/package.json index bd3648a..1f21a82 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,6 +27,8 @@ "@ai-sdk/openai": "3.0.7", "@modelcontextprotocol/sdk": "^1.25.2", "@prisma/client": "^6.19.1", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@tpmjs/db": "workspace:*", "@tpmjs/env": "workspace:*", "@tpmjs/npm-client": "workspace:*", @@ -57,7 +59,9 @@ "remark-gfm": "^4.0.1", "resend": "^6.7.0", "sonner": "^2.0.7", + "swr": "^2.2.5", "streamdown": "^1.6.11", + "three": "^0.182.0", "zod": "^4.3.5" }, "devDependencies": { @@ -68,6 +72,7 @@ "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@types/three": "^0.182.0", "autoprefixer": "^10.4.23", "dotenv": "^17.2.3", "eslint": "^9.39.2", diff --git a/apps/web/src/app/about/page.tsx b/apps/web/src/app/about/page.tsx new file mode 100644 index 0000000..3eabc68 --- /dev/null +++ b/apps/web/src/app/about/page.tsx @@ -0,0 +1,57 @@ +import { Container } from '@tpmjs/ui/Container/Container'; +import { Icon } from '@tpmjs/ui/Icon/Icon'; +import type { Metadata } from 'next'; +import { AppHeader } from '~/components/AppHeader'; + +export const metadata: Metadata = { + title: 'About', + description: 'About TPMJS and its creator', +}; + +export default function AboutPage(): React.ReactElement { + return ( +
+ + + +

About TPMJS

+ +
+

+ TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools. + It was started in 2024 to make it easy for developers to publish and discover tools + that AI agents can use. +

+ +

Creator

+ +

+ TPMJS was created by Ajax Davis (Thomas Davis). +

+ +
+ + + @ajaxdavis + + + + + ajaxdavis.dev + +
+
+
+
+ ); +} diff --git a/apps/web/src/app/agents/page.tsx b/apps/web/src/app/agents/page.tsx index 538e941..26f078d 100644 --- a/apps/web/src/app/agents/page.tsx +++ b/apps/web/src/app/agents/page.tsx @@ -9,30 +9,12 @@ import { LoadingState } from '@tpmjs/ui/LoadingState/LoadingState'; import { PageHeader } from '@tpmjs/ui/PageHeader/PageHeader'; import { Select } from '@tpmjs/ui/Select/Select'; import Link from 'next/link'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { TableVirtuoso } from 'react-virtuoso'; import { AppHeader } from '~/components/AppHeader'; import { CopyDropdown, getAgentCopyOptions } from '~/components/CopyDropdown'; import { LikeButton } from '~/components/LikeButton'; - -interface PublicAgent { - id: string; - uid: string; - name: string; - description: string | null; - provider: string; - modelId: string; - likeCount: number; - toolCount: number; - collectionCount: number; - createdAt: string; - createdBy: { - id: string; - name: string; - image: string | null; - username: string | null; - }; -} +import { type PublicAgent, useAgents } from '~/hooks/useAgents'; type SortOption = 'likes' | 'recent' | 'tools'; @@ -57,68 +39,24 @@ function truncateText(text: string, maxLength: number): string { } export default function PublicAgentsPage(): React.ReactElement { - const [agents, setAgents] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [hasMore, setHasMore] = useState(false); const [search, setSearch] = useState(''); const [sort, setSort] = useState('likes'); - const loadingMore = useRef(false); - const fetchAgents = useCallback( - async (offset: number, resetList = false) => { - try { - if (loadingMore.current && !resetList) return; - loadingMore.current = true; + // Fetch agents using SWR + const { data, isLoading, error: swrError, mutate } = useAgents({ sort }); - const params = new URLSearchParams({ - limit: '100', - offset: String(offset), - sort, - }); + const agents = data?.agents ?? []; + const hasMore = data?.pagination.hasMore ?? false; + const error = swrError?.message ?? null; - const response = await fetch(`/api/public/agents?${params}`); - const data = await response.json(); - - if (data.success) { - if (resetList || offset === 0) { - setAgents(data.data); - } else { - setAgents((prev) => [...prev, ...data.data]); - } - setHasMore(data.pagination.hasMore); - } else { - setError(data.error?.message || 'Failed to fetch agents'); - } - } catch (err) { - console.error('Failed to fetch agents:', err); - setError('Failed to fetch agents'); - } finally { - setIsLoading(false); - loadingMore.current = false; - } - }, - [sort] - ); - - useEffect(() => { - setIsLoading(true); - fetchAgents(0, true); - }, [fetchAgents]); - - const loadMore = useCallback(() => { - if (!hasMore || loadingMore.current) return; - fetchAgents(agents.length); - }, [hasMore, agents.length, fetchAgents]); - - // Filter and sort agents + // Filter and sort agents (client-side search) const filteredAgents = useMemo(() => { let result = agents; if (search) { const query = search.toLowerCase(); result = result.filter( - (a) => + (a: PublicAgent) => a.name.toLowerCase().includes(query) || a.description?.toLowerCase().includes(query) || a.provider.toLowerCase().includes(query) @@ -253,7 +191,7 @@ export default function PublicAgentsPage(): React.ReactElement { {/* Content */} {error ? ( - fetchAgents(0, true)} /> + mutate()} /> ) : isLoading ? ( ) : filteredAgents.length === 0 ? ( @@ -269,7 +207,6 @@ export default function PublicAgentsPage(): React.ReactElement { style={{ height: 'calc(100vh - 350px)', minHeight: '400px' }} data={filteredAgents} overscan={30} - endReached={loadMore} fixedHeaderContent={TableHeader} itemContent={TableRow} components={{ diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index f92a486..cbf14a1 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,6 +5,7 @@ import Script from 'next/script'; import { Toaster } from 'sonner'; import { AppFooter } from '../components/AppFooter'; import { ThemeProvider } from '../components/providers/ThemeProvider'; +import { SWRProvider } from '../components/SWRProvider'; import './globals.css'; const spaceGrotesk = Space_Grotesk({ @@ -157,11 +158,13 @@ export default function RootLayout({ enableSystem={true} disableTransitionOnChange={false} > -
-
{children}
- -
- + +
+
{children}
+ +
+ +
diff --git a/apps/web/src/app/tool/tool-search/page.tsx b/apps/web/src/app/tool/tool-search/page.tsx index 696dbf6..5fb3ab8 100644 --- a/apps/web/src/app/tool/tool-search/page.tsx +++ b/apps/web/src/app/tool/tool-search/page.tsx @@ -11,7 +11,7 @@ import { LoadingState } from '@tpmjs/ui/LoadingState/LoadingState'; import { PageHeader } from '@tpmjs/ui/PageHeader/PageHeader'; import { Select } from '@tpmjs/ui/Select/Select'; import Link from 'next/link'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { TableVirtuoso } from 'react-virtuoso'; import { AppHeader } from '~/components/AppHeader'; import { CopyButton } from '~/components/CopyButton'; @@ -21,26 +21,7 @@ import { getInstallCommand, usePackageManager, } from '~/components/PackageManagerSelector'; - -interface Tool { - id: string; - name: string; - description: string; - qualityScore: string; - likeCount?: number; - importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; - executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; - createdAt: string; - package: { - npmPackageName: string; - npmVersion: string; - npmPublishedAt: string; - category: string; - npmRepository: { url: string; type: string } | null; - isOfficial: boolean; - npmDownloadsLastMonth: number; - }; -} +import { type Tool, useTools } from '~/hooks/useTools'; type SortOption = 'downloads' | 'likes' | 'recent' | 'name'; @@ -97,60 +78,28 @@ export default function ToolSearchPage(): React.ReactElement { const [categoryFilter, setCategoryFilter] = useState('all'); const [healthFilter, setHealthFilter] = useState('all'); const [sortBy, setSortBy] = useState('downloads'); - const [tools, setTools] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [availableCategories, setAvailableCategories] = useState([]); const [packageManager, setPackageManager] = usePackageManager(); - // Fetch tools from API - useEffect(() => { - const fetchTools = async () => { - try { - setLoading(true); - const params = new URLSearchParams(); + // Fetch tools from API using SWR + const { data: tools = [], isLoading: loading, error: swrError } = useTools({ + category: categoryFilter !== 'all' ? categoryFilter : undefined, + importHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined, + executionHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined, + broken: healthFilter === 'broken' ? true : undefined, + }); - if (categoryFilter !== 'all') { - params.set('category', categoryFilter); - } + const error = swrError?.message ?? null; - if (healthFilter === 'healthy') { - params.set('importHealth', 'HEALTHY'); - params.set('executionHealth', 'HEALTHY'); - } else if (healthFilter === 'broken') { - params.set('broken', 'true'); - } - - // Fetch all tools (no pagination limit) - params.set('limit', '1000'); - const toolsResponse = await fetch(`/api/tools?${params.toString()}`); - const toolsData = await toolsResponse.json(); - - if (toolsData.success) { - const fetchedTools = toolsData.data; - setTools(fetchedTools); - setError(null); - - // Extract unique categories from all tools - const categories = new Set(); - for (const tool of fetchedTools) { - if (tool.package.category) { - categories.add(tool.package.category); - } - } - setAvailableCategories(Array.from(categories).sort()); - } else { - setError(toolsData.error || 'Failed to fetch tools'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); + // Extract unique categories from all tools + const availableCategories = useMemo(() => { + const categories = new Set(); + for (const tool of tools) { + if (tool.package.category) { + categories.add(tool.package.category); } - }; - - fetchTools(); - }, [categoryFilter, healthFilter]); + } + return Array.from(categories).sort(); + }, [tools]); // Filter and sort tools const filteredTools = useMemo(() => { @@ -160,7 +109,7 @@ export default function ToolSearchPage(): React.ReactElement { if (searchQuery) { const query = searchQuery.toLowerCase(); result = result.filter( - (tool) => + (tool: Tool) => tool.name.toLowerCase().includes(query) || tool.package.npmPackageName.toLowerCase().includes(query) || tool.description.toLowerCase().includes(query) diff --git a/apps/web/src/components/BundleSize.tsx b/apps/web/src/components/BundleSize.tsx index 47bbebf..77f1d78 100644 --- a/apps/web/src/components/BundleSize.tsx +++ b/apps/web/src/components/BundleSize.tsx @@ -1,15 +1,7 @@ 'use client'; import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card'; -import { useEffect, useState } from 'react'; - -interface BundleSizeData { - name: string; - version: string; - size: number; - gzip: number; - dependencyCount: number; -} +import { useBundleSize } from '~/hooks/useBundleSize'; interface BundleSizeProps { packageName: string; @@ -25,42 +17,10 @@ function formatBytes(bytes: number): string { } export function BundleSize({ packageName, version }: BundleSizeProps): React.ReactElement | null { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const { data, isLoading: loading, error } = useBundleSize(packageName, version); - useEffect(() => { - const fetchBundleSize = async () => { - setLoading(true); - setError(null); - - try { - const params = new URLSearchParams({ package: packageName, version }); - const response = await fetch(`/api/bundlephobia?${params}`); - - if (!response.ok) { - if (response.status === 404) { - setError('not-found'); - } else { - setError('failed'); - } - return; - } - - const result = await response.json(); - setData(result); - } catch { - setError('failed'); - } finally { - setLoading(false); - } - }; - - fetchBundleSize(); - }, [packageName, version]); - - // Don't render anything if package not found (common for scoped packages) - if (error === 'not-found') { + // Don't render anything if package not found (common for scoped packages) or error + if (error || (!loading && !data)) { return null; } @@ -80,7 +40,7 @@ export function BundleSize({ packageName, version }: BundleSizeProps): React.Rea ); } - if (error || !data) { + if (!data) { return null; } diff --git a/apps/web/src/components/LikeButton.tsx b/apps/web/src/components/LikeButton.tsx index 8ef845e..537ee68 100644 --- a/apps/web/src/components/LikeButton.tsx +++ b/apps/web/src/components/LikeButton.tsx @@ -3,7 +3,8 @@ import { useSession } from '@/lib/auth-client'; import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; +import { useLikeStatus } from '~/hooks/useLikeStatus'; export type LikeEntityType = 'tool' | 'collection' | 'agent'; @@ -31,41 +32,19 @@ export function LikeButton({ onLikeChange, }: LikeButtonProps): React.ReactElement { const { data: session } = useSession(); - const [liked, setLiked] = useState(initialLiked); - const [count, setCount] = useState(initialCount); - const [isLoading, setIsLoading] = useState(false); - const [hasFetched, setHasFetched] = useState(false); const [showTooltip, setShowTooltip] = useState(false); + const [isLoading, setIsLoading] = useState(false); - // Fetch initial like status when user is logged in - useEffect(() => { - if (!session || hasFetched) return; + // Use SWR for like status with optimistic updates + const { data, toggleLike } = useLikeStatus( + entityType, + entityId, + !!session, + { liked: initialLiked, likeCount: initialCount } + ); - const fetchLikeStatus = async () => { - try { - const response = await fetch(`/api/${entityType}s/${entityId}/like`); - const data = await response.json(); - if (data.success) { - setLiked(data.data.liked); - setCount(data.data.likeCount); - } - } catch (error) { - console.error('Failed to fetch like status:', error); - } finally { - setHasFetched(true); - } - }; - - fetchLikeStatus(); - }, [session, entityType, entityId, hasFetched]); - - // Update from props when they change - useEffect(() => { - if (!hasFetched) { - setLiked(initialLiked); - setCount(initialCount); - } - }, [initialLiked, initialCount, hasFetched]); + const liked = data?.liked ?? initialLiked; + const count = data?.likeCount ?? initialCount; const handleClick = useCallback(async () => { if (!session) { @@ -77,40 +56,17 @@ export function LikeButton({ if (isLoading) return; - // Optimistic update - const newLiked = !liked; - const newCount = newLiked ? count + 1 : Math.max(0, count - 1); - setLiked(newLiked); - setCount(newCount); - setIsLoading(true); - try { - const response = await fetch(`/api/${entityType}s/${entityId}/like`, { - method: newLiked ? 'POST' : 'DELETE', - }); - - const data = await response.json(); - - if (data.success) { - // Update with server values - setLiked(data.data.liked); - setCount(data.data.likeCount); - onLikeChange?.(data.data.liked, data.data.likeCount); - } else { - // Revert on error - setLiked(!newLiked); - setCount(liked ? count : Math.max(0, count - 1)); + await toggleLike(); + // Call the onLikeChange callback if provided + if (onLikeChange && data) { + onLikeChange(!data.liked, data.liked ? data.likeCount - 1 : data.likeCount + 1); } - } catch (error) { - console.error('Failed to toggle like:', error); - // Revert on error - setLiked(!newLiked); - setCount(liked ? count : Math.max(0, count - 1)); } finally { setIsLoading(false); } - }, [session, liked, count, isLoading, entityType, entityId, onLikeChange]); + }, [session, isLoading, toggleLike, onLikeChange, data]); return (
diff --git a/apps/web/src/components/SWRProvider.tsx b/apps/web/src/components/SWRProvider.tsx new file mode 100644 index 0000000..3ba560f --- /dev/null +++ b/apps/web/src/components/SWRProvider.tsx @@ -0,0 +1,23 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { SWRConfig } from 'swr'; +import { fetcher } from '~/lib/swr/fetcher'; + +interface SWRProviderProps { + children: ReactNode; +} + +export function SWRProvider({ children }: SWRProviderProps): React.ReactElement { + return ( + + {children} + + ); +} diff --git a/apps/web/src/hooks/useActivity.ts b/apps/web/src/hooks/useActivity.ts new file mode 100644 index 0000000..2737c38 --- /dev/null +++ b/apps/web/src/hooks/useActivity.ts @@ -0,0 +1,91 @@ +import useSWRInfinite from 'swr/infinite'; + +export interface Activity { + id: string; + type: string; + metadata: Record; + createdAt: string; + tool?: { + name: string; + package: { npmPackageName: string }; + }; + agent?: { + name: string; + uid: string; + }; + collection?: { + name: string; + uid: string; + }; +} + +interface ActivityResponse { + activities: Activity[]; + pagination: { + nextCursor: string | null; + }; +} + +/** + * Infinite scroll hook for user activity stream + */ +export function useActivity(limit = 20) { + const getKey = (pageIndex: number, previousPageData: ActivityResponse | null) => { + // Reached the end + if (previousPageData && !previousPageData.pagination.nextCursor) { + return null; + } + + // First page + if (pageIndex === 0) { + return `/api/user/activity?limit=${limit}`; + } + + // Next pages + const cursor = previousPageData?.pagination.nextCursor; + return `/api/user/activity?limit=${limit}&cursor=${cursor}`; + }; + + const { data, error, isLoading, isValidating, size, setSize, mutate } = + useSWRInfinite( + getKey, + async (url: string) => { + const res = await fetch(url); + const json = await res.json(); + + if (!json.success) { + throw new Error(json.error || 'Failed to fetch activity'); + } + + return { + activities: json.data, + pagination: json.pagination, + }; + }, + { + revalidateFirstPage: false, + } + ); + + // Flatten all activities from all pages + const activities = data ? data.flatMap((page: ActivityResponse) => page.activities) : []; + + // Check if there are more pages + const hasMore = data ? data[data.length - 1]?.pagination.nextCursor !== null : false; + + const loadMore = () => { + if (hasMore && !isValidating) { + setSize(size + 1); + } + }; + + return { + activities, + isLoading, + isLoadingMore: isValidating && data && data.length > 0, + error, + hasMore, + loadMore, + mutate, + }; +} diff --git a/apps/web/src/hooks/useAgents.ts b/apps/web/src/hooks/useAgents.ts new file mode 100644 index 0000000..5293923 --- /dev/null +++ b/apps/web/src/hooks/useAgents.ts @@ -0,0 +1,66 @@ +import useSWR from 'swr'; + +export interface PublicAgent { + id: string; + uid: string; + name: string; + description: string | null; + provider: string; + modelId: string; + likeCount: number; + toolCount: number; + collectionCount: number; + createdAt: string; + createdBy: { + id: string; + name: string; + image: string | null; + username: string | null; + }; +} + +export interface UseAgentsParams { + sort?: 'likes' | 'recent' | 'tools'; + limit?: number; + offset?: number; +} + +interface AgentsResponse { + agents: PublicAgent[]; + pagination: { + hasMore: boolean; + }; +} + +/** + * Fetch public agents + */ +export function useAgents(params: UseAgentsParams = {}) { + const searchParams = new URLSearchParams(); + + searchParams.set('limit', String(params.limit ?? 100)); + searchParams.set('offset', String(params.offset ?? 0)); + if (params.sort) { + searchParams.set('sort', params.sort); + } + + const queryString = searchParams.toString(); + + // Custom response handler since the API returns { success, data, pagination } + return useSWR( + `/api/public/agents?${queryString}`, + async (url: string) => { + const res = await fetch(url); + const json = await res.json(); + + if (!json.success) { + throw new Error(json.error?.message || 'Failed to fetch agents'); + } + + return { + agents: json.data, + pagination: json.pagination, + }; + } + ); +} diff --git a/apps/web/src/hooks/useBundleSize.ts b/apps/web/src/hooks/useBundleSize.ts new file mode 100644 index 0000000..4d93619 --- /dev/null +++ b/apps/web/src/hooks/useBundleSize.ts @@ -0,0 +1,31 @@ +import useSWR from 'swr'; + +export interface BundleSizeData { + name: string; + version: string; + size: number; + gzip: number; + dependencyCount: number; +} + +/** + * Fetch bundle size data from bundlephobia API + * Returns null for the key if packageName is not provided (disables fetching) + */ +export function useBundleSize(packageName: string | undefined, version?: string) { + const params = new URLSearchParams(); + if (packageName) { + params.set('package', packageName); + if (version) { + params.set('version', version); + } + } + + return useSWR( + packageName ? `/api/bundlephobia?${params.toString()}` : null, + { + // Don't retry on 404s (common for scoped packages) + shouldRetryOnError: false, + } + ); +} diff --git a/apps/web/src/hooks/useCollections.ts b/apps/web/src/hooks/useCollections.ts new file mode 100644 index 0000000..b6b23b6 --- /dev/null +++ b/apps/web/src/hooks/useCollections.ts @@ -0,0 +1,64 @@ +import useSWR from 'swr'; + +export interface PublicCollection { + id: string; + uid: string; + name: string; + description: string | null; + likeCount: number; + forkCount: number; + toolCount: number; + createdAt: string; + createdBy: { + id: string; + name: string; + image: string | null; + username: string | null; + }; +} + +export interface UseCollectionsParams { + sort?: 'likes' | 'recent' | 'tools'; + limit?: number; + offset?: number; +} + +interface CollectionsResponse { + collections: PublicCollection[]; + pagination: { + hasMore: boolean; + }; +} + +/** + * Fetch public collections + */ +export function useCollections(params: UseCollectionsParams = {}) { + const searchParams = new URLSearchParams(); + + searchParams.set('limit', String(params.limit ?? 100)); + searchParams.set('offset', String(params.offset ?? 0)); + if (params.sort) { + searchParams.set('sort', params.sort); + } + + const queryString = searchParams.toString(); + + // Custom response handler since the API returns { success, data, pagination } + return useSWR( + `/api/public/collections?${queryString}`, + async (url: string) => { + const res = await fetch(url); + const json = await res.json(); + + if (!json.success) { + throw new Error(json.error?.message || 'Failed to fetch collections'); + } + + return { + collections: json.data, + pagination: json.pagination, + }; + } + ); +} diff --git a/apps/web/src/hooks/useLikeStatus.ts b/apps/web/src/hooks/useLikeStatus.ts new file mode 100644 index 0000000..059abda --- /dev/null +++ b/apps/web/src/hooks/useLikeStatus.ts @@ -0,0 +1,76 @@ +import useSWR, { type KeyedMutator } from 'swr'; +import type { LikeEntityType } from '~/components/LikeButton'; + +export interface LikeStatusData { + liked: boolean; + likeCount: number; +} + +interface UseLikeStatusReturn { + data: LikeStatusData | undefined; + isLoading: boolean; + error: Error | undefined; + mutate: KeyedMutator; + toggleLike: () => Promise; +} + +/** + * Hook for managing like status with optimistic updates + * @param entityType - 'tool' | 'collection' | 'agent' + * @param entityId - The ID of the entity + * @param enabled - Whether to enable fetching (typically based on session) + * @param initialData - Optional initial data to use before fetch completes + */ +export function useLikeStatus( + entityType: LikeEntityType, + entityId: string, + enabled: boolean, + initialData?: LikeStatusData +): UseLikeStatusReturn { + const key = enabled ? `/api/${entityType}s/${entityId}/like` : null; + + const { data, error, isLoading, mutate } = useSWR(key, { + fallbackData: initialData, + revalidateOnFocus: false, + }); + + const toggleLike = async () => { + if (!data) return; + + const newLiked = !data.liked; + const newCount = newLiked ? data.likeCount + 1 : Math.max(0, data.likeCount - 1); + + // Optimistic update + await mutate( + { liked: newLiked, likeCount: newCount }, + { revalidate: false } + ); + + try { + const response = await fetch(`/api/${entityType}s/${entityId}/like`, { + method: newLiked ? 'POST' : 'DELETE', + }); + + const result = await response.json(); + + if (result.success) { + // Update with server values + await mutate(result.data, { revalidate: false }); + } else { + // Revert on error + await mutate(data, { revalidate: false }); + } + } catch { + // Revert on error + await mutate(data, { revalidate: false }); + } + }; + + return { + data, + isLoading, + error, + mutate, + toggleLike, + }; +} diff --git a/apps/web/src/hooks/useStats.ts b/apps/web/src/hooks/useStats.ts new file mode 100644 index 0000000..1da4f24 --- /dev/null +++ b/apps/web/src/hooks/useStats.ts @@ -0,0 +1,61 @@ +import useSWR from 'swr'; + +export interface PlatformStats { + packages: number; + tools: number; + agents: number; + collections: number; + executions: number; + users: number; + categories: { category: string; _count: { _all: number } }[]; + healthyTools: number; + brokenTools: number; +} + +export interface ExecutionStats { + total: number; + successRate: number; + avgDurationMs: number; + dailyExecutions: { date: string; count: number }[]; +} + +export interface HistorySnapshot { + date: string; + data: { + packages?: number; + tools?: number; + agents?: number; + collections?: number; + users?: number; + executions?: number; + }; +} + +/** + * Fetch platform statistics + * Returns multiple SWR hooks for parallel fetching + */ +export function useStats() { + const stats = useSWR('/api/stats'); + const executions = useSWR('/api/stats/executions'); + const history = useSWR('/api/sync/stats-snapshot?days=90'); + + return { + stats: { + data: stats.data, + isLoading: stats.isLoading, + error: stats.error, + }, + executions: { + data: executions.data, + isLoading: executions.isLoading, + error: executions.error, + }, + history: { + data: history.data, + isLoading: history.isLoading, + error: history.error, + }, + isLoading: stats.isLoading || executions.isLoading || history.isLoading, + }; +} diff --git a/apps/web/src/hooks/useTools.ts b/apps/web/src/hooks/useTools.ts new file mode 100644 index 0000000..8701146 --- /dev/null +++ b/apps/web/src/hooks/useTools.ts @@ -0,0 +1,51 @@ +import useSWR from 'swr'; + +export interface Tool { + id: string; + name: string; + description: string; + qualityScore: string; + likeCount?: number; + importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; + executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; + createdAt: string; + package: { + npmPackageName: string; + npmVersion: string; + npmPublishedAt: string; + category: string; + npmRepository: { url: string; type: string } | null; + isOfficial: boolean; + npmDownloadsLastMonth: number; + }; +} + +export interface UseToolsParams { + category?: string; + importHealth?: string; + executionHealth?: string; + broken?: boolean; + limit?: number; +} + +export function useTools(params: UseToolsParams = {}) { + const searchParams = new URLSearchParams(); + + if (params.category && params.category !== 'all') { + searchParams.set('category', params.category); + } + if (params.importHealth) { + searchParams.set('importHealth', params.importHealth); + } + if (params.executionHealth) { + searchParams.set('executionHealth', params.executionHealth); + } + if (params.broken) { + searchParams.set('broken', 'true'); + } + searchParams.set('limit', String(params.limit ?? 1000)); + + const queryString = searchParams.toString(); + + return useSWR(`/api/tools?${queryString}`); +} diff --git a/apps/web/src/lib/swr/fetcher.ts b/apps/web/src/lib/swr/fetcher.ts new file mode 100644 index 0000000..f308b6e --- /dev/null +++ b/apps/web/src/lib/swr/fetcher.ts @@ -0,0 +1,14 @@ +/** + * SWR fetcher that handles our API response format + * All API responses have { success: boolean, data: T, error?: string } + */ +export const fetcher = async (url: string): Promise => { + const res = await fetch(url); + const json = await res.json(); + + if (!json.success) { + throw new Error(json.error || 'Request failed'); + } + + return json.data; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36ee7f6..5a80620 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,6 +248,12 @@ importers: '@prisma/client': specifier: ^6.19.1 version: 6.19.1(prisma@6.19.1(typescript@5.9.3))(typescript@5.9.3) + '@react-three/drei': + specifier: ^10.7.7 + version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0))(@types/react@19.2.7)(@types/three@0.182.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0) + '@react-three/fiber': + specifier: ^9.5.0 + version: 9.5.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0) '@tpmjs/db': specifier: workspace:* version: link:../../packages/db @@ -341,6 +347,12 @@ importers: streamdown: specifier: ^1.6.11 version: 1.6.11(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.3) + swr: + specifier: ^2.2.5 + version: 2.3.8(react@19.2.3) + three: + specifier: ^0.182.0 + version: 0.182.0 zod: specifier: ^4.3.5 version: 4.3.5 @@ -366,6 +378,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) + '@types/three': + specifier: ^0.182.0 + version: 0.182.0 autoprefixer: specifier: ^10.4.23 version: 10.4.23(postcss@8.5.6) @@ -4328,6 +4343,9 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -5133,6 +5151,9 @@ packages: '@types/react': '>=16' react: '>=16' + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@mendable/firecrawl-js@4.10.0': resolution: {integrity: sha512-40qtKCVY3a1A4Y6t/m5Ar10HbzrWuyCNt7vR3uBh+j14GZC0JoxEkjaFRC00wBmPD9N5JMT4gmTXvzM/SI9enw==} engines: {node: '>=22.0.0'} @@ -5156,6 +5177,11 @@ packages: '@mongodb-js/saslprep@1.4.4': resolution: {integrity: sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==} + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + '@mozilla/readability@0.6.0': resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} @@ -5454,6 +5480,42 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@react-three/drei@10.7.7': + resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.5.0': + resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=19 <19.3' + react-dom: '>=19 <19.3' + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@redis/bloom@1.2.0': resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} peerDependencies: @@ -5892,6 +5954,9 @@ packages: '@total-typescript/ts-reset@0.6.1': resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -6026,6 +6091,9 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -6085,6 +6153,9 @@ packages: '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} @@ -6096,6 +6167,11 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} @@ -6111,9 +6187,15 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/three@0.182.0': + resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==} + '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -6138,6 +6220,9 @@ packages: '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -6307,6 +6392,14 @@ packages: '@upstash/redis@1.36.1': resolution: {integrity: sha512-N6SjDcgXdOcTAF+7uNoY69o7hCspe9BcA7YjQdxVu5d25avljTwyLaHBW3krWjrP0FfocgMk94qyVtQbeDp39A==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vercel/analytics@1.6.1': resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==} peerDependencies: @@ -6396,6 +6489,9 @@ packages: '@vitest/utils@4.0.16': resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -6766,6 +6862,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6808,6 +6907,12 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001763: resolution: {integrity: sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==} @@ -7014,6 +7119,11 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -7314,6 +7424,9 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -7377,6 +7490,9 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + drizzle-kit@0.31.8: resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} hasBin: true @@ -7830,6 +7946,12 @@ packages: picomatch: optional: true + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -8047,6 +8169,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -8169,6 +8294,9 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + hono@4.10.6: resolution: {integrity: sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==} engines: {node: '>=16.9.0'} @@ -8241,6 +8369,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -8411,6 +8542,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -8497,6 +8631,11 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -8720,6 +8859,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -8875,6 +9017,12 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -8978,6 +9126,14 @@ packages: mermaid@11.12.2: resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@0.22.0: + resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -9726,6 +9882,9 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -9778,6 +9937,9 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -9872,6 +10034,15 @@ packages: peerDependencies: react: '>= 0.14.0' + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react-virtuoso@4.18.1: resolution: {integrity: sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==} peerDependencies: @@ -10297,6 +10468,15 @@ packages: standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -10451,6 +10631,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + svix@1.84.1: resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} @@ -10500,6 +10685,19 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.182.0: + resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -10571,6 +10769,19 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -10637,6 +10848,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + turbo-darwin-64@2.7.3: resolution: {integrity: sha512-aZHhvRiRHXbJw1EcEAq4aws1hsVVUZ9DPuSFaq9VVFAKCup7niIEwc22glxb7240yYEr1vLafdQ2U294Vcwz+w==} cpu: [x64] @@ -10829,6 +11043,10 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -10985,6 +11203,12 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -11165,6 +11389,39 @@ packages: zod@4.3.5: resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.10: + resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11743,6 +12000,8 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@dimforge/rapier3d-compat@0.12.0': {} + '@drizzle-team/brocli@0.10.2': {} '@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)': @@ -12289,6 +12548,8 @@ snapshots: '@types/react': 19.2.7 react: 19.2.3 + '@mediapipe/tasks-vision@0.10.17': {} + '@mendable/firecrawl-js@4.10.0': dependencies: axios: 1.13.2 @@ -12330,6 +12591,11 @@ snapshots: dependencies: sparse-bitfield: 3.0.3 + '@monogrid/gainmap-js@3.4.0(three@0.182.0)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.182.0 + '@mozilla/readability@0.6.0': {} '@mrleebo/prisma-ast@0.12.1': @@ -12611,6 +12877,59 @@ snapshots: react-dom: 19.2.3(react@19.2.3) optional: true + '@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0))(@types/react@19.2.7)(@types/three@0.182.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.182.0) + '@react-three/fiber': 9.5.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0) + '@use-gesture/react': 10.3.1(react@19.2.3) + camera-controls: 3.1.2(three@0.182.0) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.15 + maath: 0.10.8(@types/three@0.182.0)(three@0.182.0) + meshline: 3.3.1(three@0.182.0) + react: 19.2.3 + stats-gl: 2.4.2(@types/three@0.182.0)(three@0.182.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.3) + three: 0.182.0 + three-mesh-bvh: 0.8.3(three@0.182.0) + three-stdlib: 2.36.1(three@0.182.0) + troika-three-text: 0.52.4(three@0.182.0) + tunnel-rat: 0.1.2(@types/react@19.2.7)(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + utility-types: 3.11.0 + zustand: 5.0.10(@types/react@19.2.7)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.5.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + react-use-measure: 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.3) + three: 0.182.0 + use-sync-external-store: 1.6.0(react@19.2.3) + zustand: 5.0.10(@types/react@19.2.7)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - immer + '@redis/bloom@1.2.0(@redis/client@1.6.1)': dependencies: '@redis/client': 1.6.1 @@ -13076,6 +13395,8 @@ snapshots: '@total-typescript/ts-reset@0.6.1': {} + '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -13246,6 +13567,8 @@ snapshots: '@types/doctrine@0.0.9': {} + '@types/draco3d@1.4.10': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -13304,6 +13627,8 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/offscreencanvas@2019.7.3': {} + '@types/papaparse@5.5.2': dependencies: '@types/node': 25.0.3 @@ -13314,6 +13639,10 @@ snapshots: dependencies: '@types/react': 19.2.7 + '@types/react-reconciler@0.28.9(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + '@types/react-syntax-highlighter@15.5.13': dependencies: '@types/react': 19.2.7 @@ -13328,8 +13657,20 @@ snapshots: '@types/semver@7.7.1': {} + '@types/stats.js@0.17.4': {} + '@types/statuses@2.0.6': {} + '@types/three@0.182.0': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.69 + fflate: 0.8.2 + meshoptimizer: 0.22.0 + '@types/tough-cookie@4.0.5': {} '@types/trusted-types@2.0.7': @@ -13347,6 +13688,8 @@ snapshots: '@types/webidl-conversions@7.0.3': {} + '@types/webxr@0.5.24': {} + '@types/whatwg-mimetype@3.0.2': {} '@types/whatwg-url@11.0.5': @@ -13513,6 +13856,13 @@ snapshots: dependencies: uncrypto: 0.1.3 + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.2.3)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.3 + '@vercel/analytics@1.6.1(next@16.1.1(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': optionalDependencies: next: 16.1.1(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -13606,6 +13956,8 @@ snapshots: '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 + '@webgpu/types@0.1.69': {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -13987,6 +14339,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.27.2): dependencies: esbuild: 0.27.2 @@ -14032,6 +14389,10 @@ snapshots: camelcase-css@2.0.1: {} + camera-controls@3.1.2(three@0.182.0): + dependencies: + three: 0.182.0 + caniuse-lite@1.0.30001763: {} caseless@0.12.0: {} @@ -14222,6 +14583,10 @@ snapshots: dependencies: layout-base: 2.0.1 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -14551,6 +14916,10 @@ snapshots: destr@2.0.5: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -14607,6 +14976,8 @@ snapshots: dotenv@17.2.3: {} + draco3d@1.5.7: {} + drizzle-kit@0.31.8: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -15261,6 +15632,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.6.10: {} + + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -15509,6 +15884,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + glsl-noise@0.0.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -15702,6 +16079,8 @@ snapshots: highlightjs-vue@1.0.0: {} + hls.js@1.6.15: {} + hono@4.10.6: {} html-encoding-sniffer@3.0.0: @@ -15799,6 +16178,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -15946,6 +16327,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} + is-promise@4.0.0: {} is-property@1.0.2: @@ -16035,6 +16418,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + its-fine@2.0.0(@types/react@19.2.7)(react@19.2.3): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.7) + react: 19.2.3 + transitivePeerDependencies: + - '@types/react' + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -16263,6 +16653,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.30.2: optional: true @@ -16383,6 +16777,11 @@ snapshots: lz-string@1.5.0: {} + maath@0.10.8(@types/three@0.182.0)(three@0.182.0): + dependencies: + '@types/three': 0.182.0 + three: 0.182.0 + magic-string@0.27.0: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -16605,6 +17004,12 @@ snapshots: ts-dedent: 2.2.0 uuid: 11.1.0 + meshline@3.3.1(three@0.182.0): + dependencies: + three: 0.182.0 + + meshoptimizer@0.22.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -17505,6 +17910,8 @@ snapshots: postgres@3.4.7: optional: true + potpack@1.0.2: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -17563,6 +17970,11 @@ snapshots: process@0.11.10: {} + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -17690,6 +18102,12 @@ snapshots: react: 19.2.3 refractor: 5.0.0 + react-use-measure@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react-virtuoso@4.18.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 @@ -18285,6 +18703,13 @@ snapshots: '@stablelib/base64': 1.0.1 fast-sha256: 1.3.0 + stats-gl@2.4.2(@types/three@0.182.0)(three@0.182.0): + dependencies: + '@types/three': 0.182.0 + three: 0.182.0 + + stats.js@0.17.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -18482,6 +18907,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@19.2.3): + dependencies: + react: 19.2.3 + svix@1.84.1: dependencies: standardwebhooks: 1.0.0 @@ -18555,6 +18984,22 @@ snapshots: dependencies: any-promise: 1.3.0 + three-mesh-bvh@0.8.3(three@0.182.0): + dependencies: + three: 0.182.0 + + three-stdlib@2.36.1(three@0.182.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.182.0 + + three@0.182.0: {} + throttleit@2.1.0: {} tiny-invariant@1.3.3: {} @@ -18609,6 +19054,20 @@ snapshots: trim-lines@3.0.1: {} + troika-three-text@0.52.4(three@0.182.0): + dependencies: + bidi-js: 1.0.3 + three: 0.182.0 + troika-three-utils: 0.52.4(three@0.182.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.182.0): + dependencies: + three: 0.182.0 + + troika-worker-utils@0.52.0: {} + trough@2.2.0: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -18687,6 +19146,14 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tunnel-rat@0.1.2(@types/react@19.2.7)(react@19.2.3): + dependencies: + zustand: 4.5.7(@types/react@19.2.7)(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - immer + - react + turbo-darwin-64@2.7.3: optional: true @@ -18933,6 +19400,8 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 + utility-types@3.11.0: {} + uuid@10.0.0: {} uuid@11.1.0: {} @@ -19054,6 +19523,10 @@ snapshots: web-namespaces@2.0.1: {} + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webidl-conversions@7.0.0: {} webidl-conversions@8.0.1: {} @@ -19223,4 +19696,17 @@ snapshots: zod@4.3.5: {} + zustand@4.5.7(@types/react@19.2.7)(react@19.2.3): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + react: 19.2.3 + + zustand@5.0.10(@types/react@19.2.7)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): + optionalDependencies: + '@types/react': 19.2.7 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + zwitch@2.0.4: {}