feat(web): implement SWR for instant data fetching

- Add SWR package and global SWRProvider
- Create reusable hooks: useTools, useAgents, useCollections,
  useStats, useLikeStatus, useBundleSize, useActivity
- Update tool-search page to use useTools hook
- Update agents page to use useAgents hook
- Update BundleSize component to use useBundleSize hook
- Update LikeButton with optimistic updates via useLikeStatus
- Add simple about page with creator info
This commit is contained in:
Ajax Davis 2026-01-16 11:42:55 +10:00
parent 7483e697fc
commit 0489f5cfdb
17 changed files with 1085 additions and 255 deletions

View file

@ -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",

View file

@ -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 (
<div className="min-h-screen bg-background">
<AppHeader />
<Container size="md" padding="lg" className="py-16">
<h1 className="text-3xl font-bold mb-8 text-foreground">About TPMJS</h1>
<div className="prose prose-neutral dark:prose-invert max-w-none">
<p className="text-lg text-foreground-secondary mb-6">
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.
</p>
<h2 className="text-xl font-semibold mt-10 mb-4 text-foreground">Creator</h2>
<p className="text-foreground-secondary mb-6">
TPMJS was created by <strong>Ajax Davis</strong> (Thomas Davis).
</p>
<div className="flex items-center gap-6">
<a
href="https://x.com/ajaxdavis"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-foreground-secondary hover:text-primary transition-colors"
>
<Icon icon="x" size="md" />
<span>@ajaxdavis</span>
</a>
<a
href="https://ajaxdavis.dev"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-foreground-secondary hover:text-primary transition-colors"
>
<Icon icon="globe" size="md" />
<span>ajaxdavis.dev</span>
</a>
</div>
</div>
</Container>
</div>
);
}

View file

@ -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<PublicAgent[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(false);
const [search, setSearch] = useState('');
const [sort, setSort] = useState<SortOption>('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 ? (
<ErrorState message={error} onRetry={() => fetchAgents(0, true)} />
<ErrorState message={error} onRetry={() => mutate()} />
) : isLoading ? (
<LoadingState message="Loading agents..." size="lg" />
) : 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={{

View file

@ -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}
>
<div className="flex flex-col min-h-screen">
<div className="flex-1">{children}</div>
<AppFooter />
</div>
<Toaster position="bottom-right" richColors closeButton />
<SWRProvider>
<div className="flex flex-col min-h-screen">
<div className="flex-1">{children}</div>
<AppFooter />
</div>
<Toaster position="bottom-right" richColors closeButton />
</SWRProvider>
</ThemeProvider>
<Analytics />
</body>

View file

@ -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<SortOption>('downloads');
const [tools, setTools] = useState<Tool[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
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<string>();
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<string>();
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)

View file

@ -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<BundleSizeData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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;
}

View file

@ -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 (
<div className="relative inline-block">

View file

@ -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 (
<SWRConfig
value={{
fetcher,
revalidateOnFocus: false,
dedupingInterval: 5000,
}}
>
{children}
</SWRConfig>
);
}

View file

@ -0,0 +1,91 @@
import useSWRInfinite from 'swr/infinite';
export interface Activity {
id: string;
type: string;
metadata: Record<string, unknown>;
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<ActivityResponse>(
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,
};
}

View file

@ -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<AgentsResponse>(
`/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,
};
}
);
}

View file

@ -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<BundleSizeData>(
packageName ? `/api/bundlephobia?${params.toString()}` : null,
{
// Don't retry on 404s (common for scoped packages)
shouldRetryOnError: false,
}
);
}

View file

@ -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<CollectionsResponse>(
`/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,
};
}
);
}

View file

@ -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<LikeStatusData>;
toggleLike: () => Promise<void>;
}
/**
* 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<LikeStatusData>(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,
};
}

View file

@ -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<PlatformStats>('/api/stats');
const executions = useSWR<ExecutionStats>('/api/stats/executions');
const history = useSWR<HistorySnapshot[]>('/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,
};
}

View file

@ -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<Tool[]>(`/api/tools?${queryString}`);
}

View file

@ -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 <T>(url: string): Promise<T> => {
const res = await fetch(url);
const json = await res.json();
if (!json.success) {
throw new Error(json.error || 'Request failed');
}
return json.data;
};

486
pnpm-lock.yaml generated
View file

@ -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: {}