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 (
-
+
-
+
-
+
{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 ? (
-
- ) : (
-
- Bild A
-
- )}
-
-
- {data.rightUrl ? (
-
- ) : (
-
- 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
+

+ )}
+
+ {hasLeft && (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+ )}
+
+ {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: {}