From da6529f263d931f6e0f45d4f3cdde6b6bc82ef0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2026 21:33:22 +0100 Subject: [PATCH] feat: enhance canvas functionality with new node types and improved data handling - Added support for a new "compare" node type to facilitate side-by-side image comparisons. - Updated AI image and prompt nodes to include aspect ratio handling for better image generation. - Enhanced canvas toolbar to include export functionality for canvas data. - Improved data resolution for compare nodes by resolving incoming edges and updating node data accordingly. - Refactored frame node to support dynamic resizing and exporting capabilities. - Introduced debounced saving for prompt node to optimize performance during user input. --- .cursor/.gitignore | 1 + app/globals.css | 7 + components/canvas/canvas-toolbar.tsx | 20 +- components/canvas/canvas.tsx | 91 ++- components/canvas/export-button.tsx | 99 +++ components/canvas/nodes/ai-image-node.tsx | 19 +- components/canvas/nodes/compare-node.tsx | 198 ++++-- components/canvas/nodes/frame-node.tsx | 139 ++-- components/canvas/nodes/prompt-node.tsx | 99 ++- components/ui/select.tsx | 193 +++++ convex/_generated/api.d.ts | 2 + convex/ai.ts | 7 + convex/export.ts | 114 +++ convex/nodes.ts | 15 +- convex/openrouter.ts | 10 +- lib/canvas-utils.ts | 7 +- lib/image-formats.ts | 85 +++ package.json | 5 + pnpm-lock.yaml | 812 ++++++++++++++++++++++ 19 files changed, 1801 insertions(+), 122 deletions(-) create mode 100644 .cursor/.gitignore create mode 100644 components/canvas/export-button.tsx create mode 100644 components/ui/select.tsx create mode 100644 convex/export.ts create mode 100644 lib/image-formats.ts diff --git a/.cursor/.gitignore b/.cursor/.gitignore new file mode 100644 index 0000000..8bf7cc2 --- /dev/null +++ b/.cursor/.gitignore @@ -0,0 +1 @@ +plans/ diff --git a/app/globals.css b/app/globals.css index c530d90..ed6143e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -142,4 +142,11 @@ .animate-shimmer { animation: shimmer 1.5s ease-in-out infinite; } +} + +@layer components { + /* Verbindungs-Punkte über Node-Inhalt (XYFlow setzt kein z-index) */ + .react-flow__handle { + z-index: 50; + } } \ No newline at end of file diff --git a/components/canvas/canvas-toolbar.tsx b/components/canvas/canvas-toolbar.tsx index 661e536..6620a4e 100644 --- a/components/canvas/canvas-toolbar.tsx +++ b/components/canvas/canvas-toolbar.tsx @@ -5,6 +5,7 @@ import { useRef } from "react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; +import { ExportButton } from "@/components/canvas/export-button"; const nodeTemplates = [ { @@ -25,8 +26,8 @@ const nodeTemplates = [ type: "prompt", label: "Prompt", width: 320, - height: 140, - defaultData: { prompt: "", model: "" }, + height: 220, + defaultData: { prompt: "", model: "", aspectRatio: "1:1" }, }, { type: "note", @@ -42,13 +43,24 @@ const nodeTemplates = [ height: 240, defaultData: { label: "Untitled", exportWidth: 1080, exportHeight: 1080 }, }, + { + type: "compare", + label: "Compare", + width: 500, + height: 380, + defaultData: {}, + }, ] as const; interface CanvasToolbarProps { canvasId: Id<"canvases">; + canvasName?: string; } -export default function CanvasToolbar({ canvasId }: CanvasToolbarProps) { +export default function CanvasToolbar({ + canvasId, + canvasName, +}: CanvasToolbarProps) { const createNode = useMutation(api.nodes.create); const nodeCountRef = useRef(0); @@ -91,6 +103,8 @@ export default function CanvasToolbar({ canvasId }: CanvasToolbarProps) { {template.label} ))} +
+
); } diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx index b007018..09b8b0d 100644 --- a/components/canvas/canvas.tsx +++ b/components/canvas/canvas.tsx @@ -32,6 +32,54 @@ interface CanvasInnerProps { canvasId: Id<"canvases">; } +function withResolvedCompareData(nodes: RFNode[], edges: RFEdge[]): RFNode[] { + return nodes.map((node) => { + if (node.type !== "compare") return node; + + const incoming = edges.filter((edge) => edge.target === node.id); + let leftUrl: string | undefined; + let rightUrl: string | undefined; + let leftLabel: string | undefined; + let rightLabel: string | undefined; + + for (const edge of incoming) { + const source = nodes.find((candidate) => candidate.id === edge.source); + if (!source) continue; + + const srcData = source.data as { url?: string; label?: string }; + + if (edge.targetHandle === "left") { + leftUrl = srcData.url; + leftLabel = srcData.label ?? source.type ?? "Before"; + } else if (edge.targetHandle === "right") { + rightUrl = srcData.url; + rightLabel = srcData.label ?? source.type ?? "After"; + } + } + + const current = node.data as { + leftUrl?: string; + rightUrl?: string; + leftLabel?: string; + rightLabel?: string; + }; + + if ( + current.leftUrl === leftUrl && + current.rightUrl === rightUrl && + current.leftLabel === leftLabel && + current.rightLabel === rightLabel + ) { + return node; + } + + return { + ...node, + data: { ...node.data, leftUrl, rightUrl, leftLabel, rightLabel }, + }; + }); +} + function CanvasInner({ canvasId }: CanvasInnerProps) { const { screenToFlowPosition } = useReactFlow(); const { resolvedTheme } = useTheme(); @@ -54,9 +102,14 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { api.edges.list, shouldSkipCanvasQueries ? "skip" : { canvasId }, ); + const canvas = useQuery( + api.canvases.get, + shouldSkipCanvasQueries ? "skip" : { canvasId }, + ); // ─── Convex Mutations (exakte Signaturen aus nodes.ts / edges.ts) ── const moveNode = useMutation(api.nodes.move); + const resizeNode = useMutation(api.nodes.resize); const batchMoveNodes = useMutation(api.nodes.batchMove); const createNode = useMutation(api.nodes.create); const removeNode = useMutation(api.nodes.remove); @@ -74,8 +127,8 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { useEffect(() => { if (!convexNodes || isDragging.current) return; // eslint-disable-next-line react-hooks/set-state-in-effect - setNodes(convexNodes.map(convexNodeToRF)); - }, [convexNodes]); + setNodes(withResolvedCompareData(convexNodes.map(convexNodeToRF), edges)); + }, [convexNodes, edges]); useEffect(() => { if (!convexEdges) return; @@ -83,10 +136,36 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { setEdges(convexEdges.map(convexEdgeToRF)); }, [convexEdges]); + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setNodes((nds) => withResolvedCompareData(nds, edges)); + }, [edges]); + // ─── Node Changes (Drag, Select, Remove) ───────────────────── - const onNodesChange = useCallback((changes: NodeChange[]) => { - setNodes((nds) => applyNodeChanges(changes, nds)); - }, []); + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + setNodes((nds) => { + const nextNodes = applyNodeChanges(changes, nds); + + for (const change of changes) { + if (change.type !== "dimensions") continue; + if (change.resizing !== false || !change.dimensions) continue; + + const resizedNode = nextNodes.find((node) => node.id === change.id); + if (resizedNode?.type !== "frame") continue; + + void resizeNode({ + nodeId: change.id as Id<"nodes">, + width: change.dimensions.width, + height: change.dimensions.height, + }); + } + + return nextNodes; + }); + }, + [resizeNode], + ); const onEdgesChange = useCallback((changes: EdgeChange[]) => { setEdges((eds) => applyEdgeChanges(changes, eds)); @@ -212,7 +291,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { return (
- + (null); + const [error, setError] = useState(null); + + const handleZipExport = useCallback(async () => { + if (isExporting) return; + setIsExporting(true); + setError(null); + + try { + const nodes = getNodes(); + const frameNodes = nodes.filter((node) => node.type === "frame"); + + if (frameNodes.length === 0) { + throw new Error("No frames on canvas - add a Frame node first"); + } + + const zip = new JSZip(); + + for (let i = 0; i < frameNodes.length; i += 1) { + const frame = frameNodes[i]; + const frameLabel = + (frame.data as { label?: string }).label?.trim() || `frame-${i + 1}`; + + setProgress(`Exporting ${frameLabel} (${i + 1}/${frameNodes.length})...`); + + const result = await exportFrame({ + frameNodeId: frame.id as Id<"nodes">, + }); + + const response = await fetch(result.url); + if (!response.ok) { + throw new Error(`Failed to fetch export for ${frameLabel}`); + } + + const blob = await response.blob(); + zip.file(`${frameLabel}.png`, blob); + } + + setProgress("Packing ZIP..."); + const zipBlob = await zip.generateAsync({ type: "blob" }); + const url = URL.createObjectURL(zipBlob); + + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `${canvasName}-export.zip`; + anchor.click(); + + URL.revokeObjectURL(url); + } catch (err) { + setError(err instanceof Error ? err.message : "Export failed"); + } finally { + setIsExporting(false); + setProgress(null); + } + }, [canvasName, exportFrame, getNodes, isExporting]); + + return ( +
+ + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/components/canvas/nodes/ai-image-node.tsx b/components/canvas/nodes/ai-image-node.tsx index 46ad8e3..e8ab04a 100644 --- a/components/canvas/nodes/ai-image-node.tsx +++ b/components/canvas/nodes/ai-image-node.tsx @@ -7,6 +7,7 @@ import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import BaseNodeWrapper from "./base-node-wrapper"; import { DEFAULT_MODEL_ID, getModel } from "@/lib/ai-models"; +import { DEFAULT_ASPECT_RATIO } from "@/lib/image-formats"; import { cn, formatEurFromCents } from "@/lib/utils"; import { Loader2, @@ -25,6 +26,10 @@ type AiImageNodeData = { /** Gebuchte Credits in Euro-Cent (PRD: nach Commit) */ creditCost?: number; canvasId?: string; + /** OpenRouter image_config.aspect_ratio */ + aspectRatio?: string; + outputWidth?: number; + outputHeight?: number; _status?: string; _statusMessage?: string; }; @@ -93,6 +98,7 @@ export default function AiImageNode({ prompt, referenceStorageId, model: nodeData.model ?? DEFAULT_MODEL_ID, + aspectRatio: nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO, }); } catch (err) { setLocalError(err instanceof Error ? err.message : "Generation failed"); @@ -105,7 +111,10 @@ export default function AiImageNode({ getModel(nodeData.model ?? DEFAULT_MODEL_ID)?.name ?? "AI"; return ( - + -
+
🖼️ AI Image
-
+
{status === "idle" && !nodeData.url && (
@@ -209,12 +218,12 @@ export default function AiImageNode({
{nodeData.prompt && ( -
+

{nodeData.prompt}

- {modelName} + {modelName} · {nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO}

)} diff --git a/components/canvas/nodes/compare-node.tsx b/components/canvas/nodes/compare-node.tsx index c417c27..818b8a8 100644 --- a/components/canvas/nodes/compare-node.tsx +++ b/components/canvas/nodes/compare-node.tsx @@ -1,74 +1,168 @@ "use client"; -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; -import Image from "next/image"; +import { useCallback, useRef, useState } from "react"; +import { Handle, Position, type NodeProps } from "@xyflow/react"; +import { ImageIcon } from "lucide-react"; import BaseNodeWrapper from "./base-node-wrapper"; -type CompareNodeData = { +interface CompareNodeData { leftUrl?: string; rightUrl?: string; - _status?: string; -}; + leftLabel?: string; + rightLabel?: string; +} -export type CompareNode = Node; +export default function CompareNode({ data, selected }: NodeProps) { + const nodeData = data as CompareNodeData; + const [sliderX, setSliderX] = useState(50); + const containerRef = useRef(null); + + const hasLeft = !!nodeData.leftUrl; + const hasRight = !!nodeData.rightUrl; + + const handleMouseDown = useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + + const move = (moveEvent: MouseEvent) => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const x = Math.max( + 0, + Math.min(1, (moveEvent.clientX - rect.left) / rect.width), + ); + setSliderX(x * 100); + }; + + const up = () => { + window.removeEventListener("mousemove", move); + window.removeEventListener("mouseup", up); + }; + + window.addEventListener("mousemove", move); + window.addEventListener("mouseup", up); + }, []); + + const handleTouchStart = useCallback((event: React.TouchEvent) => { + event.stopPropagation(); + + const move = (moveEvent: TouchEvent) => { + if (!containerRef.current || moveEvent.touches.length === 0) return; + const rect = containerRef.current.getBoundingClientRect(); + const touch = moveEvent.touches[0]; + const x = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); + setSliderX(x * 100); + }; + + const end = () => { + window.removeEventListener("touchmove", move); + window.removeEventListener("touchend", end); + }; + + window.addEventListener("touchmove", move); + window.addEventListener("touchend", end); + }, []); -export default function CompareNode({ - data, - selected, -}: NodeProps) { return ( - -
- 🔀 Vergleich -
-
-
- {data.leftUrl ? ( - Vergleich Bild A - ) : ( -
- Bild A -
- )} -
-
- {data.rightUrl ? ( - Vergleich Bild B - ) : ( -
- Bild B -
- )} -
-
+ +
⚖️ Compare
+ + +
+ {!hasLeft && !hasRight && ( +
+ +

+ Connect two image nodes - left handle (blue) and right handle (green) +

+
+ )} + + {hasRight && ( + // eslint-disable-next-line @next/next/no-img-element + {nodeData.rightLabel + )} + + {hasLeft && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {nodeData.leftLabel +
+ )} + + {hasLeft && hasRight && ( + <> +
+
+
+ + + +
+
+ + )} + + {hasLeft && ( +
+ + {nodeData.leftLabel ?? "Before"} + +
+ )} + + {hasRight && ( +
+ + {nodeData.rightLabel ?? "After"} + +
+ )} +
); } diff --git a/components/canvas/nodes/frame-node.tsx b/components/canvas/nodes/frame-node.tsx index e2038dd..36ac8b4 100644 --- a/components/canvas/nodes/frame-node.tsx +++ b/components/canvas/nodes/frame-node.tsx @@ -1,72 +1,119 @@ "use client"; -import { useState, useCallback } from "react"; -import { type NodeProps, type Node } from "@xyflow/react"; -import { useMutation } from "convex/react"; +import { useCallback, useState } from "react"; +import { Handle, NodeResizer, Position, type NodeProps } from "@xyflow/react"; +import { useAction, useMutation } from "convex/react"; +import { Download, Loader2 } from "lucide-react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; +import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import BaseNodeWrapper from "./base-node-wrapper"; -type FrameNodeData = { +interface FrameNodeData { label?: string; - resolution?: string; - _status?: string; - _statusMessage?: string; -}; + width?: number; + height?: number; +} -export type FrameNode = Node; - -export default function FrameNode({ id, data, selected }: NodeProps) { +export default function FrameNode({ id, data, selected, width, height }: NodeProps) { + const nodeData = data as FrameNodeData; const updateData = useMutation(api.nodes.updateData); - const [editingLabel, setEditingLabel] = useState(null); + const exportFrame = useAction(api.export.exportFrame); - const displayLabel = data.label ?? "Frame"; - const isEditing = editingLabel !== null; + const [label, setLabel] = useState(nodeData.label ?? "Frame"); + const [isExporting, setIsExporting] = useState(false); + const [exportError, setExportError] = useState(null); - const handleDoubleClick = useCallback(() => { - setEditingLabel(displayLabel); - }, [displayLabel]); + const debouncedSave = useDebouncedCallback((value: string) => { + void updateData({ nodeId: id as Id<"nodes">, data: { ...nodeData, label: value } }); + }, 500); - const handleBlur = useCallback(() => { - if (editingLabel !== null && editingLabel !== data.label) { - updateData({ - nodeId: id as Id<"nodes">, - data: { - ...data, - label: editingLabel, - _status: undefined, - _statusMessage: undefined, - }, - }); + const handleLabelChange = useCallback( + (event: React.ChangeEvent) => { + setLabel(event.target.value); + debouncedSave(event.target.value); + }, + [debouncedSave], + ); + + const handleExport = useCallback(async () => { + if (isExporting) return; + setIsExporting(true); + setExportError(null); + + try { + const result = await exportFrame({ frameNodeId: id as Id<"nodes"> }); + const a = document.createElement("a"); + a.href = result.url; + a.download = result.filename; + a.click(); + } catch (error) { + setExportError(error instanceof Error ? error.message : "Export failed"); + } finally { + setIsExporting(false); } - setEditingLabel(null); - }, [editingLabel, data, id, updateData]); + }, [exportFrame, id, isExporting]); + + const frameW = Math.round(width ?? 400); + const frameH = Math.round(height ?? 300); return ( - {isEditing ? ( + + +
setEditingLabel(e.target.value)} - onBlur={handleBlur} - onKeyDown={(e) => e.key === "Enter" && handleBlur()} - autoFocus - className="nodrag text-xs font-medium text-blue-500 bg-transparent border-0 outline-none w-full" + value={label} + onChange={handleLabelChange} + onKeyDown={(event) => { + if (event.key === "Enter") { + (event.target as HTMLInputElement).blur(); + } + }} + className="nodrag nowheel w-40 border-none bg-transparent text-sm font-medium text-muted-foreground outline-none focus:text-foreground" /> - ) : ( -
+ {frameW}x{frameH} + + +
+ {isExporting ? "Exporting..." : "Export PNG"} + +
+ + {exportError && ( +
{exportError}
)} + +
+ + + ); } diff --git a/components/canvas/nodes/prompt-node.tsx b/components/canvas/nodes/prompt-node.tsx index ffe1e27..e108568 100644 --- a/components/canvas/nodes/prompt-node.tsx +++ b/components/canvas/nodes/prompt-node.tsx @@ -8,10 +8,28 @@ import type { Id } from "@/convex/_generated/dataModel"; import BaseNodeWrapper from "./base-node-wrapper"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { DEFAULT_MODEL_ID } from "@/lib/ai-models"; +import { + DEFAULT_ASPECT_RATIO, + getAiImageNodeOuterSize, + getImageViewportSize, + IMAGE_FORMAT_GROUP_LABELS, + IMAGE_FORMAT_PRESETS, +} from "@/lib/image-formats"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Sparkles, Loader2 } from "lucide-react"; type PromptNodeData = { prompt?: string; + aspectRatio?: string; model?: string; canvasId?: string; _status?: string; @@ -29,13 +47,25 @@ export default function PromptNode({ const { getEdges, getNode } = useReactFlow(); const [prompt, setPrompt] = useState(nodeData.prompt ?? ""); + const [aspectRatio, setAspectRatio] = useState( + nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO + ); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); + const promptRef = useRef(prompt); + const aspectRatioRef = useRef(aspectRatio); + promptRef.current = prompt; + aspectRatioRef.current = aspectRatio; + useEffect(() => { setPrompt(nodeData.prompt ?? ""); }, [nodeData.prompt]); + useEffect(() => { + setAspectRatio(nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO); + }, [nodeData.aspectRatio]); + const dataRef = useRef(data); dataRef.current = data; @@ -44,14 +74,18 @@ export default function PromptNode({ const createEdge = useMutation(api.edges.create); const generateImage = useAction(api.ai.generateImage); - const debouncedSave = useDebouncedCallback((value: string) => { + const debouncedSave = useDebouncedCallback(() => { const raw = dataRef.current as Record; const { _status, _statusMessage, ...rest } = raw; void _status; void _statusMessage; updateData({ nodeId: id as Id<"nodes">, - data: { ...rest, prompt: value }, + data: { + ...rest, + prompt: promptRef.current, + aspectRatio: aspectRatioRef.current, + }, }); }, 500); @@ -59,7 +93,15 @@ export default function PromptNode({ (e: React.ChangeEvent) => { const value = e.target.value; setPrompt(value); - debouncedSave(value); + debouncedSave(); + }, + [debouncedSave] + ); + + const handleAspectRatioChange = useCallback( + (value: string) => { + setAspectRatio(value); + debouncedSave(); }, [debouncedSave] ); @@ -93,18 +135,24 @@ export default function PromptNode({ const posX = (currentNode?.position?.x ?? 0) + offsetX; const posY = currentNode?.position?.y ?? 0; + const viewport = getImageViewportSize(aspectRatio); + const outer = getAiImageNodeOuterSize(viewport); + const aiNodeId = await createNode({ canvasId, type: "ai-image", positionX: posX, positionY: posY, - width: 320, - height: 320, + width: outer.width, + height: outer.height, data: { prompt, model: DEFAULT_MODEL_ID, modelTier: "standard", canvasId, + aspectRatio, + outputWidth: viewport.width, + outputHeight: viewport.height, }, }); @@ -122,6 +170,7 @@ export default function PromptNode({ prompt, referenceStorageId, model: DEFAULT_MODEL_ID, + aspectRatio, }); } catch (err) { setError(err instanceof Error ? err.message : "Generation failed"); @@ -130,6 +179,7 @@ export default function PromptNode({ } }, [ prompt, + aspectRatio, isGenerating, nodeData.canvasId, id, @@ -166,6 +216,45 @@ export default function PromptNode({ className="nodrag nowheel w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-violet-500" /> +
+ + +
+ {error && (

{error}

)} diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..0acb9f6 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,193 @@ +"use client" + +import * as React from "react" +import { Select as SelectPrimitive } from "radix-ui" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index b3458fa..aa89155 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -13,6 +13,7 @@ import type * as auth from "../auth.js"; import type * as canvases from "../canvases.js"; import type * as credits from "../credits.js"; import type * as edges from "../edges.js"; +import type * as export_ from "../export.js"; import type * as helpers from "../helpers.js"; import type * as http from "../http.js"; import type * as nodes from "../nodes.js"; @@ -31,6 +32,7 @@ declare const fullApi: ApiFromModules<{ canvases: typeof canvases; credits: typeof credits; edges: typeof edges; + export: typeof export_; helpers: typeof helpers; http: typeof http; nodes: typeof nodes; diff --git a/convex/ai.ts b/convex/ai.ts index a3e27c1..24dce4a 100644 --- a/convex/ai.ts +++ b/convex/ai.ts @@ -14,6 +14,7 @@ export const generateImage = action({ prompt: v.string(), referenceStorageId: v.optional(v.id("_storage")), model: v.optional(v.string()), + aspectRatio: v.optional(v.string()), }, handler: async (ctx, args) => { const apiKey = process.env.OPENROUTER_API_KEY; @@ -55,6 +56,7 @@ export const generateImage = action({ prompt: args.prompt, referenceImageUrl, model: modelId, + aspectRatio: args.aspectRatio, }); const binaryString = atob(result.imageBase64); @@ -71,6 +73,10 @@ export const generateImage = action({ const prev = (existing.data ?? {}) as Record; const creditCost = modelConfig.estimatedCostPerImage; + const aspectRatio = + args.aspectRatio?.trim() || + (typeof prev.aspectRatio === "string" ? prev.aspectRatio : undefined); + await ctx.runMutation(api.nodes.updateData, { nodeId: args.nodeId, data: { @@ -81,6 +87,7 @@ export const generateImage = action({ modelTier: modelConfig.tier, generatedAt: Date.now(), creditCost, + ...(aspectRatio ? { aspectRatio } : {}), }, }); diff --git a/convex/export.ts b/convex/export.ts new file mode 100644 index 0000000..f4c9f7b --- /dev/null +++ b/convex/export.ts @@ -0,0 +1,114 @@ +"use node"; + +// convex/export.ts +// +// Server-side frame export via jimp (pure JS, no native binaries). +// Loads all image nodes within a frame, composites them onto a canvas, +// stores the result in Convex Storage, and returns a short-lived download URL. +// +// Install: pnpm add jimp + +import { v } from "convex/values"; +import { action } from "./_generated/server"; +import { api } from "./_generated/api"; +import type { Id } from "./_generated/dataModel"; +import { Jimp } from "jimp"; + +export const exportFrame = action({ + args: { + frameNodeId: v.id("nodes"), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + + // ── 1. Load the frame node ───────────────────────────────────────────── + const frame = await ctx.runQuery(api.nodes.get, { nodeId: args.frameNodeId }); + if (!frame) throw new Error("Frame node not found"); + if (frame.type !== "frame") throw new Error("Node is not a frame"); + + const frameData = frame.data as { + label?: string; + width?: number; + height?: number; + }; + + const exportWidth = frameData.width ?? frame.width ?? 1920; + const exportHeight = frameData.height ?? frame.height ?? 1080; + const frameX = frame.positionX; + const frameY = frame.positionY; + + // ── 2. Load all nodes in this canvas ─────────────────────────────────── + const allNodes = await ctx.runQuery(api.nodes.list, { + canvasId: frame.canvasId, + }); + + // Find image/ai-image nodes visually within the frame + const imageNodes = allNodes.filter((node) => { + if (node.type !== "image" && node.type !== "ai-image") return false; + const data = node.data as { storageId?: string }; + if (!data.storageId) return false; + + const nodeRight = node.positionX + node.width; + const nodeBottom = node.positionY + node.height; + const frameRight = frameX + exportWidth; + const frameBottom = frameY + exportHeight; + + return ( + node.positionX < frameRight && + nodeRight > frameX && + node.positionY < frameBottom && + nodeBottom > frameY + ); + }); + + if (imageNodes.length === 0) { + throw new Error("No images found within this frame"); + } + + // ── 3. Create base canvas ────────────────────────────────────────────── + const base = new Jimp({ + width: exportWidth, + height: exportHeight, + color: 0xffffffff, // white background + }); + + // ── 4. Fetch, resize and composite each image ────────────────────────── + for (const node of imageNodes) { + const data = node.data as { storageId: string }; + const url = await ctx.storage.getUrl(data.storageId as Id<"_storage">); + if (!url) continue; + + const response = await fetch(url); + if (!response.ok) continue; + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + const relX = Math.max(0, Math.round(node.positionX - frameX)); + const relY = Math.max(0, Math.round(node.positionY - frameY)); + const nodeW = Math.round(node.width); + const nodeH = Math.round(node.height); + + const img = await Jimp.fromBuffer(buffer); + img.resize({ w: nodeW, h: nodeH }); + base.composite(img, relX, relY); + } + + // ── 5. Encode to PNG buffer ──────────────────────────────────────────── + const outputBuffer = await base.getBuffer("image/png"); + + // ── 6. Store in Convex Storage ───────────────────────────────────────── + const blob = new Blob([new Uint8Array(outputBuffer)], { type: "image/png" }); + const storageId = await ctx.storage.store(blob); + + const downloadUrl = await ctx.storage.getUrl(storageId); + if (!downloadUrl) throw new Error("Failed to generate download URL"); + + return { + url: downloadUrl, + storageId, + filename: `${frameData.label ?? "frame"}-export.png`, + }; + }, +}); diff --git a/convex/nodes.ts b/convex/nodes.ts index 58009bc..41c7f08 100644 --- a/convex/nodes.ts +++ b/convex/nodes.ts @@ -88,7 +88,20 @@ export const get = query({ return null; } - return node; + const data = node.data as Record | undefined; + if (!data?.storageId) { + return node; + } + + const url = await ctx.storage.getUrl(data.storageId as Id<"_storage">); + + return { + ...node, + data: { + ...data, + url: url ?? undefined, + }, + }; }, }); diff --git a/convex/openrouter.ts b/convex/openrouter.ts index 1917f42..5d8c48b 100644 --- a/convex/openrouter.ts +++ b/convex/openrouter.ts @@ -24,6 +24,8 @@ export interface GenerateImageParams { prompt: string; referenceImageUrl?: string; // optional image-to-image input model?: string; + /** OpenRouter image_config.aspect_ratio e.g. "16:9", "1:1" */ + aspectRatio?: string; } export interface OpenRouterImageResponse { @@ -59,7 +61,7 @@ export async function generateImageViaOpenRouter( text: params.prompt, }); - const body = { + const body: Record = { model: modelId, modalities: ["image", "text"], messages: [ @@ -70,6 +72,12 @@ export async function generateImageViaOpenRouter( ], }; + if (params.aspectRatio?.trim()) { + body.image_config = { + aspect_ratio: params.aspectRatio.trim(), + }; + } + const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, { method: "POST", headers: { diff --git a/lib/canvas-utils.ts b/lib/canvas-utils.ts index 97787b9..71235cf 100644 --- a/lib/canvas-utils.ts +++ b/lib/canvas-utils.ts @@ -49,8 +49,9 @@ export const NODE_DEFAULTS: Record< > = { image: { width: 280, height: 200, data: {} }, text: { width: 256, height: 120, data: { content: "" } }, - prompt: { width: 288, height: 140, data: { prompt: "" } }, - "ai-image": { width: 280, height: 220, data: {} }, + prompt: { width: 288, height: 220, data: { prompt: "", aspectRatio: "1:1" } }, + // 1:1 viewport 320 + chrome 88 ≈ äußere Höhe (siehe lib/image-formats.ts) + "ai-image": { width: 320, height: 408, data: {} }, group: { width: 400, height: 300, data: { label: "Gruppe" } }, frame: { width: 400, @@ -58,5 +59,5 @@ export const NODE_DEFAULTS: Record< data: { label: "Frame", resolution: "1080x1080" }, }, note: { width: 208, height: 100, data: { content: "" } }, - compare: { width: 500, height: 220, data: {} }, + compare: { width: 500, height: 380, data: {} }, }; diff --git a/lib/image-formats.ts b/lib/image-formats.ts new file mode 100644 index 0000000..ad75683 --- /dev/null +++ b/lib/image-formats.ts @@ -0,0 +1,85 @@ +/** OpenRouter / Gemini image_config.aspect_ratio values */ +export const DEFAULT_ASPECT_RATIO = "1:1" as const; + +export type ImageFormatGroup = "square" | "landscape" | "portrait"; + +export type ImageFormatPreset = { + label: string; + aspectRatio: string; + group: ImageFormatGroup; +}; + +export const IMAGE_FORMAT_GROUP_LABELS: Record = { + square: "Quadratisch", + landscape: "Querformat", + portrait: "Hochformat", +}; + +/** Presets for Prompt Node Select (labels DE, ratios API-compatible) */ +export const IMAGE_FORMAT_PRESETS: ImageFormatPreset[] = [ + { label: "1:1 · Quadrat", aspectRatio: "1:1", group: "square" }, + { label: "16:9 · Breitbild", aspectRatio: "16:9", group: "landscape" }, + { label: "21:9 · Cinematic", aspectRatio: "21:9", group: "landscape" }, + { label: "4:3 · Klassisch", aspectRatio: "4:3", group: "landscape" }, + { label: "3:2 · Foto (quer)", aspectRatio: "3:2", group: "landscape" }, + { label: "5:4 · leicht quer", aspectRatio: "5:4", group: "landscape" }, + { label: "9:16 · Stories", aspectRatio: "9:16", group: "portrait" }, + { label: "3:4 · Porträt", aspectRatio: "3:4", group: "portrait" }, + { label: "2:3 · Foto (hoch)", aspectRatio: "2:3", group: "portrait" }, + { label: "4:5 · Social hoch", aspectRatio: "4:5", group: "portrait" }, +]; + +/** Header row + footer strip (prompt preview) inside AI Image node */ +export const AI_IMAGE_NODE_HEADER_PX = 40; +export const AI_IMAGE_NODE_FOOTER_PX = 48; + +export function parseAspectRatioString(aspectRatio: string): { + w: number; + h: number; +} { + const parts = aspectRatio.split(":").map((x) => Number.parseInt(x, 10)); + if ( + parts.length !== 2 || + parts.some((n) => !Number.isFinite(n) || n <= 0) + ) { + throw new Error(`Invalid aspect ratio: ${aspectRatio}`); + } + return { w: parts[0]!, h: parts[1]! }; +} + +/** Bildfläche: längere Kante = maxEdgePx */ +export function getImageViewportSize( + aspectRatio: string, + options?: { maxEdge?: number } +): { width: number; height: number } { + const maxEdge = options?.maxEdge ?? 320; + const { w, h } = parseAspectRatioString(aspectRatio); + if (w >= h) { + return { + width: maxEdge, + height: Math.max(1, Math.round(maxEdge * (h / w))), + }; + } + return { + width: Math.max(1, Math.round(maxEdge * (w / h))), + height: maxEdge, + }; +} + +/** Outer Convex / React Flow node size (includes chrome) */ +export function getAiImageNodeOuterSize(viewport: { + width: number; + height: number; +}): { width: number; height: number } { + return { + width: viewport.width, + height: AI_IMAGE_NODE_HEADER_PX + viewport.height + AI_IMAGE_NODE_FOOTER_PX, + }; +} + +export function getPresetLabel(aspectRatio: string): string { + return ( + IMAGE_FORMAT_PRESETS.find((p) => p.aspectRatio === aspectRatio)?.label ?? + aspectRatio + ); +} diff --git a/package.json b/package.json index 082558b..619506f 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,18 @@ "@daveyplate/better-auth-ui": "^3.4.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/utilities": "^3.2.2", + "@napi-rs/canvas": "^0.1.97", "@xyflow/react": "^12.10.1", "better-auth": "^1.5.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.34.0", + "jimp": "^1.6.0", + "jszip": "^3.10.1", "lucide-react": "^1.6.0", "next": "16.2.1", "next-themes": "^0.4.6", + "optional": "^0.1.4", "radix-ui": "^1.4.3", "react": "19.2.4", "react-dom": "19.2.4", @@ -33,6 +37,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/jszip": "^3.4.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46af5e4..b98e36a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.2.4) + '@napi-rs/canvas': + specifier: ^0.1.97 + version: 0.1.97 '@xyflow/react': specifier: ^12.10.1 version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -35,6 +38,12 @@ importers: convex: specifier: ^1.34.0 version: 1.34.0(react@19.2.4) + jimp: + specifier: ^1.6.0 + version: 1.6.0 + jszip: + specifier: ^3.10.1 + version: 3.10.1 lucide-react: specifier: ^1.6.0 version: 1.6.0(react@19.2.4) @@ -44,6 +53,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + optional: + specifier: ^0.1.4 + version: 0.1.4 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -75,6 +87,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.2.2 + '@types/jszip': + specifier: ^3.4.1 + version: 3.4.1 '@types/node': specifier: ^20 version: 20.19.37 @@ -869,6 +884,118 @@ packages: '@instantdb/version@0.22.169': resolution: {integrity: sha512-czyJthQ2ipr+zPT/T0lQxrC9gDw0umocms3fXsydDmr29KQb/aWDmg7FmXK2b6RVfTsvwJZfjZS1VQrFiL88ZQ==} + '@jimp/core@1.6.0': + resolution: {integrity: sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==} + engines: {node: '>=18'} + + '@jimp/diff@1.6.0': + resolution: {integrity: sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==} + engines: {node: '>=18'} + + '@jimp/file-ops@1.6.0': + resolution: {integrity: sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==} + engines: {node: '>=18'} + + '@jimp/js-bmp@1.6.0': + resolution: {integrity: sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==} + engines: {node: '>=18'} + + '@jimp/js-gif@1.6.0': + resolution: {integrity: sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==} + engines: {node: '>=18'} + + '@jimp/js-jpeg@1.6.0': + resolution: {integrity: sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==} + engines: {node: '>=18'} + + '@jimp/js-png@1.6.0': + resolution: {integrity: sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==} + engines: {node: '>=18'} + + '@jimp/js-tiff@1.6.0': + resolution: {integrity: sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==} + engines: {node: '>=18'} + + '@jimp/plugin-blit@1.6.0': + resolution: {integrity: sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==} + engines: {node: '>=18'} + + '@jimp/plugin-blur@1.6.0': + resolution: {integrity: sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==} + engines: {node: '>=18'} + + '@jimp/plugin-circle@1.6.0': + resolution: {integrity: sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==} + engines: {node: '>=18'} + + '@jimp/plugin-color@1.6.0': + resolution: {integrity: sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==} + engines: {node: '>=18'} + + '@jimp/plugin-contain@1.6.0': + resolution: {integrity: sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==} + engines: {node: '>=18'} + + '@jimp/plugin-cover@1.6.0': + resolution: {integrity: sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==} + engines: {node: '>=18'} + + '@jimp/plugin-crop@1.6.0': + resolution: {integrity: sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==} + engines: {node: '>=18'} + + '@jimp/plugin-displace@1.6.0': + resolution: {integrity: sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==} + engines: {node: '>=18'} + + '@jimp/plugin-dither@1.6.0': + resolution: {integrity: sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==} + engines: {node: '>=18'} + + '@jimp/plugin-fisheye@1.6.0': + resolution: {integrity: sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==} + engines: {node: '>=18'} + + '@jimp/plugin-flip@1.6.0': + resolution: {integrity: sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==} + engines: {node: '>=18'} + + '@jimp/plugin-hash@1.6.0': + resolution: {integrity: sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==} + engines: {node: '>=18'} + + '@jimp/plugin-mask@1.6.0': + resolution: {integrity: sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==} + engines: {node: '>=18'} + + '@jimp/plugin-print@1.6.0': + resolution: {integrity: sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==} + engines: {node: '>=18'} + + '@jimp/plugin-quantize@1.6.0': + resolution: {integrity: sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==} + engines: {node: '>=18'} + + '@jimp/plugin-resize@1.6.0': + resolution: {integrity: sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==} + engines: {node: '>=18'} + + '@jimp/plugin-rotate@1.6.0': + resolution: {integrity: sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==} + engines: {node: '>=18'} + + '@jimp/plugin-threshold@1.6.0': + resolution: {integrity: sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==} + engines: {node: '>=18'} + + '@jimp/types@1.6.0': + resolution: {integrity: sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==} + engines: {node: '>=18'} + + '@jimp/utils@1.6.0': + resolution: {integrity: sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -908,6 +1035,81 @@ packages: resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} engines: {node: '>=18'} + '@napi-rs/canvas-android-arm64@0.1.97': + resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.97': + resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.97': + resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': + resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.97': + resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-arm64-musl@0.1.97': + resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': + resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-gnu@0.1.97': + resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-musl@0.1.97': + resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.97': + resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.97': + resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.97': + resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2044,6 +2246,9 @@ packages: peerDependencies: react: ^18 || ^19 + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@triplit/client@1.0.50': resolution: {integrity: sha512-3vjXTSdDQ3fzLDrewCK7elkAQc7CiDg0eZEOZInQbVMFRiakdieO5C2voSnNjSepIYHxDxFSBllgg32QsNpL9Q==} engines: {node: '>=18.0.0'} @@ -2108,6 +2313,13 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jszip@3.4.1': + resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} + deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. + + '@types/node@16.9.1': + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} @@ -2306,6 +2518,10 @@ packages: '@xyflow/system@0.0.75': resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2350,6 +2566,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2412,6 +2631,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + await-to-js@3.0.0: + resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} + engines: {node: '>=6.0.0'} + axe-core@4.11.1: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} @@ -2427,6 +2650,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.10: resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} engines: {node: '>=6.0.0'} @@ -2510,6 +2736,9 @@ packages: zod: optional: true + bmp-ts@1.0.9: + resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2533,6 +2762,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2693,6 +2925,9 @@ packages: core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -3088,6 +3323,14 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -3108,6 +3351,9 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + express-rate-limit@8.3.1: resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} engines: {node: '>= 16'} @@ -3165,6 +3411,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -3264,6 +3514,9 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + gifwrap@0.10.1: + resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3365,6 +3618,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3373,6 +3629,12 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3572,6 +3834,9 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3586,6 +3851,10 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jimp@1.6.0: + resolution: {integrity: sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==} + engines: {node: '>=18'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -3593,6 +3862,9 @@ packages: jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3639,6 +3911,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3668,6 +3943,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.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -3807,6 +4085,11 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3959,6 +4242,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3978,6 +4264,9 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + optional@0.1.4: + resolution: {integrity: sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4001,10 +4290,22 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + + parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + + parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4047,6 +4348,10 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4058,10 +4363,22 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pixelmatch@5.3.0: + resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4099,6 +4416,13 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4218,6 +4542,17 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -4294,6 +4629,12 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -4305,6 +4646,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4343,6 +4688,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -4385,6 +4733,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-xml-to-json@1.2.4: + resolution: {integrity: sha512-3MY16e0ocMHL7N1ufpdObURGyX+lCo0T/A+y6VCwosLdH1HSda4QZl1Sdt/O+2qWp48WFi26XEp5rF0LoaL0Dg==} + engines: {node: '>=20.12.2'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4454,6 +4806,12 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -4482,6 +4840,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -4527,6 +4889,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -4546,6 +4911,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -4682,6 +5051,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utif2@4.1.0: + resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4770,6 +5142,17 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5538,6 +5921,195 @@ snapshots: '@instantdb/version@0.22.169': {} + '@jimp/core@1.6.0': + dependencies: + '@jimp/file-ops': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + await-to-js: 3.0.0 + exif-parser: 0.1.12 + file-type: 16.5.4 + mime: 3.0.0 + + '@jimp/diff@1.6.0': + dependencies: + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + pixelmatch: 5.3.0 + + '@jimp/file-ops@1.6.0': {} + + '@jimp/js-bmp@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + bmp-ts: 1.0.9 + + '@jimp/js-gif@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + gifwrap: 0.10.1 + omggif: 1.0.10 + + '@jimp/js-jpeg@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + jpeg-js: 0.4.4 + + '@jimp/js-png@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + pngjs: 7.0.0 + + '@jimp/js-tiff@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + utif2: 4.1.0 + + '@jimp/plugin-blit@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-blur@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/utils': 1.6.0 + + '@jimp/plugin-circle@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-color@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + tinycolor2: 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-contain@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-cover@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-crop@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-displace@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-dither@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + + '@jimp/plugin-fisheye@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-flip@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-hash@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/js-bmp': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/js-tiff': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + any-base: 1.1.0 + + '@jimp/plugin-mask@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-print@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/types': 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + simple-xml-to-json: 1.2.4 + zod: 3.25.76 + + '@jimp/plugin-quantize@1.6.0': + dependencies: + image-q: 4.0.0 + zod: 3.25.76 + + '@jimp/plugin-resize@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-rotate@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-threshold@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-hash': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/types@1.6.0': + dependencies: + zod: 3.25.76 + + '@jimp/utils@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + tinycolor2: 1.6.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5595,6 +6167,53 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 + '@napi-rs/canvas-android-arm64@0.1.97': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.97': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.97': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.97': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.97': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.97': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.97': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.97': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.97': + optional: true + + '@napi-rs/canvas@0.1.97': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.97 + '@napi-rs/canvas-darwin-arm64': 0.1.97 + '@napi-rs/canvas-darwin-x64': 0.1.97 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.97 + '@napi-rs/canvas-linux-arm64-musl': 0.1.97 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.97 + '@napi-rs/canvas-linux-x64-gnu': 0.1.97 + '@napi-rs/canvas-linux-x64-musl': 0.1.97 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.97 + '@napi-rs/canvas-win32-x64-msvc': 0.1.97 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.1 @@ -6754,6 +7373,8 @@ snapshots: '@tanstack/query-core': 5.95.2 react: 19.2.4 + '@tokenizer/token@0.3.0': {} + '@triplit/client@1.0.50(typescript@5.9.3)': dependencies: '@triplit/db': 1.1.10(typescript@5.9.3) @@ -6831,6 +7452,12 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jszip@3.4.1': + dependencies: + jszip: 3.10.1 + + '@types/node@16.9.1': {} + '@types/node@20.19.37': dependencies: undici-types: 6.21.0 @@ -7028,6 +7655,10 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -7067,6 +7698,8 @@ snapshots: dependencies: color-convert: 2.0.1 + any-base@1.1.0: {} + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -7160,6 +7793,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + await-to-js@3.0.0: {} + axe-core@4.11.1: {} axobject-query@4.1.0: {} @@ -7168,6 +7803,8 @@ snapshots: balanced-match@4.0.4: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.10: {} better-auth@1.5.6(@opentelemetry/api@1.9.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -7215,6 +7852,8 @@ snapshots: optionalDependencies: zod: 4.3.6 + bmp-ts@1.0.9: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -7252,6 +7891,11 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -7367,6 +8011,8 @@ snapshots: core-js@3.49.0: {} + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -7904,6 +8550,10 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + + events@3.3.0: {} + eventsource-parser@3.0.6: {} eventsource@3.0.7: @@ -7941,6 +8591,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + exif-parser@0.1.12: {} + express-rate-limit@8.3.1(express@5.2.1): dependencies: express: 5.2.1 @@ -8026,6 +8678,12 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 6.3.0 + token-types: 4.2.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -8133,6 +8791,11 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + gifwrap@0.10.1: + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -8230,10 +8893,18 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} + image-q@4.0.0: + dependencies: + '@types/node': 16.9.1 + + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8408,6 +9079,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8423,10 +9096,42 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jimp@1.6.0: + dependencies: + '@jimp/core': 1.6.0 + '@jimp/diff': 1.6.0 + '@jimp/js-bmp': 1.6.0 + '@jimp/js-gif': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/js-tiff': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/plugin-blur': 1.6.0 + '@jimp/plugin-circle': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-contain': 1.6.0 + '@jimp/plugin-cover': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-displace': 1.6.0 + '@jimp/plugin-dither': 1.6.0 + '@jimp/plugin-fisheye': 1.6.0 + '@jimp/plugin-flip': 1.6.0 + '@jimp/plugin-hash': 1.6.0 + '@jimp/plugin-mask': 1.6.0 + '@jimp/plugin-print': 1.6.0 + '@jimp/plugin-quantize': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/plugin-rotate': 1.6.0 + '@jimp/plugin-threshold': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + jiti@2.6.1: {} jose@6.2.2: {} + jpeg-js@0.4.4: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -8466,6 +9171,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8489,6 +9201,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.32.0: optional: true @@ -8590,6 +9306,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@3.0.0: {} + mimic-fn@2.1.0: {} mimic-function@5.0.1: {} @@ -8747,6 +9465,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + omggif@1.0.10: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -8772,6 +9492,8 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + optional@0.1.4: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8809,10 +9531,21 @@ snapshots: dependencies: p-limit: 3.1.0 + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-bmfont-ascii@1.0.6: {} + + parse-bmfont-binary@1.0.6: {} + + parse-bmfont-xml@1.1.6: + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -8845,14 +9578,24 @@ snapshots: peberminta@0.9.0: {} + peek-readable@4.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} picomatch@4.0.4: {} + pixelmatch@5.3.0: + dependencies: + pngjs: 6.0.0 + pkce-challenge@5.0.1: {} + pngjs@6.0.0: {} + + pngjs@7.0.0: {} + possible-typed-array-names@1.1.0: {} postcss-selector-parser@7.1.1: @@ -8884,6 +9627,10 @@ snapshots: prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -9050,6 +9797,28 @@ snapshots: react@19.2.4: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -9147,6 +9916,10 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -9160,6 +9933,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.6.0: {} + scheduler@0.27.0: {} selderee@0.11.0: @@ -9219,6 +9994,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shadcn@4.1.0(@types/node@20.19.37)(typescript@5.9.3): @@ -9334,6 +10111,8 @@ snapshots: signal-exit@4.1.0: {} + simple-xml-to-json@1.2.4: {} + sisteransi@1.0.5: {} sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -9422,6 +10201,14 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -9444,6 +10231,11 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): dependencies: client-only: 0.0.1 @@ -9473,6 +10265,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinycolor2@1.6.0: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -9490,6 +10284,11 @@ snapshots: toidentifier@1.0.1: {} + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + tough-cookie@6.0.1: dependencies: tldts: 7.0.27 @@ -9658,6 +10457,10 @@ snapshots: dependencies: react: 19.2.4 + utif2@4.1.0: + dependencies: + pako: 1.0.11 + util-deprecate@1.0.2: {} uuid@11.1.0: {} @@ -9755,6 +10558,15 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + xml-parse-from-string@1.0.1: {} + + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + y18n@5.0.8: {} yallist@3.1.1: {}