diff --git a/apps/dashboard/features/app/home/sibebar/new-folder.tsx b/apps/dashboard/features/app/home/sibebar/new-folder.tsx
new file mode 100644
index 0000000..78fcc63
--- /dev/null
+++ b/apps/dashboard/features/app/home/sibebar/new-folder.tsx
@@ -0,0 +1,72 @@
+import { Button } from "@castfy/ui/components/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@castfy/ui/components/dialog";
+import { Field, FieldGroup } from "@castfy/ui/components/field";
+import { Input } from "@castfy/ui/components/input";
+import { Label } from "@castfy/ui/components/label";
+import { PlusIcon } from "lucide-react";
+
+export function NewFolder() {
+ return (
+
+ );
+}
diff --git a/apps/dashboard/hooks/use-aspect-ratio-dimensions.ts b/apps/dashboard/hooks/use-aspect-ratio-dimensions.ts
new file mode 100644
index 0000000..141644c
--- /dev/null
+++ b/apps/dashboard/hooks/use-aspect-ratio-dimensions.ts
@@ -0,0 +1,137 @@
+/**
+ * Hook for getting aspect ratio dimensions
+ * Provides reactive dimensions based on selected aspect ratio
+ */
+
+import { useEffect, useMemo, useState } from "react";
+import {
+ calculateFitDimensions,
+ getAspectRatioCSS,
+ getAspectRatioPreset,
+} from "@/lib/aspect-ratio-utils";
+import { useImageStore } from "@/lib/store";
+
+/**
+ * Hook to get canvas dimensions based on selected aspect ratio
+ * Returns dimensions that fit within viewport constraints
+ */
+export function useAspectRatioDimensions(options?: {
+ maxWidth?: number;
+ maxHeight?: number;
+}) {
+ const { selectedAspectRatio } = useImageStore();
+
+ const dimensions = useMemo(() => {
+ const preset = getAspectRatioPreset(selectedAspectRatio);
+ if (!preset) {
+ return { width: 1920, height: 1080, aspectRatio: "16/9" };
+ }
+
+ const { maxWidth, maxHeight } = options || {};
+
+ // If constraints provided, calculate fit dimensions
+ if (maxWidth || maxHeight) {
+ const fitDimensions = calculateFitDimensions(
+ preset.width,
+ preset.height,
+ maxWidth,
+ maxHeight
+ );
+ return {
+ ...fitDimensions,
+ aspectRatio: getAspectRatioCSS(preset.width, preset.height),
+ originalWidth: preset.width,
+ originalHeight: preset.height,
+ };
+ }
+
+ // Return original dimensions
+ return {
+ width: preset.width,
+ height: preset.height,
+ aspectRatio: getAspectRatioCSS(preset.width, preset.height),
+ originalWidth: preset.width,
+ originalHeight: preset.height,
+ };
+ }, [selectedAspectRatio, options]);
+
+ return dimensions;
+}
+
+/**
+ * Hook to get responsive canvas dimensions that fit within viewport
+ * Automatically calculates max dimensions based on viewport size
+ * Reactively updates when window is resized
+ */
+export function useResponsiveCanvasDimensions() {
+ const { selectedAspectRatio } = useImageStore();
+ const [viewportSize, setViewportSize] = useState({
+ width: 1920,
+ height: 1080,
+ });
+
+ // Track viewport size changes
+ useEffect(() => {
+ const updateViewportSize = () => {
+ setViewportSize({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+ };
+
+ // Set initial size
+ updateViewportSize();
+
+ // Listen for resize events
+ window.addEventListener("resize", updateViewportSize);
+ return () => window.removeEventListener("resize", updateViewportSize);
+ }, []);
+
+ const dimensions = useMemo(() => {
+ const preset = getAspectRatioPreset(selectedAspectRatio);
+ if (!preset) {
+ return { width: 1920, height: 1080, aspectRatio: "16/9" };
+ }
+
+ // Determine layout constraints based on viewport size.
+ // On mobile the side panels are hidden inside sheets, so we should not
+ // subtract their desktop width (otherwise calculations go negative and
+ // the canvas collapses). This keeps the preview and export canvases in sync.
+ const MOBILE_BREAKPOINT = 768;
+ const isMobileViewport = viewportSize.width < MOBILE_BREAKPOINT;
+ const sidePanelsWidth = isMobileViewport ? 0 : 640; // left + right panels on desktop
+ const horizontalPadding = isMobileViewport ? 32 : 48; // reduce padding on small screens
+ const verticalPadding = isMobileViewport ? 140 : 200; // header + footer allowance
+
+ const rawAvailableWidth =
+ viewportSize.width - sidePanelsWidth - horizontalPadding;
+ const rawAvailableHeight = viewportSize.height - verticalPadding;
+
+ // Prevent negative/too-small values so fit calculations remain stable.
+ const MIN_AVAILABLE = 320;
+ const availableWidth = Math.max(rawAvailableWidth, MIN_AVAILABLE);
+ const availableHeight = Math.max(rawAvailableHeight, MIN_AVAILABLE);
+
+ // Allow a little breathing room on larger screens without over-scaling mobile.
+ const widthScale = isMobileViewport ? 1 : 1.1;
+ const heightScale = isMobileViewport ? 1 : 1.1;
+ const maxWidth = Math.min(availableWidth * widthScale, 3000);
+ const maxHeight = Math.min(availableHeight * heightScale, 1500);
+
+ const fitDimensions = calculateFitDimensions(
+ preset.width,
+ preset.height,
+ maxWidth,
+ maxHeight
+ );
+
+ return {
+ ...fitDimensions,
+ aspectRatio: getAspectRatioCSS(preset.width, preset.height),
+ originalWidth: preset.width,
+ originalHeight: preset.height,
+ };
+ }, [selectedAspectRatio, viewportSize.width, viewportSize.height]);
+
+ return dimensions;
+}
diff --git a/apps/dashboard/lib/aspect-ratio-utils.ts b/apps/dashboard/lib/aspect-ratio-utils.ts
new file mode 100644
index 0000000..7afc99c
--- /dev/null
+++ b/apps/dashboard/lib/aspect-ratio-utils.ts
@@ -0,0 +1,200 @@
+/**
+ * Aspect Ratio Utilities
+ *
+ * Provides standard pixel dimensions for common aspect ratios
+ * Based on industry standards for social media, video, and design platforms
+ */
+
+import { ASPECT_RATIO_PRESETS, type AspectRatioPreset } from "@/lib/constants";
+import { aspectRatios } from "@/lib/constants/aspect-ratios";
+import { useImageStore } from "@/lib/store";
+
+/**
+ * Standard pixel dimensions mapping for aspect ratios
+ * These are industry-standard resolutions that maintain aspect ratios
+ */
+const STANDARD_DIMENSIONS: Record
= {
+ "16:9": { width: 1920, height: 1080 }, // Full HD - Standard for video/YouTube
+ "1:1": { width: 1080, height: 1080 }, // Square - Instagram feed posts
+ "4:5": { width: 1080, height: 1350 }, // Portrait - Instagram portrait posts
+ "9:16": { width: 1080, height: 1920 }, // Story/Reel - Instagram Stories, TikTok
+ "3:4": { width: 1080, height: 1440 }, // Portrait - Pinterest pins
+ "2:3": { width: 1200, height: 1800 }, // Portrait - Social media posts
+ "3:2": { width: 1920, height: 1280 }, // Photo - Standard photography
+ "4:3": { width: 1920, height: 1440 }, // Traditional - Classic displays
+ "5:4": { width: 1920, height: 1536 }, // Photo - Classic photography
+ "16:10": { width: 1920, height: 1200 }, // Widescreen - Desktop displays
+ "40:21": { width: 1200, height: 630 }, // Open Graph - Standard OG image format (1200×630px)
+ "3:1": { width: 1500, height: 500 }, // Twitter Banner - Twitter/X profile banner format
+ "4:1": { width: 1584, height: 396 }, // LinkedIn Banner - LinkedIn profile/company banner format
+};
+
+// Special dimensions for specific aspect ratio IDs that share common ratios
+const SPECIAL_DIMENSIONS: Record = {
+ youtube_banner: { width: 2560, height: 1440 }, // YouTube Channel Banner - Higher resolution 16:9
+ instagram_banner: { width: 1080, height: 1080 }, // Instagram Highlight Cover - Square format
+ youtube_thumbnail: { width: 1280, height: 720 }, // YouTube Thumbnail
+ youtube_video: { width: 1920, height: 1080 }, // YouTube Video
+ pinterest_long: { width: 1000, height: 2100 }, // Pinterest Long Pin
+ appstore_iphone65: { width: 1284, height: 2778 }, // iPhone 6.5" screenshot
+ appstore_iphone55: { width: 1242, height: 2208 }, // iPhone 5.5" screenshot
+ appstore_ipad: { width: 2048, height: 2732 }, // iPad Pro 12.9" screenshot
+ appstore_iphone65_landscape: { width: 2778, height: 1284 }, // iPhone 6.5" landscape
+ appstore_iphone55_landscape: { width: 2208, height: 1242 }, // iPhone 5.5" landscape
+ appstore_ipad_landscape: { width: 2732, height: 2048 }, // iPad Pro 12.9" landscape
+};
+
+/**
+ * Get standard pixel dimensions for an aspect ratio
+ * Returns dimensions that maintain the aspect ratio but use standard sizes
+ *
+ * @param width - Aspect ratio width (e.g., 16 for 16:9)
+ * @param height - Aspect ratio height (e.g., 9 for 16:9)
+ * @returns Standard pixel dimensions for the aspect ratio
+ */
+export function getStandardDimensions(
+ width: number,
+ height: number
+): { width: number; height: number } {
+ const ratioString = `${width}:${height}`;
+
+ // Check if we have a standard dimension for this ratio
+ if (STANDARD_DIMENSIONS[ratioString]) {
+ return STANDARD_DIMENSIONS[ratioString];
+ }
+
+ // Calculate ratio
+ const ratio = width / height;
+
+ // Fallback: scale to a reasonable size maintaining aspect ratio
+ // Aim for width around 1920px for landscape or 1080px for portrait
+ if (ratio > 1) {
+ // Landscape - use Full HD width
+ return { width: 1920, height: Math.round(1920 / ratio) };
+ }
+ // Portrait - use Full HD height
+ return { width: Math.round(1080 * ratio), height: 1080 };
+}
+
+/**
+ * Get aspect ratio preset from aspect ratio ID
+ *
+ * @param aspectRatioId - The ID of the aspect ratio (e.g., '16_9')
+ * @returns AspectRatioPreset with proper dimensions
+ */
+export function getAspectRatioPreset(
+ aspectRatioId: string
+): AspectRatioPreset | null {
+ // Handle custom dimensions from the store
+ if (aspectRatioId === "custom") {
+ const customDimensions = useImageStore.getState().customDimensions;
+ if (customDimensions) {
+ return {
+ id: "custom",
+ name: "Custom",
+ category: "Custom",
+ width: customDimensions.width,
+ height: customDimensions.height,
+ ratio: `${customDimensions.width}:${customDimensions.height}`,
+ description: "Custom dimensions",
+ };
+ }
+ }
+
+ const aspectRatio = aspectRatios.find((ar) => ar.id === aspectRatioId);
+
+ if (!aspectRatio) {
+ return null;
+ }
+
+ // Check for special dimensions first (for formats that share ratios but have different dimensions)
+ if (SPECIAL_DIMENSIONS[aspectRatioId]) {
+ const specialDimensions = SPECIAL_DIMENSIONS[aspectRatioId];
+ const ratioString = `${aspectRatio.width}:${aspectRatio.height}`;
+ return {
+ id: aspectRatio.id,
+ name: aspectRatio.name,
+ category: aspectRatio.category || "Custom",
+ width: specialDimensions.width,
+ height: specialDimensions.height,
+ ratio: ratioString,
+ description: aspectRatio.description,
+ };
+ }
+
+ // Find matching preset by comparing ratio strings
+ const ratioString = `${aspectRatio.width}:${aspectRatio.height}`;
+ const matchingPreset = ASPECT_RATIO_PRESETS.find(
+ (preset) =>
+ preset.ratio === ratioString ||
+ preset.ratio === aspectRatio.ratio.toString()
+ );
+
+ if (matchingPreset) {
+ return matchingPreset;
+ }
+
+ // Create a preset with standard dimensions for this aspect ratio
+ const standardDimensions = getStandardDimensions(
+ aspectRatio.width,
+ aspectRatio.height
+ );
+ return {
+ id: aspectRatio.id,
+ name: aspectRatio.name,
+ category: aspectRatio.category || "Custom",
+ width: standardDimensions.width,
+ height: standardDimensions.height,
+ ratio: ratioString,
+ description: aspectRatio.description,
+ };
+}
+
+/**
+ * Calculate display dimensions that fit within viewport constraints
+ * while maintaining aspect ratio
+ *
+ * @param width - Original width
+ * @param height - Original height
+ * @param maxWidth - Maximum width constraint
+ * @param maxHeight - Maximum height constraint
+ * @returns Dimensions that fit within constraints
+ */
+export function calculateFitDimensions(
+ width: number,
+ height: number,
+ maxWidth?: number,
+ maxHeight?: number
+): { width: number; height: number } {
+ const ratio = width / height;
+
+ let displayWidth = width;
+ let displayHeight = height;
+
+ // Apply constraints
+ if (maxWidth && displayWidth > maxWidth) {
+ displayWidth = maxWidth;
+ displayHeight = displayWidth / ratio;
+ }
+
+ if (maxHeight && displayHeight > maxHeight) {
+ displayHeight = maxHeight;
+ displayWidth = displayHeight * ratio;
+ }
+
+ return {
+ width: Math.round(displayWidth),
+ height: Math.round(displayHeight),
+ };
+}
+
+/**
+ * Get CSS aspect ratio string from dimensions
+ *
+ * @param width - Width of the aspect ratio
+ * @param height - Height of the aspect ratio
+ * @returns CSS aspect ratio string (e.g., "16/9")
+ */
+export function getAspectRatioCSS(width: number, height: number): string {
+ return `${width} / ${height}`;
+}
diff --git a/apps/dashboard/lib/constants.ts b/apps/dashboard/lib/constants.ts
index 37b5426..3edd3ae 100644
--- a/apps/dashboard/lib/constants.ts
+++ b/apps/dashboard/lib/constants.ts
@@ -17,3 +17,164 @@ export const LocalStorageKeys = {
};
export const SUPPORT_EMAIL = "support@midday.ai";
+
+// Default canvas dimensions
+export const DEFAULT_CANVAS_WIDTH = 1920;
+export const DEFAULT_CANVAS_HEIGHT = 1080;
+
+// Image upload limits
+export const MAX_IMAGE_SIZE = 100 * 1024 * 1024; // 100MB
+export const ALLOWED_IMAGE_TYPES = [
+ "image/jpeg",
+ "image/jpg",
+ "image/png",
+ "image/webp",
+];
+
+// Text defaults
+export const DEFAULT_TEXT_FONT_SIZE = 48;
+export const DEFAULT_TEXT_COLOR = "#000000";
+export const DEFAULT_FONT_FAMILY = "Arial";
+
+// Canvas defaults
+export const CANVAS_BACKGROUND_COLOR = "#ffffff";
+
+// Aspect Ratio Presets
+export interface AspectRatioPreset {
+ category: string;
+ description?: string;
+ height: number;
+ id: string;
+ name: string;
+ ratio: string; // e.g., "1:1", "4:5", "9:16"
+ width: number;
+}
+
+export const ASPECT_RATIO_PRESETS: AspectRatioPreset[] = [
+ // Instagram Formats
+ {
+ id: "instagram-square",
+ name: "Instagram Square",
+ category: "Instagram",
+ width: 1080,
+ height: 1080,
+ ratio: "1:1",
+ description: "Perfect for Instagram feed posts",
+ },
+ {
+ id: "instagram-portrait",
+ name: "Instagram Portrait",
+ category: "Instagram",
+ width: 1080,
+ height: 1350,
+ ratio: "4:5",
+ description: "Portrait format for Instagram feed",
+ },
+ {
+ id: "instagram-landscape",
+ name: "Instagram Landscape",
+ category: "Instagram",
+ width: 1080,
+ height: 566,
+ ratio: "1.91:1",
+ description: "Landscape format for Instagram feed",
+ },
+ {
+ id: "instagram-story",
+ name: "Instagram Story",
+ category: "Instagram",
+ width: 1080,
+ height: 1920,
+ ratio: "9:16",
+ description: "Full-screen vertical stories and reels",
+ },
+ {
+ id: "instagram-reel",
+ name: "Instagram Reel",
+ category: "Instagram",
+ width: 1080,
+ height: 1920,
+ ratio: "9:16",
+ description: "Vertical video format for reels",
+ },
+
+ // Common Social Media
+ {
+ id: "facebook-post",
+ name: "Facebook Post",
+ category: "Facebook",
+ width: 1200,
+ height: 630,
+ ratio: "1.91:1",
+ description: "Standard Facebook post size",
+ },
+ {
+ id: "twitter-post",
+ name: "Twitter/X Post",
+ category: "Twitter",
+ width: 1200,
+ height: 675,
+ ratio: "16:9",
+ description: "Standard Twitter post size",
+ },
+ {
+ id: "youtube-thumbnail",
+ name: "YouTube Thumbnail",
+ category: "YouTube",
+ width: 1280,
+ height: 720,
+ ratio: "16:9",
+ description: "YouTube video thumbnail size",
+ },
+
+ // Standard Formats
+ {
+ id: "custom",
+ name: "Custom",
+ category: "Custom",
+ width: 1920,
+ height: 1080,
+ ratio: "16:9",
+ description: "Custom dimensions",
+ },
+ {
+ id: "square",
+ name: "Square",
+ category: "Standard",
+ width: 1080,
+ height: 1080,
+ ratio: "1:1",
+ description: "Square format",
+ },
+ {
+ id: "portrait-4-3",
+ name: "Portrait 4:3",
+ category: "Standard",
+ width: 1200,
+ height: 1600,
+ ratio: "3:4",
+ description: "Portrait format 3:4",
+ },
+ {
+ id: "landscape-16-9",
+ name: "Landscape 16:9",
+ category: "Standard",
+ width: 1920,
+ height: 1080,
+ ratio: "16:9",
+ description: "Widescreen landscape format",
+ },
+ {
+ id: "landscape-21-9",
+ name: "Ultrawide 21:9",
+ category: "Standard",
+ width: 2560,
+ height: 1080,
+ ratio: "21:9",
+ description: "Ultrawide format",
+ },
+];
+
+export const DEFAULT_ASPECT_RATIO =
+ ASPECT_RATIO_PRESETS.find((p) => p.id === "custom") ||
+ ASPECT_RATIO_PRESETS[0];
diff --git a/apps/dashboard/lib/constants/aspect-ratios.ts b/apps/dashboard/lib/constants/aspect-ratios.ts
new file mode 100644
index 0000000..c1066b4
--- /dev/null
+++ b/apps/dashboard/lib/constants/aspect-ratios.ts
@@ -0,0 +1,278 @@
+export interface AspectRatio {
+ category: string;
+ description: string;
+ height: number;
+ id: string;
+ name: string;
+ ratio: number;
+ useCase?: string;
+ width: number;
+}
+
+export const aspectRatios: AspectRatio[] = [
+ // Instagram Formats
+ {
+ id: "1_1",
+ name: "Square",
+ ratio: 1,
+ width: 1,
+ height: 1,
+ category: "Instagram",
+ description: "Perfect for Instagram feed posts",
+ useCase: "Instagram Posts",
+ },
+ {
+ id: "4_5",
+ name: "Portrait",
+ ratio: 4 / 5,
+ width: 4,
+ height: 5,
+ category: "Instagram",
+ description: "Portrait format for Instagram feed",
+ useCase: "Instagram Posts",
+ },
+ {
+ id: "9_16",
+ name: "Story/Reel",
+ ratio: 9 / 16,
+ width: 9,
+ height: 16,
+ category: "Instagram",
+ description: "Full-screen vertical stories and reels",
+ useCase: "Instagram Stories, Reels, TikTok",
+ },
+
+ // Social Media
+ {
+ id: "16_9",
+ name: "Landscape",
+ ratio: 16 / 9,
+ width: 16,
+ height: 9,
+ category: "Social Media",
+ description: "Widescreen format for videos and posts",
+ useCase: "YouTube, Twitter/X, Facebook Posts",
+ },
+ {
+ id: "3_4",
+ name: "Portrait",
+ ratio: 3 / 4,
+ width: 3,
+ height: 4,
+ category: "Social Media",
+ description: "Vertical format for Pinterest and social posts",
+ useCase: "Pinterest Pins, Social Posts",
+ },
+ {
+ id: "2_3",
+ name: "Portrait",
+ ratio: 2 / 3,
+ width: 2,
+ height: 3,
+ category: "Social Media",
+ description: "Tall vertical format",
+ useCase: "Social Media Posts",
+ },
+ {
+ id: "og_image",
+ name: "Open Graph",
+ ratio: 1200 / 630,
+ width: 40,
+ height: 21,
+ category: "Social Media",
+ description: "Standard Open Graph image format (1200×630px)",
+ useCase: "Open Graph Images, Facebook, LinkedIn, Twitter/X Cards",
+ },
+ {
+ id: "twitter_banner",
+ name: "Twitter Banner",
+ ratio: 3 / 1,
+ width: 3,
+ height: 1,
+ category: "Social Media",
+ description: "Twitter/X profile banner format",
+ useCase: "Twitter/X Profile Banners",
+ },
+ {
+ id: "instagram_banner",
+ name: "Instagram Banner",
+ ratio: 1,
+ width: 1,
+ height: 1,
+ category: "Instagram",
+ description: "Instagram highlight cover format (1080×1080px)",
+ useCase: "Instagram Highlight Covers",
+ },
+ {
+ id: "youtube_banner",
+ name: "YouTube Banner",
+ ratio: 16 / 9,
+ width: 16,
+ height: 9,
+ category: "Social Media",
+ description: "YouTube channel banner format (2560×1440px)",
+ useCase: "YouTube Channel Banners",
+ },
+ {
+ id: "linkedin_banner",
+ name: "LinkedIn Banner",
+ ratio: 4 / 1,
+ width: 4,
+ height: 1,
+ category: "Social Media",
+ description: "LinkedIn profile/company banner format (1584×396px)",
+ useCase: "LinkedIn Profile & Company Banners",
+ },
+
+ // Standard Formats
+ {
+ id: "3_2",
+ name: "Photo",
+ ratio: 3 / 2,
+ width: 3,
+ height: 2,
+ category: "Standard",
+ description: "Standard photo format",
+ useCase: "Photography",
+ },
+ {
+ id: "4_3",
+ name: "Traditional",
+ ratio: 4 / 3,
+ width: 4,
+ height: 3,
+ category: "Standard",
+ description: "Traditional display format",
+ useCase: "Traditional Displays",
+ },
+ {
+ id: "5_4",
+ name: "Photo",
+ ratio: 5 / 4,
+ width: 5,
+ height: 4,
+ category: "Standard",
+ description: "Classic photo format",
+ useCase: "Photography",
+ },
+ {
+ id: "16_10",
+ name: "Widescreen",
+ ratio: 16 / 10,
+ width: 16,
+ height: 10,
+ category: "Standard",
+ description: "Widescreen desktop format",
+ useCase: "Desktop Displays",
+ },
+
+ // YouTube Specific
+ {
+ id: "youtube_thumbnail",
+ name: "YouTube Thumbnail",
+ ratio: 16 / 9,
+ width: 16,
+ height: 9,
+ category: "YouTube",
+ description: "YouTube video thumbnail (1280×720px)",
+ useCase: "YouTube Thumbnails",
+ },
+ {
+ id: "youtube_video",
+ name: "YouTube Video",
+ ratio: 16 / 9,
+ width: 16,
+ height: 9,
+ category: "YouTube",
+ description: "YouTube video format (1920×1080px)",
+ useCase: "YouTube Videos",
+ },
+
+ // Pinterest
+ {
+ id: "pinterest_long",
+ name: "Long Pin",
+ ratio: 10 / 21,
+ width: 10,
+ height: 21,
+ category: "Pinterest",
+ description: "Long Pinterest pin format (1000×2100px)",
+ useCase: "Pinterest Long Pins",
+ },
+
+ // App Store Screenshots
+ {
+ id: "appstore_iphone65",
+ name: 'iPhone 6.5"',
+ ratio: 1284 / 2778,
+ width: 1284,
+ height: 2778,
+ category: "App Store",
+ description: 'iPhone 6.5" screenshot',
+ useCase: "App Store Screenshots",
+ },
+ {
+ id: "appstore_iphone55",
+ name: 'iPhone 5.5"',
+ ratio: 1242 / 2208,
+ width: 1242,
+ height: 2208,
+ category: "App Store",
+ description: 'iPhone 5.5" screenshot',
+ useCase: "App Store Screenshots",
+ },
+ {
+ id: "appstore_ipad",
+ name: 'iPad Pro 12.9"',
+ ratio: 2048 / 2732,
+ width: 2048,
+ height: 2732,
+ category: "App Store",
+ description: 'iPad Pro 12.9" screenshot',
+ useCase: "App Store Screenshots",
+ },
+ {
+ id: "appstore_iphone65_landscape",
+ name: 'iPhone 6.5" Landscape',
+ ratio: 2778 / 1284,
+ width: 2778,
+ height: 1284,
+ category: "App Store",
+ description: 'iPhone 6.5" landscape screenshot',
+ useCase: "App Store Screenshots",
+ },
+ {
+ id: "appstore_iphone55_landscape",
+ name: 'iPhone 5.5" Landscape',
+ ratio: 2208 / 1242,
+ width: 2208,
+ height: 1242,
+ category: "App Store",
+ description: 'iPhone 5.5" landscape screenshot',
+ useCase: "App Store Screenshots",
+ },
+ {
+ id: "appstore_ipad_landscape",
+ name: 'iPad Pro 12.9" Landscape',
+ ratio: 2732 / 2048,
+ width: 2732,
+ height: 2048,
+ category: "App Store",
+ description: 'iPad Pro 12.9" landscape screenshot',
+ useCase: "App Store Screenshots",
+ },
+
+ // Custom - placeholder entry, actual dimensions come from store
+ {
+ id: "custom",
+ name: "Custom",
+ ratio: 16 / 9,
+ width: 16,
+ height: 9,
+ category: "Custom",
+ description: "Custom dimensions",
+ useCase: "Custom",
+ },
+];
+
+export type AspectRatioKey = (typeof aspectRatios)[number]["id"];
diff --git a/apps/dashboard/lib/constants/backgrounds.ts b/apps/dashboard/lib/constants/backgrounds.ts
new file mode 100644
index 0000000..ffe1b64
--- /dev/null
+++ b/apps/dashboard/lib/constants/backgrounds.ts
@@ -0,0 +1,144 @@
+import { getR2ImageUrl } from "@/lib/r2";
+import { backgroundPaths } from "@/lib/r2/r2-backgrounds";
+import { type GradientKey, gradientColors } from "./gradient-colors";
+import {
+ type MagicGradientKey,
+ type MeshGradientKey,
+ magicGradients,
+ meshGradients,
+} from "./mesh-gradients";
+import { type SolidColorKey, solidColors } from "./solid-colors";
+
+export type BackgroundType = "gradient" | "solid" | "image";
+
+export interface BackgroundConfig {
+ opacity?: number;
+ type: BackgroundType;
+ value: GradientKey | SolidColorKey | string;
+}
+
+export const getBackgroundStyle = (config: BackgroundConfig): string => {
+ const { type, value } = config;
+
+ switch (type) {
+ case "gradient": {
+ if (typeof value === "string" && value.startsWith("mesh:")) {
+ const meshKey = value.replace("mesh:", "") as MeshGradientKey;
+ return meshGradients[meshKey] || gradientColors.vibrant_orange_pink;
+ }
+ if (typeof value === "string" && value.startsWith("magic:")) {
+ const magicKey = value.replace("magic:", "") as MagicGradientKey;
+ return magicGradients[magicKey] || gradientColors.vibrant_orange_pink;
+ }
+ return gradientColors[value as GradientKey];
+ }
+
+ case "solid": {
+ if (value === "transparent") {
+ return "transparent";
+ }
+ if (
+ typeof value === "string" &&
+ (value.startsWith("#") || value.startsWith("rgb"))
+ ) {
+ return value;
+ }
+ const color = solidColors[value as SolidColorKey];
+ return color || "#ffffff";
+ }
+
+ case "image":
+ return `url(${value})`;
+
+ default:
+ return gradientColors.vibrant_orange_pink;
+ }
+};
+
+export const getBackgroundCSS = (
+ config: BackgroundConfig
+): React.CSSProperties => {
+ const { type, value, opacity = 1 } = config;
+
+ switch (type) {
+ case "gradient": {
+ let gradient: string;
+
+ if (typeof value === "string" && value.startsWith("mesh:")) {
+ const meshKey = value.replace("mesh:", "") as MeshGradientKey;
+ gradient = meshGradients[meshKey] || gradientColors.vibrant_orange_pink;
+ } else if (typeof value === "string" && value.startsWith("magic:")) {
+ const magicKey = value.replace("magic:", "") as MagicGradientKey;
+ gradient =
+ magicGradients[magicKey] || gradientColors.vibrant_orange_pink;
+ } else {
+ gradient =
+ gradientColors[value as GradientKey] ||
+ gradientColors.vibrant_orange_pink;
+ }
+
+ return {
+ background: gradient,
+ opacity,
+ };
+ }
+
+ case "solid": {
+ // Handle transparent background
+ if (value === "transparent") {
+ return {
+ backgroundColor: "transparent",
+ opacity: 1,
+ };
+ }
+ // Handle direct color values (hex, rgb, rgba)
+ if (
+ typeof value === "string" &&
+ (value.startsWith("#") || value.startsWith("rgb"))
+ ) {
+ return {
+ backgroundColor: value,
+ opacity,
+ };
+ }
+ const color = solidColors[value as SolidColorKey] || "#ffffff";
+ return {
+ backgroundColor: color,
+ opacity,
+ };
+ }
+
+ case "image": {
+ // Local assets (from /public) are served directly
+ const isLocalPath = typeof value === "string" && value.startsWith("/");
+
+ // Check if it's a known R2 background path
+ const isR2Path =
+ typeof value === "string" &&
+ !isLocalPath &&
+ !value.startsWith("blob:") &&
+ !value.startsWith("http") &&
+ !value.startsWith("data:") &&
+ backgroundPaths.includes(value);
+
+ // Get the image URL (R2 URL if it's a known path, otherwise use as-is)
+ const imageUrl = isR2Path
+ ? getR2ImageUrl({ src: value })
+ : (value as string);
+
+ return {
+ backgroundImage: `url(${imageUrl})`,
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
+ opacity,
+ };
+ }
+
+ default:
+ return {
+ background: gradientColors.vibrant_orange_pink,
+ opacity,
+ };
+ }
+};
diff --git a/apps/dashboard/lib/constants/fonts.ts b/apps/dashboard/lib/constants/fonts.ts
new file mode 100644
index 0000000..7d41f50
--- /dev/null
+++ b/apps/dashboard/lib/constants/fonts.ts
@@ -0,0 +1,484 @@
+export interface FontFamily {
+ availableWeights: string[];
+ category:
+ | "system"
+ | "sans-serif"
+ | "serif"
+ | "display"
+ | "handwriting"
+ | "monospace";
+ cssVariable?: string; // CSS variable name for Next.js loaded fonts
+ description?: string; // Short description for UI
+ fallback: string;
+ id: string;
+ name: string;
+}
+
+export const fontFamilies: FontFamily[] = [
+ // ============ PREMIUM FONTS (Local) ============
+ {
+ id: "sf-pro-display",
+ name: "SF Pro Display",
+ category: "sans-serif",
+ fallback:
+ '"SF Pro Display", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Apple's premium font",
+ },
+
+ // ============ SYSTEM FONTS ============
+ {
+ id: "system",
+ name: "System Default",
+ category: "system",
+ fallback:
+ 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+ availableWeights: ["normal", "bold"],
+ description: "Native system font",
+ },
+ {
+ id: "arial",
+ name: "Arial",
+ category: "system",
+ fallback: "Arial, Helvetica, sans-serif",
+ availableWeights: ["normal", "bold"],
+ description: "Classic web-safe font",
+ },
+
+ // ============ MODERN SANS-SERIF ============
+ {
+ id: "inter",
+ name: "Inter",
+ category: "sans-serif",
+ cssVariable: "--font-inter",
+ fallback: "Inter, system-ui, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Modern UI favorite",
+ },
+ {
+ id: "geist",
+ name: "Geist",
+ category: "sans-serif",
+ cssVariable: "--font-geist-sans",
+ fallback: "Geist, system-ui, sans-serif",
+ availableWeights: ["normal", "500", "600", "bold"],
+ description: "Vercel's modern typeface",
+ },
+ {
+ id: "poppins",
+ name: "Poppins",
+ category: "sans-serif",
+ cssVariable: "--font-poppins",
+ fallback: "Poppins, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Geometric & friendly",
+ },
+ {
+ id: "space-grotesk",
+ name: "Space Grotesk",
+ category: "sans-serif",
+ cssVariable: "--font-space-grotesk",
+ fallback: "Space Grotesk, sans-serif",
+ availableWeights: ["300", "normal", "500", "600", "bold"],
+ description: "Tech & startup aesthetic",
+ },
+ {
+ id: "outfit",
+ name: "Outfit",
+ category: "sans-serif",
+ cssVariable: "--font-outfit",
+ fallback: "Outfit, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Modern & approachable",
+ },
+ {
+ id: "plus-jakarta-sans",
+ name: "Plus Jakarta Sans",
+ category: "sans-serif",
+ cssVariable: "--font-plus-jakarta-sans",
+ fallback: "Plus Jakarta Sans, sans-serif",
+ availableWeights: ["200", "300", "normal", "500", "600", "bold", "800"],
+ description: "Clean & professional",
+ },
+ {
+ id: "dm-sans",
+ name: "DM Sans",
+ category: "sans-serif",
+ cssVariable: "--font-dm-sans",
+ fallback: "DM Sans, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Geometric & readable",
+ },
+ {
+ id: "sora",
+ name: "Sora",
+ category: "sans-serif",
+ cssVariable: "--font-sora",
+ fallback: "Sora, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ ],
+ description: "Futuristic geometric",
+ },
+ {
+ id: "manrope",
+ name: "Manrope",
+ category: "sans-serif",
+ cssVariable: "--font-manrope",
+ fallback: "Manrope, sans-serif",
+ availableWeights: ["200", "300", "normal", "500", "600", "bold", "800"],
+ description: "Modern & versatile",
+ },
+ {
+ id: "raleway",
+ name: "Raleway",
+ category: "sans-serif",
+ cssVariable: "--font-raleway",
+ fallback: "Raleway, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Elegant sans-serif",
+ },
+ {
+ id: "montserrat",
+ name: "Montserrat",
+ category: "sans-serif",
+ cssVariable: "--font-montserrat",
+ fallback: "Montserrat, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Urban & modern",
+ },
+ {
+ id: "lexend",
+ name: "Lexend",
+ category: "sans-serif",
+ cssVariable: "--font-lexend",
+ fallback: "Lexend, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Bold geometric, great readability",
+ },
+ {
+ id: "work-sans",
+ name: "Work Sans",
+ category: "sans-serif",
+ cssVariable: "--font-work-sans",
+ fallback: "Work Sans, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Clean & optimized for screens",
+ },
+ {
+ id: "urbanist",
+ name: "Urbanist",
+ category: "sans-serif",
+ cssVariable: "--font-urbanist",
+ fallback: "Urbanist, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Modern geometric sans",
+ },
+ {
+ id: "albert-sans",
+ name: "Albert Sans",
+ category: "sans-serif",
+ cssVariable: "--font-albert-sans",
+ fallback: "Albert Sans, sans-serif",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ "900",
+ ],
+ description: "Geometric grotesk",
+ },
+
+ // ============ DISPLAY / CONDENSED ============
+ {
+ id: "oswald",
+ name: "Oswald",
+ category: "display",
+ cssVariable: "--font-oswald",
+ fallback: "Oswald, Impact, sans-serif",
+ availableWeights: ["200", "300", "normal", "500", "600", "bold"],
+ description: "Bold condensed",
+ },
+ {
+ id: "bebas-neue",
+ name: "Bebas Neue",
+ category: "display",
+ cssVariable: "--font-bebas-neue",
+ fallback: "Bebas Neue, Impact, sans-serif",
+ availableWeights: ["normal"],
+ description: "Impact headlines",
+ },
+ {
+ id: "righteous",
+ name: "Righteous",
+ category: "display",
+ cssVariable: "--font-righteous",
+ fallback: "Righteous, cursive",
+ availableWeights: ["normal"],
+ description: "Retro-modern display",
+ },
+
+ // ============ SERIF ============
+ {
+ id: "playfair-display",
+ name: "Playfair Display",
+ category: "serif",
+ cssVariable: "--font-playfair-display",
+ fallback: "Playfair Display, Georgia, serif",
+ availableWeights: ["normal", "500", "600", "bold", "800", "900"],
+ description: "Elegant display serif",
+ },
+ {
+ id: "lora",
+ name: "Lora",
+ category: "serif",
+ cssVariable: "--font-lora",
+ fallback: "Lora, Georgia, serif",
+ availableWeights: ["normal", "500", "600", "bold"],
+ description: "Contemporary serif",
+ },
+ {
+ id: "libre-baskerville",
+ name: "Libre Baskerville",
+ category: "serif",
+ cssVariable: "--font-libre-baskerville",
+ fallback: "Libre Baskerville, Georgia, serif",
+ availableWeights: ["normal", "bold"],
+ description: "Classic elegance",
+ },
+ {
+ id: "georgia",
+ name: "Georgia",
+ category: "serif",
+ fallback: "Georgia, Times, serif",
+ availableWeights: ["normal", "bold"],
+ description: "Classic web serif",
+ },
+
+ // ============ HANDWRITING / SCRIPT ============
+ {
+ id: "caveat",
+ name: "Caveat",
+ category: "handwriting",
+ cssVariable: "--font-caveat",
+ fallback: "Caveat, cursive",
+ availableWeights: ["normal", "500", "600", "bold"],
+ description: "Casual handwriting",
+ },
+ {
+ id: "pacifico",
+ name: "Pacifico",
+ category: "handwriting",
+ cssVariable: "--font-pacifico",
+ fallback: "Pacifico, cursive",
+ availableWeights: ["normal"],
+ description: "Retro brush script",
+ },
+ {
+ id: "dancing-script",
+ name: "Dancing Script",
+ category: "handwriting",
+ cssVariable: "--font-dancing-script",
+ fallback: "Dancing Script, cursive",
+ availableWeights: ["normal", "500", "600", "bold"],
+ description: "Elegant script",
+ },
+
+ // ============ MONOSPACE ============
+ {
+ id: "geist-mono",
+ name: "Geist Mono",
+ category: "monospace",
+ cssVariable: "--font-geist-mono",
+ fallback: "Geist Mono, monospace",
+ availableWeights: ["normal", "500", "600", "bold"],
+ description: "Modern code font",
+ },
+ {
+ id: "jetbrains-mono",
+ name: "JetBrains Mono",
+ category: "monospace",
+ cssVariable: "--font-jetbrains-mono",
+ fallback: "JetBrains Mono, monospace",
+ availableWeights: [
+ "100",
+ "200",
+ "300",
+ "normal",
+ "500",
+ "600",
+ "bold",
+ "800",
+ ],
+ description: "Developer favorite",
+ },
+ {
+ id: "fira-code",
+ name: "Fira Code",
+ category: "monospace",
+ cssVariable: "--font-fira-code",
+ fallback: "Fira Code, monospace",
+ availableWeights: ["300", "normal", "500", "600", "bold"],
+ description: "Code with ligatures",
+ },
+ {
+ id: "courier",
+ name: "Courier New",
+ category: "monospace",
+ fallback: "Courier New, Courier, monospace",
+ availableWeights: ["normal", "bold"],
+ description: "Classic typewriter",
+ },
+];
+
+// Group fonts by category for UI display
+export const fontCategories = {
+ "sans-serif": "Modern Sans-Serif",
+ display: "Display & Headlines",
+ serif: "Elegant Serif",
+ handwriting: "Handwriting & Script",
+ monospace: "Monospace & Code",
+ system: "System Fonts",
+} as const;
+
+export const getFontFamily = (id: string): FontFamily | undefined =>
+ fontFamilies.find((font) => font.id === id);
+
+export const getFontCSS = (fontId: string): string => {
+ const font = getFontFamily(fontId);
+ if (!font) {
+ return fontFamilies[0].fallback;
+ }
+
+ // If font has a CSS variable (Next.js loaded font), use it
+ if (font.cssVariable) {
+ return `var(${font.cssVariable}), ${font.fallback}`;
+ }
+
+ return font.fallback;
+};
+
+export const getAvailableFontWeights = (fontId: string): string[] => {
+ const font = getFontFamily(fontId);
+ if (!font) {
+ return ["normal", "bold"];
+ }
+
+ return font.availableWeights;
+};
+
+export const getFontsByCategory = (
+ category: FontFamily["category"]
+): FontFamily[] => fontFamilies.filter((font) => font.category === category);
diff --git a/apps/dashboard/lib/constants/gradient-colors.ts b/apps/dashboard/lib/constants/gradient-colors.ts
new file mode 100644
index 0000000..a4f7f04
--- /dev/null
+++ b/apps/dashboard/lib/constants/gradient-colors.ts
@@ -0,0 +1,107 @@
+export const gradientColors = {
+ vibrant_orange_pink: 'linear-gradient(135deg, rgb(255, 100, 50) 12.8%, rgb(255, 0, 101) 43.52%, rgb(123, 46, 255) 84.34%)',
+ peach_pink_purple: 'linear-gradient(135deg, rgb(255, 177, 122) 12.8%, rgb(233, 107, 189) 43.52%, rgb(123, 79, 255) 84.34%)',
+ cyan_blue_purple: 'linear-gradient(135deg, rgb(0, 255, 229) 12.8%, rgb(75, 108, 255) 43.52%, rgb(156, 31, 217) 84.34%)',
+ orange_pink_dark: 'linear-gradient(135deg, rgb(255, 184, 107) 12.8%, rgb(255, 69, 133) 43.52%, rgb(47, 28, 150) 84.34%)',
+ green_teal_navy: 'linear-gradient(135deg, rgb(71, 246, 132) 12.8%, rgb(0, 184, 169) 43.52%, rgb(24, 78, 104) 84.34%)',
+ pink_red_yellow: 'linear-gradient(135deg, rgb(255, 97, 230) 12.8%, rgb(255, 51, 51) 43.52%, rgb(255, 184, 0) 84.34%)',
+ cyan_blue_violet: 'linear-gradient(135deg, rgb(0, 255, 224) 12.8%, rgb(0, 102, 255) 43.52%, rgb(102, 0, 255) 84.34%)',
+ peach_pink_lavender: 'linear-gradient(135deg, rgb(255, 177, 122) 12.8%, rgb(255, 77, 109) 43.52%, rgb(132, 94, 194) 84.34%)',
+ lime_cyan_blue: 'linear-gradient(135deg, rgb(89, 255, 0) 12.8%, rgb(0, 255, 209) 43.52%, rgb(0, 102, 255) 84.34%)',
+ pink_red_burgundy: 'linear-gradient(135deg, rgb(255, 94, 154) 12.8%, rgb(255, 0, 61) 43.52%, rgb(144, 0, 72) 84.34%)',
+ blue_pink: 'linear-gradient(135deg, rgb(52, 148, 230), rgb(236, 110, 173))',
+ green_blue: 'linear-gradient(135deg, rgb(103, 178, 111), rgb(76, 162, 205))',
+ pink_orange: 'linear-gradient(135deg, rgb(238, 9, 121), rgb(255, 106, 0))',
+ teal_navy: 'linear-gradient(135deg, rgb(67, 198, 172), rgb(25, 22, 84))',
+ pink_burgundy: 'linear-gradient(135deg, rgb(243, 98, 101), rgb(150, 18, 118))',
+ blue_lavender: 'linear-gradient(135deg, rgb(97, 144, 232), rgb(167, 191, 232))',
+ green_navy: 'linear-gradient(135deg, rgb(52, 232, 158), rgb(15, 52, 67))',
+ lime_mint: 'linear-gradient(135deg, rgb(170, 255, 169), rgb(17, 255, 189))',
+ cyan_blue: 'linear-gradient(135deg, rgb(178, 254, 250), rgb(14, 210, 247))',
+ mint_sky: 'linear-gradient(135deg, rgb(132, 250, 176), rgb(143, 211, 244))',
+ lavender_pink: 'linear-gradient(135deg, rgb(166, 192, 254), rgb(246, 128, 132))',
+ purple_sky: 'linear-gradient(135deg, rgb(224, 195, 252), rgb(142, 197, 252))',
+ cyan_lime: 'linear-gradient(135deg, rgb(0, 201, 255), rgb(146, 254, 157))',
+ navy_blue: 'linear-gradient(135deg, rgb(63, 43, 150), rgb(168, 192, 255))',
+ blue_teal: 'linear-gradient(135deg, rgb(0, 147, 233), rgb(128, 208, 199))',
+ mint_yellow: 'linear-gradient(135deg, rgb(133, 255, 189), rgb(255, 251, 125))',
+ sky_blue: 'linear-gradient(135deg, rgb(171, 220, 255), rgb(3, 150, 255))',
+ green_cyan: 'linear-gradient(135deg, rgb(81, 207, 102), rgb(55, 213, 214))',
+ pink_purple_blue: 'linear-gradient(135deg, rgb(255, 60, 172), rgb(120, 75, 160), rgb(43, 134, 197))',
+ pink_cyan_lime: 'linear-gradient(135deg, rgb(250, 139, 255), rgb(43, 210, 255), rgb(43, 255, 136))',
+ blue_purple: 'linear-gradient(135deg, rgb(139, 198, 236), rgb(149, 153, 226))',
+ mint_pink: 'linear-gradient(135deg, rgb(62, 236, 172), rgb(238, 116, 225))',
+ blue_pink_dark: 'linear-gradient(135deg, rgb(2, 80, 197), rgb(212, 63, 141))',
+ pink_blue: 'linear-gradient(135deg, rgb(252, 70, 107), rgb(63, 94, 251))',
+ purple_pink_white: 'linear-gradient(135deg, rgb(115, 3, 192), rgb(236, 56, 188), rgb(253, 239, 249))',
+ blue_pink_yellow: 'linear-gradient(135deg, rgb(65, 88, 208), rgb(200, 80, 192), rgb(255, 204, 112))',
+ cyan_magenta: 'linear-gradient(135deg, rgb(0, 219, 222), rgb(252, 0, 255))',
+ peach_coral: 'linear-gradient(135deg, rgb(255, 154, 158), rgb(250, 208, 196))',
+ peach_purple: 'linear-gradient(135deg, rgb(246, 211, 101), rgb(253, 160, 133))',
+ pink_orange_light: 'linear-gradient(135deg, rgb(252, 203, 144), rgb(213, 126, 235))',
+ yellow_cyan: 'linear-gradient(135deg, rgb(255, 95, 109), rgb(255, 195, 113))',
+ pink_yellow: 'linear-gradient(135deg, rgb(253, 187, 45), rgb(34, 193, 195))',
+ pink_light: 'linear-gradient(135deg, rgb(212, 20, 90), rgb(251, 176, 59))',
+ peach_pink: 'linear-gradient(135deg, rgb(254, 225, 64), rgb(250, 112, 154))',
+ yellow_blue: 'linear-gradient(135deg, rgb(255, 117, 140), rgb(255, 126, 179))',
+ pink_light_2: 'linear-gradient(135deg, rgb(240, 147, 251), rgb(245, 87, 108))',
+ yellow_pink: 'linear-gradient(135deg, rgb(255, 236, 210), rgb(252, 182, 159))',
+ pink_light_3: 'linear-gradient(135deg, rgb(161, 140, 209), rgb(251, 194, 235))',
+ peach_light: 'linear-gradient(135deg, rgb(169, 201, 255), rgb(255, 187, 236))',
+ yellow_blue_2: 'linear-gradient(135deg, rgb(101, 253, 240), rgb(29, 111, 163))',
+ rainbow: 'linear-gradient(135deg, rgb(255, 154, 139), rgb(255, 106, 136), rgb(255, 153, 172))',
+ gray_red: 'linear-gradient(135deg, rgb(251, 218, 97), rgb(255, 90, 205))',
+ navy_purple: 'linear-gradient(135deg, rgb(255, 184, 184), rgb(255, 184, 209), rgb(255, 184, 233))',
+ purple_orange: 'linear-gradient(135deg, rgb(250, 215, 161), rgb(233, 109, 113))',
+ purple_green: 'linear-gradient(135deg, rgb(255, 210, 111), rgb(54, 119, 255))',
+ blue_cyan: 'linear-gradient(135deg, rgb(5, 25, 55), rgb(0, 77, 122), rgb(0, 135, 147), rgb(0, 191, 114), rgb(168, 235, 18))',
+ purple_yellow: 'linear-gradient(135deg, rgb(51, 51, 51), rgb(221, 24, 24))',
+ navy_cyan: 'linear-gradient(135deg, rgb(15, 12, 41), rgb(48, 43, 99), rgb(36, 36, 62))',
+ slate_blue: 'linear-gradient(135deg, rgb(35, 7, 77), rgb(204, 83, 51))',
+ slate_lavender: 'linear-gradient(135deg, rgb(93, 65, 87), rgb(168, 202, 186))',
+ teal_green: 'linear-gradient(135deg, rgb(26, 41, 128), rgb(38, 208, 206))',
+ blue_orange: 'linear-gradient(135deg, rgb(75, 18, 72), rgb(240, 194, 123))',
+ pink_blue_2: 'linear-gradient(135deg, rgb(0, 0, 70), rgb(28, 181, 224))',
+ purple_pink: 'linear-gradient(135deg, rgb(22, 34, 42), rgb(58, 96, 115))',
+ purple_yellow_2: 'linear-gradient(135deg, rgb(31, 28, 44), rgb(146, 141, 171))',
+ navy_cyan_2: 'linear-gradient(135deg, rgb(17, 153, 142), rgb(56, 239, 125))',
+ purple_blue: 'linear-gradient(135deg, rgb(16, 141, 199), rgb(239, 142, 56))',
+ blue_yellow: 'linear-gradient(135deg, rgb(252, 92, 125), rgb(106, 130, 251))',
+ green_lime: 'linear-gradient(135deg, rgb(131, 77, 155), rgb(208, 78, 214))',
+ blue_green: 'linear-gradient(135deg, rgb(77, 160, 176), rgb(211, 157, 56))',
+ red_yellow: 'linear-gradient(135deg, rgb(86, 20, 176), rgb(219, 214, 92))',
+ cyan_white: 'linear-gradient(135deg, rgb(29, 151, 108), rgb(147, 249, 185))',
+ yellow_cyan_2: 'linear-gradient(135deg, rgb(33, 147, 176), rgb(109, 213, 237))',
+ lime_yellow: 'linear-gradient(135deg, rgb(204, 43, 94), rgb(117, 58, 136))',
+ purple_peach: 'linear-gradient(135deg, rgb(0, 70, 127), rgb(165, 204, 130))',
+ purple_cream: 'linear-gradient(135deg, rgb(248, 54, 0), rgb(249, 212, 35))',
+ peach_pink_2: 'linear-gradient(135deg, rgb(0, 255, 161), rgb(0, 255, 255))',
+ mint_pink_2: 'linear-gradient(135deg, rgb(240, 255, 0), rgb(88, 207, 251))',
+ beige_purple: 'linear-gradient(135deg, rgb(255, 249, 91), rgb(255, 147, 15))',
+ yellow_green: 'linear-gradient(135deg, rgb(252, 82, 150), rgb(246, 112, 98))',
+ pink_dark: 'linear-gradient(135deg, rgb(123, 255, 0), rgb(60, 213, 0))',
+ blue_yellow_2: 'linear-gradient(135deg, rgb(255, 0, 204), rgb(51, 51, 153))',
+ purple_magenta: 'linear-gradient(135deg, rgb(255, 19, 97), rgb(255, 248, 0))',
+ orange_red: 'linear-gradient(135deg, rgb(64, 224, 208), rgb(255, 140, 0), rgb(255, 0, 128))',
+ blue_cyan_lime: 'linear-gradient(135deg, rgb(138, 35, 135), rgb(233, 64, 87), rgb(242, 113, 33))',
+ blue_purple_2: 'linear-gradient(135deg, rgb(255, 239, 186), rgb(255, 255, 255))',
+ red_orange: 'linear-gradient(135deg, rgb(161, 255, 206), rgb(250, 255, 209))',
+ gradient_88: 'linear-gradient(135deg, rgb(243, 249, 167), rgb(202, 197, 49))',
+ gradient_89: 'linear-gradient(135deg, rgb(221, 214, 243), rgb(250, 172, 168))',
+ gradient_90: 'linear-gradient(135deg, rgb(232, 219, 252), rgb(248, 249, 210))',
+ gradient_91: 'linear-gradient(135deg, rgb(238, 205, 163), rgb(239, 98, 159))',
+ gradient_92: 'linear-gradient(135deg, rgb(201, 255, 191), rgb(255, 175, 189))',
+ gradient_93: 'linear-gradient(135deg, rgb(232, 203, 192), rgb(99, 111, 164))',
+ gradient_94: 'linear-gradient(135deg, rgb(220, 227, 91), rgb(69, 182, 73))',
+ gradient_95: 'linear-gradient(135deg, rgb(255, 0, 153), rgb(73, 50, 64))',
+ gradient_96: 'linear-gradient(135deg, rgb(0, 79, 249), rgb(255, 249, 76))',
+ gradient_97: 'linear-gradient(135deg, rgb(127, 0, 255), rgb(225, 0, 255))',
+ gradient_98: 'linear-gradient(135deg, rgb(253, 200, 48), rgb(243, 115, 53))',
+ gradient_99: 'linear-gradient(135deg, rgb(237, 33, 58), rgb(147, 41, 30))',
+ gradient_100: 'linear-gradient(135deg, rgb(31, 162, 255), rgb(18, 216, 250), rgb(166, 255, 203))',
+ gradient_101: 'linear-gradient(135deg, rgb(69, 104, 220), rgb(176, 106, 179))',
+ gradient_102: 'linear-gradient(135deg, rgb(255, 81, 47), rgb(221, 36, 118))',
+};
+
+export type GradientKey = keyof typeof gradientColors;
+
diff --git a/apps/dashboard/lib/constants/index.ts b/apps/dashboard/lib/constants/index.ts
new file mode 100644
index 0000000..261230c
--- /dev/null
+++ b/apps/dashboard/lib/constants/index.ts
@@ -0,0 +1,7 @@
+/** biome-ignore-all lint/performance/noBarrelFile:
+ MOCKUP_DEFINITIONS.find((def) => def.id === id);
+
+export const getMockupsByType = (
+ type: "iphone" | "macbook" | "imac" | "iwatch"
+): MockupDefinition[] => MOCKUP_DEFINITIONS.filter((def) => def.type === type);
diff --git a/apps/dashboard/lib/constants/overlays.ts b/apps/dashboard/lib/constants/overlays.ts
new file mode 100644
index 0000000..2ffa0ef
--- /dev/null
+++ b/apps/dashboard/lib/constants/overlays.ts
@@ -0,0 +1,7 @@
+/**
+ * Available anime character overlay images
+ * Using R2 paths for delivery
+ */
+import { OVERLAY_PATHS } from "@/lib/r2/r2-overlays";
+
+export const OVERLAY_IMAGES = OVERLAY_PATHS as readonly string[];
diff --git a/apps/dashboard/lib/constants/presets.ts b/apps/dashboard/lib/constants/presets.ts
new file mode 100644
index 0000000..38d6804
--- /dev/null
+++ b/apps/dashboard/lib/constants/presets.ts
@@ -0,0 +1,339 @@
+import type { ImageBorder, ImageShadow } from "@/lib/store";
+import type { AspectRatioKey } from "./aspect-ratios";
+import type { BackgroundConfig } from "./backgrounds";
+
+export interface PresetConfig {
+ aspectRatio: AspectRatioKey;
+ backgroundBlur?: number;
+ backgroundBorderRadius: number;
+ backgroundConfig: BackgroundConfig;
+ backgroundNoise?: number;
+ borderRadius: number;
+ description: string;
+ id: string;
+ imageBorder: ImageBorder;
+ imageOpacity: number;
+ imageScale: number;
+ imageShadow: ImageShadow;
+ name: string;
+ perspective3D?: {
+ perspective: number;
+ rotateX: number;
+ rotateY: number;
+ rotateZ: number;
+ translateX: number;
+ translateY: number;
+ scale: number;
+ };
+ shadowOverlay?: {
+ src: string;
+ opacity: number;
+ };
+}
+
+export const presets: PresetConfig[] = [
+ // 1. Spotlight - Dramatic dark with focused light
+ {
+ id: "spotlight",
+ name: "Spotlight",
+ description: "Dramatic dark with focused attention",
+ aspectRatio: "16_9",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/mono_dark_distortion_2.webp",
+ opacity: 1,
+ },
+ borderRadius: 8,
+ backgroundBorderRadius: 0,
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: true,
+ width: 8,
+ color: "#1a1a1a",
+ type: "arc-dark",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 40,
+ offsetX: 10,
+ offsetY: 10,
+ spread: 20,
+ color: "rgba(255, 255, 255, 0.08)",
+ opacity: 0.5,
+ },
+ backgroundBlur: 4,
+ backgroundNoise: 0,
+ },
+
+ // 8. Magazine Flatlay - Lying on textured surface with leaf shadows
+ {
+ id: "magazine-flatlay",
+ name: "Magazine Flatlay",
+ description: "Isometric flatlay with leaf shadows",
+ aspectRatio: "16_9",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/paper/26.webp",
+ opacity: 1,
+ },
+ borderRadius: 16,
+ backgroundBorderRadius: 0,
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: true,
+ width: 3,
+ color: "#ffffff",
+ type: "photograph",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 20,
+ offsetX: 10,
+ offsetY: 15,
+ spread: 0,
+ color: "rgba(0, 0, 0, 0.4)",
+ opacity: 0.5,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 15,
+ perspective3D: {
+ perspective: 2400,
+ rotateX: 45,
+ rotateY: 0,
+ rotateZ: -45,
+ translateX: 0,
+ translateY: -5,
+ scale: 0.9,
+ },
+ shadowOverlay: {
+ src: "/overlay-shadow/041.webp",
+ opacity: 0.2,
+ },
+ },
+
+ // 2. Lifted - Floating with hard shadow
+ {
+ id: "lifted",
+ name: "Lifted",
+ description: "Bold floating effect with hard shadow",
+ aspectRatio: "1_1",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/loupe-mono-light.webp",
+ opacity: 1,
+ },
+ borderRadius: 16,
+ backgroundBorderRadius: 0,
+ imageOpacity: 1,
+ imageScale: 75,
+ imageBorder: {
+ enabled: false,
+ width: 0,
+ color: "#ffffff",
+ type: "none",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 2,
+ offsetX: 20,
+ offsetY: 20,
+ spread: 0,
+ color: "rgba(0, 0, 0, 0)",
+ opacity: 0,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ shadowOverlay: {
+ src: "/overlay-shadow/017.webp",
+ opacity: 0.5,
+ },
+ },
+
+ // 3. Neon Dreams - Vibrant with colored glow
+ {
+ id: "neon-dreams",
+ name: "Neon Dreams",
+ description: "Vibrant glow for creative content",
+ aspectRatio: "16_9",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/chromatic_dark_2.webp",
+ opacity: 1,
+ },
+ borderRadius: 16,
+ backgroundBorderRadius: 20,
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: true,
+ width: 8,
+ color: "rgba(255,255,255,0.1)",
+ type: "arc-dark",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 40,
+ offsetX: 0,
+ offsetY: 20,
+ spread: 0,
+ color: "#401d90",
+ opacity: 0.5,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ },
+
+ // 4. Editorial - Clean magazine style
+ {
+ id: "editorial",
+ name: "Editorial",
+ description: "Clean magazine-style presentation",
+ aspectRatio: "4_5",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/paper/27.webp",
+ opacity: 1,
+ },
+ borderRadius: 0,
+ backgroundBorderRadius: 0,
+ imageOpacity: 1,
+ imageScale: 88,
+ imageBorder: {
+ enabled: true,
+ width: 8,
+ color: "#ffffff",
+ type: "none",
+ },
+ imageShadow: {
+ enabled: false,
+ blur: 0,
+ offsetX: 0,
+ offsetY: 0,
+ spread: 0,
+ color: "rgba(0, 0, 0, 0)",
+ opacity: 0,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 25,
+ shadowOverlay: {
+ src: "/overlay-shadow/019.webp",
+ opacity: 0.5,
+ },
+ },
+
+ // 9. Desktop View - Tilted on magic gradient
+ {
+ id: "desktop-view",
+ name: "Desktop View",
+ description: "Tilted view with magic gradient",
+ aspectRatio: "16_9",
+ backgroundConfig: {
+ type: "gradient",
+ value: "magic:magic_teal_dots",
+ opacity: 1,
+ },
+ borderRadius: 8,
+ backgroundBorderRadius: 0,
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: true,
+ width: 8,
+ color: "#2a2a2a",
+ type: "arc-dark",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 20,
+ offsetX: -15,
+ offsetY: 25,
+ spread: -10,
+ color: "rgba(0, 0, 0, 0.5)",
+ opacity: 0.5,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ perspective3D: {
+ perspective: 2400,
+ rotateX: 0,
+ rotateY: 0,
+ rotateZ: -8,
+ translateX: 0,
+ translateY: 0,
+ scale: 0.95,
+ },
+ },
+
+ // 5. Glass Card - Modern glassmorphism
+ {
+ id: "glass-card",
+ name: "Glass Card",
+ description: "Modern frosted glass effect",
+ aspectRatio: "16_9",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/mesh/Peak.webp",
+ opacity: 1,
+ },
+ borderRadius: 24,
+ backgroundBorderRadius: 32,
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: true,
+ width: 8,
+ color: "rgba(255,255,255,0.2)",
+ type: "arc-dark",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 40,
+ offsetX: 0,
+ offsetY: 30,
+ spread: -15,
+ color: "rgba(0, 0, 0, 0.3)",
+ opacity: 0.5,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 30,
+ },
+
+ // 7. Sunset Fade - Warm gradient vibes
+ {
+ id: "sunset-fade",
+ name: "Sunset Fade",
+ description: "Warm tones for lifestyle content",
+ aspectRatio: "og_image",
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/blushing-fire.webp",
+ opacity: 1,
+ },
+ borderRadius: 20,
+ backgroundBorderRadius: 24,
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: true,
+ width: 8,
+ color: "#ffffff",
+ type: "arc-light",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 50,
+ offsetX: 0,
+ offsetY: 25,
+ spread: -10,
+ color: "rgba(0, 0, 0, 0.25)",
+ opacity: 0.5,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ },
+];
+
+export const getPresetById = (id: string): PresetConfig | undefined =>
+ presets.find((preset) => preset.id === id);
diff --git a/apps/dashboard/lib/constants/solid-colors.ts b/apps/dashboard/lib/constants/solid-colors.ts
new file mode 100644
index 0000000..baf2631
--- /dev/null
+++ b/apps/dashboard/lib/constants/solid-colors.ts
@@ -0,0 +1,51 @@
+export const solidColors = {
+ // Row 1
+ white: '#ffffff',
+ very_light_gray: '#e5e5e5',
+ medium_light_gray: '#b3b3b3',
+ dark_gray: '#333333',
+
+ // Row 2
+ dark_charcoal: '#4a4a4a',
+ darker_charcoal: '#2a2a2a',
+ black: '#000000',
+ coral_red: '#ff6b6b',
+ bright_lime: '#32cd32',
+
+ // Row 3
+ orange: '#ffa500',
+ bright_yellow: '#ffff00',
+ light_olive_green: '#b8d433',
+ medium_green: '#4caf50',
+ light_pastel_pink: '#ffb3d9',
+
+ // Row 4
+ medium_green_2: '#66bb6a',
+ light_pastel_pink_2: '#ffc0e1',
+ light_peach: '#ffd9b3',
+ light_beige: '#fff5e6',
+ light_teal: '#80d4c7',
+
+ // Row 5
+ light_yellow: '#fffacd',
+ light_mint_green: '#c8f7c8',
+ light_teal_2: '#7fcdcd',
+ medium_blue: '#4a90e2',
+ medium_purple_blue: '#8b7ec8',
+
+ // Row 6
+ medium_blue_2: '#5dade2',
+ medium_purple_blue_2: '#9b7ec8',
+ darker_purple: '#6a5acd',
+ bright_fuchsia: '#ff00ff',
+ light_mint_green_2: '#b3ffb3',
+
+ // Row 7
+ light_pastel_blue: '#b3d9ff',
+ light_lavender: '#d4b3ff',
+ light_pastel_pink_3: '#ffb3d9',
+ light_pastel_pink_4: '#ffc0e1',
+} as const;
+
+export type SolidColorKey = keyof typeof solidColors;
+
diff --git a/apps/dashboard/lib/export/export-utils.ts b/apps/dashboard/lib/export/export-utils.ts
new file mode 100644
index 0000000..df503d5
--- /dev/null
+++ b/apps/dashboard/lib/export/export-utils.ts
@@ -0,0 +1,462 @@
+/**
+ * Export utility functions for style conversion and canvas configuration
+ */
+
+import { exportWorkerService } from "@/lib/workers/export-worker-service";
+
+/**
+ * Convert CSS variables and computed styles to RGB
+ */
+export function convertStylesToRGB(element: HTMLElement, doc: Document): void {
+ const win = doc.defaultView || (doc as any).parentWindow;
+ if (!win) {
+ return;
+ }
+
+ try {
+ const computedStyle = win.getComputedStyle(element);
+ const allProps = [
+ "color",
+ "backgroundColor",
+ "borderColor",
+ "borderTopColor",
+ "borderRightColor",
+ "borderBottomColor",
+ "borderLeftColor",
+ "outlineColor",
+ "boxShadow",
+ "textShadow",
+ "background",
+ "backgroundImage",
+ "backgroundColor",
+ "fill",
+ "stroke",
+ ];
+
+ // Convert all relevant CSS properties
+ for (const prop of allProps) {
+ try {
+ const value = computedStyle.getPropertyValue(prop);
+ if (value && (value.includes("oklch") || value.includes("var("))) {
+ const computed = (computedStyle as any)[prop];
+ if (
+ computed &&
+ computed !== "rgba(0, 0, 0, 0)" &&
+ computed !== "transparent" &&
+ computed !== "none" &&
+ !computed.includes("oklch")
+ ) {
+ element.style.setProperty(prop, computed, "important");
+ }
+ }
+ } catch (e) {
+ // Ignore errors for individual properties
+ }
+ }
+
+ // Also check inline styles
+ if (element.style?.cssText) {
+ const cssText = element.style.cssText;
+ if (cssText.includes("oklch") || cssText.includes("var(")) {
+ // Re-apply computed styles
+ for (const prop of allProps) {
+ try {
+ const computed = (computedStyle as any)[prop];
+ if (computed && !computed.includes("oklch")) {
+ element.style.setProperty(prop, computed, "important");
+ }
+ } catch (e) {
+ // Ignore errors
+ }
+ }
+ }
+ }
+ } catch (e) {
+ // Ignore errors
+ }
+
+ // Convert all children recursively
+ for (const child of Array.from(element.children)) {
+ if (child instanceof HTMLElement) {
+ convertStylesToRGB(child, doc);
+ }
+ }
+}
+
+/**
+ * Inject CSS overrides to replace oklch variables
+ */
+export function injectRGBOverrides(doc: Document): void {
+ // Remove or disable stylesheets that might contain oklch
+ const stylesheets = Array.from(doc.styleSheets);
+ for (const sheet of stylesheets) {
+ try {
+ if (sheet.href?.includes("globals.css")) {
+ try {
+ (sheet as any).disabled = true;
+ } catch (e) {
+ // Ignore
+ }
+ }
+ } catch (e) {
+ // Ignore cross-origin errors
+ }
+ }
+
+ // Inject CSS overrides with high specificity
+ const style = doc.createElement("style");
+ style.id = "oklch-rgb-converter";
+ style.textContent = `
+ :root, :root * {
+ --background: rgb(255, 255, 255) !important;
+ --foreground: rgb(33, 33, 33) !important;
+ --card: rgb(255, 255, 255) !important;
+ --card-foreground: rgb(33, 33, 33) !important;
+ --popover: rgb(255, 255, 255) !important;
+ --popover-foreground: rgb(33, 33, 33) !important;
+ --primary: rgb(37, 37, 37) !important;
+ --primary-foreground: rgb(251, 251, 251) !important;
+ --secondary: rgb(247, 247, 247) !important;
+ --secondary-foreground: rgb(37, 37, 37) !important;
+ --muted: rgb(247, 247, 247) !important;
+ --muted-foreground: rgb(140, 140, 140) !important;
+ --accent: rgb(247, 247, 247) !important;
+ --accent-foreground: rgb(37, 37, 37) !important;
+ --destructive: rgb(239, 68, 68) !important;
+ --border: rgb(237, 237, 237) !important;
+ --input: rgb(237, 237, 237) !important;
+ --ring: rgb(180, 180, 180) !important;
+ --sidebar: rgb(251, 251, 251) !important;
+ --sidebar-foreground: rgb(33, 33, 33) !important;
+ --sidebar-primary: rgb(37, 37, 37) !important;
+ --sidebar-primary-foreground: rgb(251, 251, 251) !important;
+ --sidebar-accent: rgb(247, 247, 247) !important;
+ --sidebar-accent-foreground: rgb(37, 37, 37) !important;
+ --sidebar-border: rgb(237, 237, 237) !important;
+ --sidebar-ring: rgb(180, 180, 180) !important;
+ }
+ *:not(img) {
+ border-color: rgb(237, 237, 237) !important;
+ outline-color: rgba(180, 180, 180, 0.5) !important;
+ }
+ `;
+
+ const head =
+ doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement;
+ if (head) {
+ head.insertBefore(style, head.firstChild);
+ }
+}
+
+/**
+ * Preserve image styles from original document
+ */
+export function preserveImageStyles(
+ img: HTMLImageElement,
+ clonedDoc: Document
+): void {
+ // Force display for images that might be hidden
+ if (img.style.display === "none") {
+ img.style.display = "";
+ }
+
+ // Preserve border styles - get from original document and apply with !important
+ const originalImg = document.querySelector(
+ `img[src="${img.getAttribute("src")}"]`
+ );
+ if (originalImg && originalImg instanceof HTMLElement) {
+ const originalStyle = window.getComputedStyle(originalImg);
+
+ // Get border properties
+ const borderTop = originalStyle.borderTop;
+ const borderRight = originalStyle.borderRight;
+ const borderBottom = originalStyle.borderBottom;
+ const borderLeft = originalStyle.borderLeft;
+ const borderRadius = originalStyle.borderRadius;
+ const boxShadow = originalStyle.boxShadow;
+ const opacity = originalStyle.opacity;
+ const transform = originalStyle.transform;
+
+ // Get border colors separately
+ const borderTopColor = originalStyle.borderTopColor;
+ const borderRightColor = originalStyle.borderRightColor;
+ const borderBottomColor = originalStyle.borderBottomColor;
+ const borderLeftColor = originalStyle.borderLeftColor;
+
+ if (
+ borderTop &&
+ borderTop !== "0px none rgb(0, 0, 0)" &&
+ borderTop !== "0px none"
+ ) {
+ const borderTopWidth = originalStyle.borderTopWidth;
+ const borderTopStyle = originalStyle.borderTopStyle;
+ if (borderTopWidth !== "0px" && borderTopStyle !== "none") {
+ const borderValue = `${borderTopWidth} ${borderTopStyle} ${borderTopColor}`;
+ img.style.setProperty("border-top", borderValue, "important");
+ }
+ }
+ if (
+ borderRight &&
+ borderRight !== "0px none rgb(0, 0, 0)" &&
+ borderRight !== "0px none"
+ ) {
+ const borderRightWidth = originalStyle.borderRightWidth;
+ const borderRightStyle = originalStyle.borderRightStyle;
+ if (borderRightWidth !== "0px" && borderRightStyle !== "none") {
+ const borderValue = `${borderRightWidth} ${borderRightStyle} ${borderRightColor}`;
+ img.style.setProperty("border-right", borderValue, "important");
+ }
+ }
+ if (
+ borderBottom &&
+ borderBottom !== "0px none rgb(0, 0, 0)" &&
+ borderBottom !== "0px none"
+ ) {
+ const borderBottomWidth = originalStyle.borderBottomWidth;
+ const borderBottomStyle = originalStyle.borderBottomStyle;
+ if (borderBottomWidth !== "0px" && borderBottomStyle !== "none") {
+ const borderValue = `${borderBottomWidth} ${borderBottomStyle} ${borderBottomColor}`;
+ img.style.setProperty("border-bottom", borderValue, "important");
+ }
+ }
+ if (
+ borderLeft &&
+ borderLeft !== "0px none rgb(0, 0, 0)" &&
+ borderLeft !== "0px none"
+ ) {
+ const borderLeftWidth = originalStyle.borderLeftWidth;
+ const borderLeftStyle = originalStyle.borderLeftStyle;
+ if (borderLeftWidth !== "0px" && borderLeftStyle !== "none") {
+ const borderValue = `${borderLeftWidth} ${borderLeftStyle} ${borderLeftColor}`;
+ img.style.setProperty("border-left", borderValue, "important");
+ }
+ }
+ if (borderRadius && borderRadius !== "0px") {
+ img.style.setProperty("border-radius", borderRadius, "important");
+ }
+ if (boxShadow && boxShadow !== "none") {
+ img.style.setProperty("box-shadow", boxShadow, "important");
+ }
+ if (opacity && opacity !== "1") {
+ img.style.setProperty("opacity", opacity, "important");
+ }
+ if (transform && transform !== "none") {
+ img.style.setProperty("transform", transform, "important");
+ }
+ }
+}
+
+/**
+ * Convert SVG elements fill/stroke attributes
+ */
+export function convertSVGStyles(
+ targetElement: HTMLElement,
+ clonedDoc: Document
+): void {
+ const svgElements = targetElement.querySelectorAll("svg, [fill], [stroke]");
+ for (const svg of svgElements) {
+ if (svg instanceof HTMLElement || svg instanceof SVGElement) {
+ const fill = svg.getAttribute("fill");
+ const stroke = svg.getAttribute("stroke");
+
+ if (fill && (fill.includes("oklch") || fill.includes("var("))) {
+ const temp = clonedDoc.createElement("div");
+ temp.style.color = fill;
+ const computed = clonedDoc.defaultView?.getComputedStyle(temp).color;
+ if (computed && !computed.includes("oklch")) {
+ svg.setAttribute("fill", computed);
+ }
+ temp.remove();
+ }
+
+ if (stroke && (stroke.includes("oklch") || stroke.includes("var("))) {
+ const temp = clonedDoc.createElement("div");
+ temp.style.color = stroke;
+ const computed = clonedDoc.defaultView?.getComputedStyle(temp).color;
+ if (computed && !computed.includes("oklch")) {
+ svg.setAttribute("stroke", computed);
+ }
+ temp.remove();
+ }
+ }
+ }
+}
+
+/**
+ * Setup element for export with proper dimensions
+ */
+export function setupExportElement(
+ targetElement: HTMLElement,
+ exportWidth: number,
+ exportHeight: number,
+ clonedDoc: Document
+): void {
+ targetElement.style.width = `${exportWidth}px`;
+ targetElement.style.height = `${exportHeight}px`;
+ targetElement.style.maxWidth = "none";
+ targetElement.style.maxHeight = "none";
+
+ // Also ensure parent containers don't constrain the size
+ let parent = targetElement.parentElement;
+ while (parent && parent !== clonedDoc.body) {
+ if (parent.style) {
+ parent.style.width = "auto";
+ parent.style.height = "auto";
+ parent.style.maxWidth = "none";
+ parent.style.maxHeight = "none";
+ parent.style.display = "flex";
+ parent.style.alignItems = "center";
+ parent.style.justifyContent = "center";
+ }
+ parent = parent.parentElement;
+ }
+}
+
+/**
+ * Wait for all images to load
+ */
+export async function waitForImages(element: HTMLElement): Promise {
+ const images = element.getElementsByTagName("img");
+ const imagePromises = Array.from(images).map((img) => {
+ if (img.complete) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve, reject) => {
+ img.onload = () => resolve();
+ img.onerror = () => reject(new Error("Image failed to load"));
+ setTimeout(() => reject(new Error("Image load timeout")), 5000);
+ });
+ });
+
+ try {
+ await Promise.all(imagePromises);
+ } catch (error) {
+ console.warn("Some images failed to load, continuing with export:", error);
+ }
+}
+
+/**
+ * Generate Gaussian (normal) distributed random number using Box-Muller transform
+ * This creates more natural-looking noise compared to uniform random
+ */
+function gaussianRandom(mean = 0, stdDev = 1): number {
+ let u = 0,
+ v = 0;
+ while (u === 0) {
+ u = Math.random(); // Converting [0,1) to (0,1)
+ }
+ while (v === 0) {
+ v = Math.random();
+ }
+ const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
+ return z * stdDev + mean;
+}
+
+/**
+ * Generate a noise texture canvas with Gaussian-distributed noise (sync version for fallback)
+ * This creates realistic image grain/noise similar to film grain or sensor noise
+ *
+ * @param width - Canvas width in pixels
+ * @param height - Canvas height in pixels
+ * @param intensity - Noise intensity (0-1), controls the standard deviation
+ * @returns Canvas element with noise texture
+ */
+function generateNoiseTextureSync(
+ width: number,
+ height: number,
+ intensity: number
+): HTMLCanvasElement {
+ const canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ return canvas;
+ }
+
+ const imageData = ctx.createImageData(width, height);
+ const data = imageData.data;
+
+ // Intensity controls the standard deviation of the Gaussian distribution
+ // Higher intensity = more variation in pixel values
+ const stdDev = intensity * 50; // Scale to reasonable range (0-50)
+
+ // Generate Gaussian noise for each pixel
+ for (let i = 0; i < data.length; i += 4) {
+ // Generate Gaussian noise value centered at 128 (mid-gray)
+ const noise = gaussianRandom(128, stdDev);
+
+ // Clamp to valid RGB range [0, 255]
+ const value = Math.max(0, Math.min(255, Math.round(noise)));
+
+ // Apply same value to R, G, B for grayscale noise
+ data[i] = value; // R
+ data[i + 1] = value; // G
+ data[i + 2] = value; // B
+ data[i + 3] = 255; // A (fully opaque)
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+ return canvas;
+}
+
+/**
+ * Generate a noise texture canvas with Gaussian-distributed noise
+ * Uses Web Worker for heavy computation to prevent UI blocking
+ * Falls back to synchronous generation if worker is unavailable
+ *
+ * @param width - Canvas width in pixels
+ * @param height - Canvas height in pixels
+ * @param intensity - Noise intensity (0-1), controls the standard deviation
+ * @returns Canvas element with noise texture
+ */
+export function generateNoiseTexture(
+ width: number,
+ height: number,
+ intensity: number
+): HTMLCanvasElement {
+ // Return sync version for immediate use
+ // The async version is available via generateNoiseTextureAsync
+ return generateNoiseTextureSync(width, height, intensity);
+}
+
+/**
+ * Generate a noise texture canvas asynchronously using Web Worker
+ * This offloads heavy computation to a worker thread to prevent UI blocking
+ *
+ * @param width - Canvas width in pixels
+ * @param height - Canvas height in pixels
+ * @param intensity - Noise intensity (0-1), controls the standard deviation
+ * @returns Promise resolving to Canvas element with noise texture
+ */
+export async function generateNoiseTextureAsync(
+ width: number,
+ height: number,
+ intensity: number
+): Promise {
+ try {
+ // Use worker service for heavy computation
+ const imageData = await exportWorkerService.generateNoiseTexture(
+ width,
+ height,
+ intensity
+ );
+
+ // Convert ImageData to canvas
+ const canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ return generateNoiseTextureSync(width, height, intensity);
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+ return canvas;
+ } catch (error) {
+ console.warn("Async noise generation failed, using sync fallback:", error);
+ return generateNoiseTextureSync(width, height, intensity);
+ }
+}
diff --git a/apps/dashboard/lib/patterns.ts b/apps/dashboard/lib/patterns.ts
new file mode 100644
index 0000000..cc051e7
--- /dev/null
+++ b/apps/dashboard/lib/patterns.ts
@@ -0,0 +1,82 @@
+export function generatePattern(
+ type: string,
+ scale = 1,
+ spacing = 20,
+ color = "#000000",
+ rotation = 0,
+ blur = 0
+): HTMLCanvasElement {
+ const canvas = document.createElement("canvas");
+ const size = 100 * scale;
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ return canvas;
+ }
+
+ ctx.save();
+ ctx.translate(size / 2, size / 2);
+ ctx.rotate((rotation * Math.PI) / 180);
+ ctx.translate(-size / 2, -size / 2);
+
+ ctx.strokeStyle = color;
+ ctx.fillStyle = color;
+ ctx.lineWidth = 1;
+
+ switch (type) {
+ case "grid":
+ for (let i = 0; i <= size; i += spacing) {
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, size);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(0, i);
+ ctx.lineTo(size, i);
+ ctx.stroke();
+ }
+ break;
+ case "dots":
+ for (let x = spacing / 2; x < size; x += spacing) {
+ for (let y = spacing / 2; y < size; y += spacing) {
+ ctx.beginPath();
+ ctx.arc(x, y, 2, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+ break;
+ case "lines":
+ for (let i = 0; i <= size; i += spacing) {
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, size);
+ ctx.stroke();
+ }
+ break;
+ default:
+ // Default to grid
+ for (let i = 0; i <= size; i += spacing) {
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, size);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(0, i);
+ ctx.lineTo(size, i);
+ ctx.stroke();
+ }
+ }
+
+ ctx.restore();
+
+ if (blur > 0) {
+ ctx.filter = `blur(${blur}px)`;
+ const imageData = ctx.getImageData(0, 0, size, size);
+ ctx.putImageData(imageData, 0, 0);
+ ctx.filter = "none";
+ }
+
+ return canvas;
+}
diff --git a/apps/dashboard/lib/r2/index.ts b/apps/dashboard/lib/r2/index.ts
new file mode 100644
index 0000000..784cebc
--- /dev/null
+++ b/apps/dashboard/lib/r2/index.ts
@@ -0,0 +1,127 @@
+/**
+ * Cloudflare R2 Storage Utilities
+ *
+ * R2 is S3-compatible object storage. For serving static assets,
+ * we use public bucket URLs. For uploads, we use the S3 SDK.
+ */
+
+// Environment variables for R2
+// R2_PUBLIC_URL - The public URL for your R2 bucket (e.g., https://assets.yourdomain.com or https://pub-xxx.r2.dev)
+// R2_ACCOUNT_ID - Your Cloudflare account ID (for S3 API access)
+// R2_ACCESS_KEY_ID - R2 API token access key
+// R2_SECRET_ACCESS_KEY - R2 API token secret key
+// R2_BUCKET_NAME - Your R2 bucket name
+
+/**
+ * Map R2 paths to local public/ directory paths.
+ * R2 uses paths like "backgrounds/mac/file.jpg" but local files are at "mac/file.jpg"
+ */
+function mapR2PathToLocal(path: string): string {
+ // Remove leading slash if present
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path;
+
+ // Map R2 paths to local public/ paths
+ const mappings: Record = {
+ 'backgrounds/': '',
+ 'overlays/shadow/': 'overlay-shadow/',
+ 'overlays/arrow/': 'overlay/',
+ };
+
+ for (const [r2Prefix, localPrefix] of Object.entries(mappings)) {
+ if (cleanPath.startsWith(r2Prefix)) {
+ return `/${localPrefix}${cleanPath.slice(r2Prefix.length)}`;
+ }
+ }
+
+ // For paths that don't need mapping (e.g., "assets/...")
+ return `/${cleanPath}`;
+}
+
+/**
+ * Get the public URL for an R2 object.
+ * Uses a same-origin proxy path (/r2-assets/...) to avoid CORS issues
+ * during canvas capture (e.g. video export with domToCanvas).
+ * The Next.js rewrite in next.config.ts proxies these to the actual R2 URL.
+ *
+ * If R2 is not configured, falls back to serving assets from local public/ directory.
+ *
+ * @param path - The object path/key in the bucket (e.g., "backgrounds/image.jpg")
+ * @returns The proxied URL path or local public path
+ */
+export function getR2PublicUrl(path: string): string {
+ const publicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
+
+ if (!publicUrl) {
+ // Fall back to local public/ directory with path mapping
+ return mapR2PathToLocal(path);
+ }
+
+ // Remove leading slash from path if present
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path;
+
+ // Use same-origin proxy to avoid CORS issues with canvas capture
+ return `/r2-assets/${cleanPath}`;
+}
+
+/**
+ * Get an optimized image URL from R2
+ * Since R2 doesn't have built-in image transformations like Cloudinary,
+ * we return the original image URL. For optimization, consider using
+ * Cloudflare Images or a custom Worker with image resizing.
+ *
+ * @param options - Image options
+ * @returns The image URL
+ */
+export function getR2ImageUrl(options: {
+ src: string;
+ width?: number;
+ height?: number;
+ quality?: number | 'auto';
+ format?: 'auto' | 'webp' | 'avif' | 'jpg' | 'png';
+}): string {
+ const { src } = options;
+
+ // If it's already a full URL, return as-is
+ if (src.startsWith('http://') || src.startsWith('https://')) {
+ return src;
+ }
+
+ // If it's a blob URL or data URL, return as-is
+ if (src.startsWith('blob:') || src.startsWith('data:')) {
+ return src;
+ }
+
+ // Otherwise, construct the R2 public URL
+ return getR2PublicUrl(src);
+}
+
+/**
+ * R2 configuration type
+ */
+export interface R2Config {
+ accountId: string;
+ accessKeyId: string;
+ secretAccessKey: string;
+ bucketName: string;
+ publicUrl: string;
+}
+
+/**
+ * Get R2 configuration from environment variables
+ */
+export function getR2Config(): R2Config {
+ return {
+ accountId: process.env.R2_ACCOUNT_ID || '',
+ accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
+ bucketName: process.env.R2_BUCKET_NAME || '',
+ publicUrl: process.env.NEXT_PUBLIC_R2_PUBLIC_URL || '',
+ };
+}
+
+/**
+ * Check if R2 is properly configured
+ */
+export function isR2Configured(): boolean {
+ return !!process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
+}
diff --git a/apps/dashboard/lib/r2/r2-backgrounds.ts b/apps/dashboard/lib/r2/r2-backgrounds.ts
new file mode 100644
index 0000000..5df3ecc
--- /dev/null
+++ b/apps/dashboard/lib/r2/r2-backgrounds.ts
@@ -0,0 +1,183 @@
+/**
+ * R2 Background Assets Configuration
+ *
+ * These are the paths to background images stored in Cloudflare R2.
+ * The paths are relative to the R2 bucket root.
+ */
+
+import { getR2PublicUrl } from ".";
+
+export interface BackgroundCategory {
+ [category: string]: string[];
+}
+
+// Background image paths in R2 (with actual file extensions)
+export const backgroundCategories: BackgroundCategory = {
+ assets: [
+ "assets/asset-1.jpg",
+ "assets/asset-2.jpg",
+ "assets/asset-3.jpg",
+ "assets/asset-4.jpg",
+ "assets/asset-5.jpg",
+ "assets/asset-13.jpg",
+ "assets/asset-19.jpg",
+ ],
+ mac: [
+ "backgrounds/mac/mac-asset-1.jpeg",
+ "backgrounds/mac/mac-asset-2.jpg",
+ "backgrounds/mac/mac-asset-3.jpg",
+ "backgrounds/mac/mac-asset-4.jpg",
+ "backgrounds/mac/mac-asset-5.jpg",
+ "backgrounds/mac/mac-asset-6.jpeg",
+ "backgrounds/mac/mac-asset-7.png",
+ "backgrounds/mac/mac-asset-8.jpg",
+ "backgrounds/mac/mac-asset-9.jpg",
+ "backgrounds/mac/mac-asset-10.jpg",
+ ],
+ radiant: [
+ "backgrounds/radiant/radiant1.jpg",
+ "backgrounds/radiant/radiant2.jpg",
+ "backgrounds/radiant/radiant3.jpg",
+ "backgrounds/radiant/radiant4.jpg",
+ "backgrounds/radiant/radiant5.jpg",
+ "backgrounds/radiant/radiant6.jpg",
+ "backgrounds/radiant/radiant8.jpg",
+ "backgrounds/radiant/radiant9.jpg",
+ "backgrounds/radiant/radiant10.jpg",
+ ],
+ mesh: [
+ "backgrounds/mesh/mesh1.webp",
+ "backgrounds/mesh/mesh2.webp",
+ "backgrounds/mesh/mesh3.webp",
+ "backgrounds/mesh/mesh4.webp",
+ "backgrounds/mesh/mesh5.webp",
+ "backgrounds/mesh/mesh6.webp",
+ "backgrounds/mesh/mesh7.webp",
+ "backgrounds/mesh/mesh8.webp",
+ "backgrounds/mesh/Astra.webp",
+ "backgrounds/mesh/Bliss.webp",
+ "backgrounds/mesh/Burst.webp",
+ "backgrounds/mesh/Dusk.webp",
+ "backgrounds/mesh/Flash.webp",
+ "backgrounds/mesh/Ghost.webp",
+ "backgrounds/mesh/Helix.webp",
+ "backgrounds/mesh/Horizon.webp",
+ "backgrounds/mesh/Peak.webp",
+ ],
+ demo: [
+ "backgrounds/demo/demo-1.png",
+ "backgrounds/demo/demo-2.png",
+ "backgrounds/demo/demo-3.png",
+ "backgrounds/demo/demo-4.png",
+ "backgrounds/demo/demo-5.png",
+ "backgrounds/demo/demo-6.png",
+ "backgrounds/demo/demo-7.png",
+ "backgrounds/demo/demo-8.png",
+ "backgrounds/demo/demo-9.png",
+ "backgrounds/demo/demo-10.png",
+ "backgrounds/demo/demo-11.png",
+ ],
+ paper: [
+ "backgrounds/paper/01.webp",
+ "backgrounds/paper/02.webp",
+ "backgrounds/paper/03.webp",
+ "backgrounds/paper/21.webp",
+ "backgrounds/paper/26.webp",
+ "backgrounds/paper/27.webp",
+ "backgrounds/paper/31.webp",
+ "backgrounds/paper/47.webp",
+ ],
+ raycast: [
+ "backgrounds/raycast/autumnal-peach.webp",
+ "backgrounds/raycast/blob-red.webp",
+ "backgrounds/raycast/blob.webp",
+ "backgrounds/raycast/blossom-2.webp",
+ "backgrounds/raycast/blue_distortion_1.webp",
+ "backgrounds/raycast/blue_distortion_2.webp",
+ "backgrounds/raycast/blushing-fire.webp",
+ "backgrounds/raycast/bright-rain.webp",
+ "backgrounds/raycast/chromatic_dark_1.webp",
+ "backgrounds/raycast/chromatic_dark_2.webp",
+ "backgrounds/raycast/chromatic_light_1.webp",
+ "backgrounds/raycast/chromatic_light_2.webp",
+ "backgrounds/raycast/cube_mono.webp",
+ "backgrounds/raycast/cube_prod.webp",
+ "backgrounds/raycast/floss.webp",
+ "backgrounds/raycast/glass-rainbow.webp",
+ "backgrounds/raycast/good-vibes.webp",
+ "backgrounds/raycast/loupe-mono-light.webp",
+ "backgrounds/raycast/loupe.webp",
+ "backgrounds/raycast/mono_dark_distortion_1.webp",
+ "backgrounds/raycast/mono_dark_distortion_2.webp",
+ "backgrounds/raycast/mono_light_distortion_1.webp",
+ "backgrounds/raycast/moonrise.webp",
+ "backgrounds/raycast/red_distortion_2.webp",
+ "backgrounds/raycast/red_distortion_4.webp",
+ "backgrounds/raycast/rose-thorn.webp",
+ ],
+ pattern: [
+ "backgrounds/pattern/1.webp",
+ "backgrounds/pattern/2.webp",
+ "backgrounds/pattern/3.webp",
+ "backgrounds/pattern/4.webp",
+ "backgrounds/pattern/5.webp",
+ "backgrounds/pattern/6.webp",
+ "backgrounds/pattern/7.webp",
+ "backgrounds/pattern/8.webp",
+ "backgrounds/pattern/9.webp",
+ "backgrounds/pattern/10.webp",
+ "backgrounds/pattern/11.webp",
+ ],
+};
+
+// Flatten all background paths for easy lookup
+export const backgroundPaths: string[] =
+ Object.values(backgroundCategories).flat();
+
+// Background paths for auth pages
+export const SIGN_IN_BACKGROUND_PATH = "backgrounds/mac/mac-asset-7.png";
+export const SIGN_UP_BACKGROUND_PATH = "backgrounds/mac/mac-asset-2.jpg";
+
+/**
+ * Get full R2 URL for a background image
+ */
+export function getBackgroundUrl(path: string): string {
+ if (path.startsWith("/")) {
+ return path;
+ }
+ return getR2PublicUrl(path);
+}
+
+/**
+ * Get thumbnail URL for a background image
+ * Note: R2 doesn't support on-the-fly image transformations.
+ * Consider using Cloudflare Images or pre-generating thumbnails.
+ */
+export function getBackgroundThumbnailUrl(path: string): string {
+ // Local assets (from /public) are served as-is
+ if (path.startsWith("/")) {
+ return path;
+ }
+ return getR2PublicUrl(path);
+}
+
+/**
+ * Check if a path is a known background path
+ */
+export function isBackgroundPath(path: string): boolean {
+ return backgroundPaths.includes(path);
+}
+
+/**
+ * Get all backgrounds for a category
+ */
+export function getBackgroundsByCategory(category: string): string[] {
+ return backgroundCategories[category] || [];
+}
+
+/**
+ * Get all available categories
+ */
+export function getAvailableCategories(): string[] {
+ return Object.keys(backgroundCategories);
+}
diff --git a/apps/dashboard/lib/r2/r2-demo-images.ts b/apps/dashboard/lib/r2/r2-demo-images.ts
new file mode 100644
index 0000000..96e62ae
--- /dev/null
+++ b/apps/dashboard/lib/r2/r2-demo-images.ts
@@ -0,0 +1,27 @@
+/**
+ * R2 Demo Images Configuration
+ *
+ * These are the paths to demo images stored in Cloudflare R2.
+ */
+
+import { getR2PublicUrl } from ".";
+
+export const demoImagePaths: string[] = [
+ "backgrounds/demo/demo-1.png",
+ "backgrounds/demo/demo-2.png",
+ "backgrounds/demo/demo-3.png",
+ "backgrounds/demo/demo-4.png",
+ "backgrounds/demo/demo-5.png",
+ "backgrounds/demo/demo-6.png",
+ "backgrounds/demo/demo-11.png",
+ "backgrounds/demo/demo-8.png",
+ "backgrounds/demo/demo-9.png",
+ "backgrounds/demo/demo-10.png",
+];
+
+/**
+ * Get full R2 URL for a demo image
+ */
+export function getDemoImageUrl(path: string): string {
+ return getR2PublicUrl(path);
+}
diff --git a/apps/dashboard/lib/r2/r2-overlays.ts b/apps/dashboard/lib/r2/r2-overlays.ts
new file mode 100644
index 0000000..1a46854
--- /dev/null
+++ b/apps/dashboard/lib/r2/r2-overlays.ts
@@ -0,0 +1,107 @@
+/**
+ * R2 Overlay Assets Configuration
+ *
+ * These are the paths to overlay images stored in Cloudflare R2.
+ * The paths are relative to the R2 bucket root.
+ */
+
+import { getR2PublicUrl } from ".";
+
+/**
+ * Arrow overlay paths in R2
+ */
+export const ARROW_PATHS = [
+ "overlays/arrow/arrow-1.svg",
+ "overlays/arrow/arrow-2.svg",
+ "overlays/arrow/arrow-3.svg",
+ "overlays/arrow/arrow-4.svg",
+ "overlays/arrow/arrow-5.svg",
+ "overlays/arrow/arrow-6.svg",
+ "overlays/arrow/arrow-7.svg",
+ "overlays/arrow/arrow-8.svg",
+ "overlays/arrow/arrow-9.svg",
+ "overlays/arrow/arrow-10.svg",
+] as const;
+
+/**
+ * Shadow overlay paths in R2
+ */
+export const SHADOW_OVERLAY_PATHS = [
+ "overlays/shadow/001.webp",
+ "overlays/shadow/002.webp",
+ "overlays/shadow/007.webp",
+ "overlays/shadow/017.webp",
+ "overlays/shadow/019.webp",
+ "overlays/shadow/023.webp",
+ "overlays/shadow/031.webp",
+ "overlays/shadow/037.webp",
+ "overlays/shadow/041.webp",
+ "overlays/shadow/050.webp",
+ "overlays/shadow/053.webp",
+ "overlays/shadow/057.webp",
+ "overlays/shadow/063.webp",
+ "overlays/shadow/064.webp",
+ "overlays/shadow/082.webp",
+ "overlays/shadow/083.webp",
+ "overlays/shadow/088.webp",
+ "overlays/shadow/097.webp",
+ "overlays/shadow/099.webp",
+] as const;
+
+/**
+ * All overlay paths combined
+ */
+export const OVERLAY_PATHS = [...ARROW_PATHS, ...SHADOW_OVERLAY_PATHS] as const;
+
+export type ArrowPath = (typeof ARROW_PATHS)[number];
+export type ShadowOverlayPath = (typeof SHADOW_OVERLAY_PATHS)[number];
+export type OverlayPath = (typeof OVERLAY_PATHS)[number];
+
+/**
+ * Get full R2 URL for an overlay image
+ */
+export function getOverlayUrl(path: string): string {
+ return getR2PublicUrl(path);
+}
+
+/**
+ * Check if a path is a known overlay path
+ */
+export function isOverlayPath(path: string): boolean {
+ return (OVERLAY_PATHS as readonly string[]).includes(path);
+}
+
+/**
+ * Check if a path is an arrow overlay
+ */
+export function isArrowPath(path: string): boolean {
+ return (ARROW_PATHS as readonly string[]).includes(path);
+}
+
+/**
+ * Check if a path is a shadow overlay
+ */
+export function isShadowOverlayPath(path: string): boolean {
+ return (SHADOW_OVERLAY_PATHS as readonly string[]).includes(path);
+}
+
+/**
+ * Get all available overlay paths
+ */
+export function getAllOverlayPaths(): readonly string[] {
+ return OVERLAY_PATHS;
+}
+
+/**
+ * Get all arrow paths
+ */
+export function getAllArrowPaths(): readonly string[] {
+ return ARROW_PATHS;
+}
+
+/**
+ * Get all shadow overlay paths
+ */
+export function getAllShadowOverlayPaths(): readonly string[] {
+ return SHADOW_OVERLAY_PATHS;
+}
diff --git a/apps/dashboard/lib/store/export-utils.ts b/apps/dashboard/lib/store/export-utils.ts
new file mode 100644
index 0000000..042f7b9
--- /dev/null
+++ b/apps/dashboard/lib/store/export-utils.ts
@@ -0,0 +1,56 @@
+// import { domToCanvas } from "modern-screenshot";
+
+/**
+ * Export image with gradient background from an element
+ * @param elementId - The ID of the element to export
+ */
+export async function exportImageWithGradient(
+ elementId: string
+): Promise {
+ const element = document.getElementById(elementId);
+
+ if (!element) {
+ throw new Error(`Element with id "${elementId}" not found`);
+ }
+
+ try {
+ // Use modern-screenshot for better CSS fidelity
+ // const canvas = await domToCanvas(element, {
+ // backgroundColor: null, // Transparent background to preserve gradients
+ // scale: 2, // Higher quality
+ // width: element.scrollWidth,
+ // height: element.scrollHeight,
+ // });
+
+ // // Convert canvas to blob and download
+ // return new Promise((resolve, reject) => {
+ // canvas.toBlob((blob) => {
+ // if (!blob) {
+ // reject(new Error("Failed to create blob from canvas"));
+ // return;
+ // }
+
+ // // Create download link
+ // const url = URL.createObjectURL(blob);
+ // const link = document.createElement("a");
+ // link.href = url;
+ // link.download = `image-${Date.now()}.png`;
+ // document.body.appendChild(link);
+ // link.click();
+ // document.body.removeChild(link);
+
+ // // Clean up
+ // URL.revokeObjectURL(url);
+ // resolve();
+ // }, "image/png");
+ // });
+ return await Promise.reject(
+ new Error(
+ "domToCanvas is not implemented. Please uncomment the code and ensure modern-screenshot is installed."
+ )
+ );
+ } catch (error) {
+ console.error("Export failed:", error);
+ throw error;
+ }
+}
diff --git a/apps/dashboard/lib/store/index.ts b/apps/dashboard/lib/store/index.ts
new file mode 100644
index 0000000..95ec86c
--- /dev/null
+++ b/apps/dashboard/lib/store/index.ts
@@ -0,0 +1,2042 @@
+"use client";
+
+import React from "react";
+import { temporal } from "zundo";
+import { create } from "zustand";
+// import {
+// trackAnimationClipAdd,
+// trackAspectRatioChange,
+// trackBackgroundChange,
+// trackFrameApply,
+// trackImageUpload,
+// trackOverlayAdd,
+// } from "@/lib/analytics";
+// import {
+// ANIMATION_PRESETS,
+// clonePresetTracks,
+// getPresetById,
+// } from "@/lib/animation/presets";
+import type { AspectRatioKey } from "@/lib/constants/aspect-ratios";
+import type {
+ BackgroundConfig,
+ BackgroundType,
+} from "@/lib/constants/backgrounds";
+import {
+ type GradientKey,
+ gradientColors,
+} from "@/lib/constants/gradient-colors";
+import { solidColors } from "@/lib/constants/solid-colors";
+// import type {
+// AnimationClip,
+// AnimationTrack,
+// Keyframe,
+// TimelineState,
+// } from "@/types/animation";
+// import { DEFAULT_TIMELINE_STATE } from "@/types/animation";
+import type { Mockup } from "@/types/mockup";
+import { exportImageWithGradient } from "./export-utils";
+
+interface TextShadow {
+ blur: number;
+ color: string;
+ enabled: boolean;
+ offsetX: number;
+ offsetY: number;
+}
+
+export interface ImageFilters {
+ blur: number; // 0-20px
+ brightness: number; // 0-200 (100 = normal)
+ contrast: number; // 0-200 (100 = normal)
+ grayscale: number; // 0-100
+ hueRotate: number; // 0-360 degrees
+ invert: number; // 0-100
+ saturate: number; // 0-200 (100 = normal)
+ sepia: number; // 0-100
+}
+interface Slide {
+ duration: number;
+ id: string;
+ name: string | null;
+ src: string;
+}
+export interface TextOverlay {
+ color: string;
+ fontFamily: string;
+ fontSize: number;
+ fontWeight: string;
+ id: string;
+ isVisible: boolean;
+ opacity: number;
+ orientation: "horizontal" | "vertical";
+ position: { x: number; y: number };
+ text: string;
+ textShadow: TextShadow;
+}
+
+export interface ImageOverlay {
+ blur?: number; // Blur amount in pixels (0 = no blur)
+ flipX: boolean;
+ flipY: boolean;
+ id: string;
+ isCustom?: boolean; // Whether it's a custom uploaded overlay
+ isVisible: boolean;
+ layer?: "front" | "back"; // Render in front of or behind the main image
+ opacity: number;
+ position: { x: number; y: number }; // Position in pixels relative to canvas
+ rotation: number; // Rotation in degrees
+ size: number; // Size in pixels
+ src: string;
+}
+
+export interface BlurRegion {
+ blurAmount: number;
+ id: string;
+ isVisible: boolean;
+ position: { x: number; y: number };
+ size: { width: number; height: number };
+}
+
+export type AnnotationToolType =
+ | "arrow"
+ | "curved-arrow"
+ | "rectangle"
+ | "circle"
+ | "line"
+ | "blur";
+
+export interface AnnotationShape {
+ cx?: number;
+ cy?: number;
+ fillColor: string;
+ id: string;
+ isVisible: boolean;
+ opacity: number;
+ strokeColor: string;
+ strokeWidth: number;
+ type: AnnotationToolType;
+ x1: number;
+ x2: number;
+ y1: number;
+ y2: number;
+}
+
+export type ImageStylePreset =
+ | "default"
+ | "glass-light"
+ | "glass-dark"
+ | "outline"
+ | "border-light"
+ | "border-dark";
+export type ShadowPreset = "none" | "hug" | "soft" | "strong";
+
+export interface ImageBorder {
+ color: string;
+ enabled: boolean;
+ opacity?: number;
+ padding?: number;
+ title?: string;
+ type:
+ | "none"
+ | "arc-light"
+ | "arc-dark"
+ | "macos-light"
+ | "macos-dark"
+ | "windows-light"
+ | "windows-dark"
+ | "photograph"
+ | "glass-light"
+ | "glass-dark"
+ | "outline-light"
+ | "border-light"
+ | "border-dark";
+ width: number;
+}
+
+export interface ImageShadow {
+ blur: number;
+ color: string;
+ enabled: boolean;
+ offsetX: number;
+ offsetY: number;
+ opacity: number;
+ spread: number;
+}
+
+// Helper function to parse gradient string and extract colors
+function parseGradientColors(gradientStr: string): {
+ colorA: string;
+ colorB: string;
+ direction: number;
+} {
+ // Default fallback
+ let colorA = "#4168d0";
+ let colorB = "#c850c0";
+ let direction = 43;
+
+ try {
+ // Extract angle from linear-gradient(angle, ...)
+ // biome-ignore lint/performance/useTopLevelRegex: = 2) {
+ colorA = rgbMatches[0];
+ colorB = rgbMatches.at(-1) || colorB;
+ } else {
+ // Try hex colors
+ const hexMatches = gradientStr.match(/#[0-9A-Fa-f]{6}/g);
+ if (hexMatches && hexMatches.length >= 2) {
+ colorA = hexMatches[0];
+ colorB = hexMatches.at(-1) || colorB;
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing gradient string:", e);
+ // Use defaults
+ }
+
+ return { colorA, colorB, direction };
+}
+
+// helper function that omits setter types from EditorState and ImageState; only keeps the properties; excludes any functions
+export type OmitFunctions = {
+ // biome-ignore lint/suspicious/noExplicitAny: any ? never : K]: T[K];
+};
+
+export interface EditorState {
+ // Background state (for Konva)
+ background: {
+ mode: "solid" | "gradient";
+ colorA: string;
+ colorB: string;
+ gradientDirection: number;
+ };
+
+ // Canvas state
+ canvas: {
+ aspectRatio: "square" | "4:3" | "2:1" | "3:2" | "free";
+ padding: number;
+ };
+
+ // Frame state (same as imageBorder)
+ frame: {
+ enabled: boolean;
+ type:
+ | "none"
+ | "arc-light"
+ | "arc-dark"
+ | "macos-light"
+ | "macos-dark"
+ | "windows-light"
+ | "windows-dark"
+ | "photograph"
+ | "glass-light"
+ | "glass-dark"
+ | "outline-light"
+ | "border-light"
+ | "border-dark";
+ width: number;
+ color: string;
+ padding?: number;
+ title?: string;
+ opacity?: number;
+ };
+
+ // Noise state
+ noise: {
+ enabled: boolean;
+ type: string;
+ opacity: number;
+ };
+
+ // Pattern state
+ pattern: {
+ enabled: boolean;
+ type: string;
+ scale: number;
+ spacing: number;
+ color: string;
+ rotation: number;
+ blur: number;
+ opacity: number;
+ };
+ // Screenshot/image state
+ screenshot: {
+ src: string | null;
+ scale: number;
+ offsetX: number;
+ offsetY: number;
+ rotation: number;
+ radius: number;
+ };
+ setBackground: (background: Partial) => void;
+ setCanvas: (canvas: Partial) => void;
+ setFrame: (frame: Partial) => void;
+ setNoise: (noise: Partial) => void;
+ setPattern: (pattern: Partial) => void;
+
+ // Setters
+ setScreenshot: (screenshot: Partial) => void;
+ setShadow: (shadow: Partial) => void;
+
+ // Shadow state (for Konva)
+ shadow: {
+ enabled: boolean;
+ elevation: number;
+ side: "bottom" | "right" | "bottom-right";
+ softness: number;
+ spread: number;
+ color: string;
+ intensity: number;
+ offsetX: number;
+ offsetY: number;
+ };
+}
+
+// Create editor store
+export const useEditorStore = create((set, _get) => ({
+ screenshot: {
+ src: null,
+ scale: 1,
+ offsetX: 0,
+ offsetY: 0,
+ rotation: 0,
+ radius: 0,
+ },
+
+ background: {
+ mode: "gradient",
+ colorA: "#4168d0",
+ colorB: "#c850c0",
+ gradientDirection: 43,
+ },
+
+ shadow: {
+ enabled: true,
+ elevation: 12,
+ side: "bottom-right",
+ softness: 15,
+ spread: 3,
+ color: "",
+ intensity: 0.5,
+ offsetX: 5,
+ offsetY: 8,
+ },
+
+ pattern: {
+ enabled: false,
+ type: "grid",
+ scale: 1,
+ spacing: 20,
+ color: "#000000",
+ rotation: 0,
+ blur: 0,
+ opacity: 0.5,
+ },
+
+ frame: {
+ enabled: false,
+ type: "none",
+ width: 8,
+ color: "#000000",
+ padding: 20,
+ title: "",
+ },
+
+ canvas: {
+ aspectRatio: "free",
+ padding: 40,
+ },
+
+ noise: {
+ enabled: false,
+ type: "none",
+ opacity: 0.5,
+ },
+
+ setScreenshot: (screenshot) => {
+ set((state) => ({
+ screenshot: { ...state.screenshot, ...screenshot },
+ }));
+ },
+
+ setBackground: (background) => {
+ set((state) => ({
+ background: { ...state.background, ...background },
+ }));
+ },
+
+ setShadow: (shadow) => {
+ set((state) => ({
+ shadow: { ...state.shadow, ...shadow },
+ }));
+ },
+
+ setPattern: (pattern) => {
+ set((state) => ({
+ pattern: { ...state.pattern, ...pattern },
+ }));
+ },
+
+ setFrame: (frame) => {
+ set((state) => ({
+ frame: { ...state.frame, ...frame },
+ }));
+ },
+
+ setCanvas: (canvas) => {
+ set((state) => ({
+ canvas: { ...state.canvas, ...canvas },
+ }));
+ },
+
+ setNoise: (noise) => {
+ set((state) => ({
+ noise: { ...state.noise, ...noise },
+ }));
+ },
+}));
+
+// Sync hook to keep editor store in sync with image store
+export function useEditorStoreSync() {
+ const imageStore = useImageStore();
+ const editorStore = useEditorStore();
+
+ // Sync when image store changes
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: {
+ // Sync screenshot src
+ if (imageStore.uploadedImageUrl !== editorStore.screenshot.src) {
+ editorStore.setScreenshot({ src: imageStore.uploadedImageUrl });
+ }
+
+ // Sync screenshot scale
+ if (imageStore.imageScale / 100 !== editorStore.screenshot.scale) {
+ editorStore.setScreenshot({ scale: imageStore.imageScale / 100 });
+ }
+
+ // Sync screenshot radius
+ if (imageStore.borderRadius !== editorStore.screenshot.radius) {
+ editorStore.setScreenshot({ radius: imageStore.borderRadius });
+ }
+
+ // Sync background
+ const bgConfig = imageStore.backgroundConfig;
+ if (bgConfig.type === "gradient") {
+ const gradientStr =
+ gradientColors[bgConfig.value as GradientKey] ||
+ gradientColors.vibrant_orange_pink;
+ const { colorA, colorB, direction } = parseGradientColors(gradientStr);
+ if (
+ editorStore.background.mode !== "gradient" ||
+ editorStore.background.colorA !== colorA ||
+ editorStore.background.colorB !== colorB ||
+ editorStore.background.gradientDirection !== direction
+ ) {
+ editorStore.setBackground({
+ mode: "gradient",
+ colorA,
+ colorB,
+ gradientDirection: direction,
+ });
+ }
+ } else if (bgConfig.type === "solid") {
+ const color =
+ (solidColors as Record)[bgConfig.value as string] ||
+ "#ffffff";
+ if (
+ editorStore.background.mode !== "solid" ||
+ editorStore.background.colorA !== color
+ ) {
+ editorStore.setBackground({
+ mode: "solid",
+ colorA: color,
+ colorB: color,
+ });
+ }
+ }
+
+ // Sync frame
+ const frame = imageStore.imageBorder;
+ if (
+ editorStore.frame.enabled !== frame.enabled ||
+ editorStore.frame.type !== frame.type ||
+ editorStore.frame.width !== frame.width ||
+ editorStore.frame.color !== frame.color ||
+ editorStore.frame.padding !== frame.padding ||
+ editorStore.frame.title !== frame.title ||
+ editorStore.frame.opacity !== frame.opacity
+ ) {
+ editorStore.setFrame({
+ enabled: frame.enabled,
+ type: frame.type,
+ width: frame.width,
+ color: frame.color,
+ padding: frame.padding,
+ title: frame.title,
+ opacity: frame.opacity,
+ });
+ }
+
+ // Sync shadow
+ const shadow = imageStore.imageShadow;
+ const offsetX = shadow.offsetX || 0;
+ const offsetY = shadow.offsetY || 0;
+ const elevation = Math.max(Math.abs(offsetX), Math.abs(offsetY)) || 4;
+
+ let side: "bottom" | "right" | "bottom-right" = "bottom";
+ if (Math.abs(offsetX) > Math.abs(offsetY)) {
+ side = "right";
+ } else if (Math.abs(offsetX) > 0 && Math.abs(offsetY) > 0) {
+ side = "bottom-right";
+ }
+
+ if (
+ editorStore.shadow.enabled !== shadow.enabled ||
+ editorStore.shadow.softness !== shadow.blur ||
+ editorStore.shadow.spread !== (shadow.spread || 0) ||
+ editorStore.shadow.color !== shadow.color ||
+ editorStore.shadow.offsetX !== offsetX ||
+ editorStore.shadow.offsetY !== offsetY ||
+ editorStore.shadow.intensity !== (shadow.opacity ?? 0.5)
+ ) {
+ editorStore.setShadow({
+ enabled: shadow.enabled,
+ softness: shadow.blur,
+ spread: shadow.spread || 0,
+ color: shadow.color,
+ elevation,
+ side,
+ intensity: shadow.opacity ?? 0.5,
+ offsetX,
+ offsetY,
+ });
+ }
+
+ // Sync canvas aspect ratio
+ const aspectRatioMap: Record<
+ AspectRatioKey,
+ "square" | "4:3" | "2:1" | "3:2" | "free"
+ > = {
+ "1_1": "square",
+ "4_3": "4:3",
+ "2_1": "2:1",
+ "3_2": "3:2",
+ "16_9": "free",
+ "9_16": "free",
+ "4_5": "free",
+ "3_4": "free",
+ "2_3": "free",
+ "5_4": "free",
+ "16_10": "free",
+ };
+ const canvasAspectRatio =
+ aspectRatioMap[imageStore.selectedAspectRatio] || "free";
+ if (editorStore.canvas.aspectRatio !== canvasAspectRatio) {
+ editorStore.setCanvas({ aspectRatio: canvasAspectRatio });
+ }
+ }, [
+ imageStore.uploadedImageUrl,
+ imageStore.imageScale,
+ imageStore.borderRadius,
+ imageStore.backgroundConfig,
+ imageStore.imageBorder,
+ imageStore.imageShadow,
+ imageStore.selectedAspectRatio,
+ editorStore,
+ ]);
+}
+
+// Re-export existing ImageState interface and store
+export interface ImageState {
+ activeAnnotationTool: AnnotationToolType | null;
+
+ // UI State
+ activeRightPanelTab:
+ | "settings"
+ | "edit"
+ | "background"
+ | "transforms"
+ | "animate"
+ | "depth";
+ activeSlideId: string | null;
+ // Animation clips
+ addAnimationClip: (presetId: string, startTime: number) => void;
+ addAnnotation: (annotation: Omit) => void;
+ addBlurRegion: (region: Omit) => void;
+ addImageOverlay: (overlay: Omit) => void;
+ // Slideshow actions
+ addImages: (files: File[]) => void;
+ addKeyframe: (trackId: string, keyframe: Omit) => void;
+ addMockup: (mockup: Omit) => void;
+ addTextOverlay: (overlay: Omit) => void;
+ // addTrack: (track: Omit) => void;
+ // animationClips: AnimationClip[];
+ annotationDefaults: {
+ strokeColor: string;
+ strokeWidth: number;
+ fillColor: string;
+ };
+
+ // Annotations (custom SVG)
+ annotations: AnnotationShape[];
+ applyAnimationPreset: (presetId: string) => void;
+ backgroundBlur: number;
+ backgroundBorderRadius: number;
+ backgroundConfig: BackgroundConfig;
+ backgroundNoise: number;
+
+ // Blur regions
+ blurRegions: BlurRegion[];
+ borderRadius: number;
+ browserHeaderSize: number;
+ browserUrl: string;
+ canvasDimensions: {
+ canvasW: number;
+ canvasH: number;
+ framedW: number;
+ framedH: number;
+ } | null;
+ clearAnimationClips: () => void;
+ clearAnnotations: () => void;
+ clearBlurRegions: () => void;
+ clearImage: () => void;
+ clearImageOverlays: () => void;
+ clearMockups: () => void;
+ clearTextOverlays: () => void;
+ clearTimeline: () => void;
+ customDimensions: { width: number; height: number } | null;
+ editorMode: "screenshot" | "browser";
+ exportImage: () => Promise;
+ exportSettings: {
+ quality: "1x" | "2x" | "3x";
+ format: "png" | "jpeg" | "webp";
+ fileName: string;
+ };
+ // Generated demo video URL (from test/generator)
+ generatedVideoUrl: string | null;
+ imageBorder: ImageBorder;
+ imageFilters: ImageFilters;
+ imageName: string | null;
+ imageOpacity: number;
+ imageOverlays: ImageOverlay[];
+ imageScale: number;
+ imageShadow: ImageShadow;
+ imageStylePreset: ImageStylePreset;
+ // Preview
+ isPreviewing: boolean;
+ mockups: Mockup[];
+ perspective3D: {
+ perspective: number;
+ rotateX: number;
+ rotateY: number;
+ rotateZ: number;
+ translateX: number;
+ translateY: number;
+ scale: number;
+ };
+ previewIndex: number;
+ previewStartedAt: number | null;
+ removeAnimationClip: (clipId: string) => void;
+ removeAnnotation: (id: string) => void;
+ removeBlurRegion: (id: string) => void;
+ removeImageOverlay: (id: string) => void;
+ removeKeyframe: (trackId: string, keyframeId: string) => void;
+ removeMockup: (id: string) => void;
+ removeSlide: (id: string) => void;
+ removeTextOverlay: (id: string) => void;
+ removeTrack: (trackId: string) => void;
+ reorderImageOverlay: (
+ id: string,
+ direction: "up" | "down" | "top" | "bottom"
+ ) => void;
+ resetCanvasSettings: () => void;
+ resetImageFilters: () => void;
+ resetSlideshow: () => void;
+ rulerInterval: number;
+ selectedAnnotationId: string | null;
+ selectedAspectRatio: AspectRatioKey;
+ selectedGradient: GradientKey;
+ setActiveAnnotationTool: (tool: AnnotationToolType | null) => void;
+ setActiveRightPanelTab: (
+ tab: "settings" | "edit" | "background" | "transforms" | "animate" | "depth"
+ ) => void;
+ setActiveSlide: (id: string) => void;
+ setAnnotationDefaults: (
+ defaults: Partial<{
+ strokeColor: string;
+ strokeWidth: number;
+ fillColor: string;
+ }>
+ ) => void;
+ setAspectRatio: (aspectRatio: AspectRatioKey) => void;
+ setBackgroundBlur: (blur: number) => void;
+ setBackgroundBorderRadius: (radius: number) => void;
+ setBackgroundConfig: (config: BackgroundConfig) => void;
+ setBackgroundNoise: (noise: number) => void;
+ setBackgroundOpacity: (opacity: number) => void;
+ setBackgroundType: (type: BackgroundType) => void;
+ setBackgroundValue: (value: string) => void;
+ setBorderRadius: (radius: number) => void;
+ setBrowserHeaderSize: (size: number) => void;
+ setBrowserUrl: (url: string) => void;
+ setCanvasDimensions: (dims: {
+ canvasW: number;
+ canvasH: number;
+ framedW: number;
+ framedH: number;
+ }) => void;
+ setCustomDimensions: (width: number, height: number) => void;
+ setEditorMode: (mode: "screenshot" | "browser") => void;
+ setExportSettings: (settings: Partial) => void;
+ setGeneratedVideoUrl: (url: string | null) => void;
+ setGradient: (gradient: GradientKey) => void;
+ setImage: (file: File) => void;
+ setImageBorder: (border: ImageBorder | Partial) => void;
+ setImageFilter: (key: keyof ImageFilters, value: number) => void;
+ setImageOpacity: (opacity: number) => void;
+ setImageScale: (scale: number) => void;
+ setImageShadow: (shadow: ImageShadow | Partial) => void;
+ setImageStylePreset: (preset: ImageStylePreset) => void;
+ setPerspective3D: (perspective: Partial) => void;
+ setPlayhead: (time: number) => void;
+ setRulerInterval: (interval: number) => void;
+ setSelectedAnnotationId: (id: string | null) => void;
+ setShadowPreset: (preset: ShadowPreset) => void;
+ setShowTemplates: (show: boolean) => void;
+ setShowTimeline: (show: boolean) => void;
+ // setSlideshow: (updates: Partial) => void;
+ // setTimeline: (updates: Partial) => void;
+ // setTimelineDuration: (duration: number) => void;
+ setUploadedImageUrl: (url: string | null, name: string | null) => void;
+ shadowPreset: ShadowPreset;
+ showGrid: boolean;
+
+ // Canvas visual guides
+ showRulers: boolean;
+ showTemplates: boolean;
+ showTimeline: boolean;
+ // Slideshow
+ slides: Slide[];
+
+ slideshow: {
+ enabled: boolean;
+ defaultDuration: number;
+ animation: "none" | "fade" | "slide";
+ };
+ startPlayback: () => void;
+ startPreview: () => void;
+ stopPlayback: () => void;
+ stopPreview: () => void;
+ textOverlays: TextOverlay[];
+
+ // Timeline / Animation
+ // timeline: TimelineState;
+ toggleGrid: () => void;
+ togglePlayback: () => void;
+ toggleRulers: () => void;
+ // toggleTimeline: () => void;
+ // updateAnimationClip: (
+ // clipId: string,
+ // updates: Partial
+ // ) => void;
+ updateAnnotation: (id: string, updates: Partial) => void;
+ updateBlurRegion: (id: string, updates: Partial) => void;
+ updateImageOverlay: (id: string, updates: Partial) => void;
+ updateKeyframe: (
+ trackId: string,
+ keyframeId: string,
+ updates: Partial
+ ) => void;
+ updateMockup: (id: string, updates: Partial) => void;
+ updateTextOverlay: (id: string, updates: Partial) => void;
+ // updateTrack: (trackId: string, updates: Partial) => void;
+ uploadedImageUrl: string | null;
+}
+
+export const useImageStore = create()(
+ temporal((set, get) => ({
+ slides: [],
+ activeSlideId: null,
+
+ slideshow: {
+ enabled: true,
+ defaultDuration: 2,
+ animation: "fade", // 'none' | 'fade' | 'slide'
+ },
+ // setSlideshow: (updates) => {
+ // set((state) => ({
+ // slideshow: { ...state.slideshow, ...updates },
+ // }));
+ // },
+ isPreviewing: false,
+ previewIndex: 0,
+ previewStartedAt: null,
+
+ uploadedImageUrl: null,
+ // Store generated demo video URL (set by demo generator)
+ generatedVideoUrl: null,
+ imageName: null,
+ selectedGradient: "vibrant_orange_pink",
+ borderRadius: 10,
+ backgroundBorderRadius: 10,
+ selectedAspectRatio: "16_9",
+ customDimensions: null,
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/red_distortion_4.webp",
+ opacity: 1,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ textOverlays: [],
+ imageOverlays: [],
+ mockups: [],
+ imageOpacity: 1,
+ imageScale: 100,
+ imageBorder: {
+ enabled: false,
+ width: 8,
+ color: "#000000",
+ type: "none",
+ padding: 20,
+ title: "",
+ },
+ imageShadow: {
+ enabled: true,
+ blur: 15,
+ offsetX: 5,
+ offsetY: 8,
+ spread: 3,
+ color: "rgba(0, 0, 0, 0.6)",
+ opacity: 0.5,
+ },
+ imageStylePreset: "default" as ImageStylePreset,
+ shadowPreset: "soft" as ShadowPreset,
+ perspective3D: {
+ perspective: 200, // em units, converted to px
+ rotateX: 0,
+ rotateY: 0,
+ rotateZ: 0,
+ translateX: 0,
+ translateY: 0,
+ scale: 1,
+ },
+ imageFilters: {
+ brightness: 100,
+ contrast: 100,
+ grayscale: 0,
+ blur: 0,
+ hueRotate: 0,
+ invert: 0,
+ saturate: 100,
+ sepia: 0,
+ },
+ exportSettings: {
+ quality: "2x",
+ format: "png",
+ fileName: "",
+ },
+
+ setUploadedImageUrl: (url: string | null, name: string | null = null) => {
+ set({
+ uploadedImageUrl: url,
+ imageName: name,
+ });
+ // Immediately sync to editor store so canvas updates without
+ // waiting for the EditorStoreSync useEffect cycle
+ useEditorStore.getState().setScreenshot({ src: url });
+ },
+
+ setGeneratedVideoUrl: (url: string | null) => {
+ set({ generatedVideoUrl: url });
+ },
+
+ setImage: (file: File) => {
+ const { uploadedImageUrl: oldUrl } = get();
+ // Revoke old image URL to prevent memory leaks
+ if (oldUrl) {
+ URL.revokeObjectURL(oldUrl);
+ }
+
+ // Track image upload
+ // trackImageUpload("file", file.size);
+
+ const imageUrl = URL.createObjectURL(file);
+ // Reset ALL effects to defaults when uploading a new image
+ set({
+ uploadedImageUrl: imageUrl,
+ imageName: file.name,
+ // Reset image settings
+ imageScale: 100,
+ imageOpacity: 1,
+ borderRadius: 10,
+ backgroundBorderRadius: 10,
+ // Reset background
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/red_distortion_4.webp",
+ opacity: 1,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ selectedGradient: "vibrant_orange_pink",
+ // Reset shadow
+ imageShadow: {
+ enabled: true,
+ blur: 15,
+ offsetX: 5,
+ offsetY: 8,
+ spread: 3,
+ color: "rgba(0, 0, 0, 0.6)",
+ opacity: 0.5,
+ },
+ // Reset border/frame
+ imageBorder: {
+ enabled: false,
+ width: 8,
+ color: "#000000",
+ type: "none",
+ padding: 20,
+ title: "",
+ },
+ imageStylePreset: "default" as ImageStylePreset,
+ shadowPreset: "soft" as ShadowPreset,
+ // Reset 3D perspective
+ perspective3D: {
+ perspective: 200,
+ rotateX: 0,
+ rotateY: 0,
+ rotateZ: 0,
+ translateX: 0,
+ translateY: 0,
+ scale: 1,
+ },
+ // Reset filters
+ imageFilters: {
+ brightness: 100,
+ contrast: 100,
+ grayscale: 0,
+ blur: 0,
+ hueRotate: 0,
+ invert: 0,
+ saturate: 100,
+ sepia: 0,
+ },
+ // Clear overlays
+ textOverlays: [],
+ imageOverlays: [],
+ mockups: [],
+ // Reset annotations & blur
+ annotations: [],
+ activeAnnotationTool: null,
+ blurRegions: [],
+ // Reset timeline/animation
+ // timeline: { ...DEFAULT_TIMELINE_STATE },
+ // animationClips: [],
+ showTimeline: false,
+ });
+ },
+
+ clearImage: () => {
+ const { uploadedImageUrl, slides, imageOverlays } = get();
+
+ // Revoke main image URL
+ if (uploadedImageUrl) {
+ URL.revokeObjectURL(uploadedImageUrl);
+ }
+ // Revoke all slide URLs to prevent memory leaks
+ for (const slide of slides) {
+ if (slide.src) {
+ URL.revokeObjectURL(slide.src);
+ }
+ }
+ // Revoke custom overlay URLs
+ for (const overlay of imageOverlays) {
+ if (overlay.isCustom && overlay.src) {
+ URL.revokeObjectURL(overlay.src);
+ }
+ }
+ // Clear everything and reset ALL effects to defaults
+ set({
+ uploadedImageUrl: null,
+ imageName: null,
+ slides: [],
+ activeSlideId: null,
+ isPreviewing: false,
+ previewIndex: 0,
+ previewStartedAt: null,
+ // Reset image settings
+ imageScale: 100,
+ imageOpacity: 1,
+ borderRadius: 10,
+ backgroundBorderRadius: 10,
+ // Reset background
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/red_distortion_4.webp",
+ opacity: 1,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ selectedGradient: "vibrant_orange_pink",
+ // Reset shadow
+ imageShadow: {
+ enabled: true,
+ blur: 15,
+ offsetX: 5,
+ offsetY: 8,
+ spread: 3,
+ color: "rgba(0, 0, 0, 0.6)",
+ opacity: 0.5,
+ },
+ // Reset border/frame
+ imageBorder: {
+ enabled: false,
+ width: 8,
+ color: "#000000",
+ type: "none",
+ padding: 20,
+ title: "",
+ },
+ imageStylePreset: "default" as ImageStylePreset,
+ shadowPreset: "soft" as ShadowPreset,
+ // Reset 3D perspective
+ perspective3D: {
+ perspective: 200,
+ rotateX: 0,
+ rotateY: 0,
+ rotateZ: 0,
+ translateX: 0,
+ translateY: 0,
+ scale: 1,
+ },
+ // Reset filters
+ imageFilters: {
+ brightness: 100,
+ contrast: 100,
+ grayscale: 0,
+ blur: 0,
+ hueRotate: 0,
+ invert: 0,
+ saturate: 100,
+ sepia: 0,
+ },
+ // Clear overlays
+ textOverlays: [],
+ imageOverlays: [],
+ mockups: [],
+ // Reset annotations & blur
+ annotations: [],
+ activeAnnotationTool: null,
+ blurRegions: [],
+ // Reset timeline/animation
+ // timeline: { ...DEFAULT_TIMELINE_STATE },
+ // animationClips: [],
+ showTimeline: false,
+ });
+ },
+
+ setGradient: (gradient: GradientKey) => {
+ set({ selectedGradient: gradient });
+ },
+
+ setBorderRadius: (radius: number) => {
+ set({ borderRadius: radius });
+ },
+ resetSlideshow: () => {
+ set({
+ slides: [],
+ activeSlideId: null,
+ isPreviewing: false,
+ previewIndex: 0,
+ previewStartedAt: null,
+ });
+ },
+ setBackgroundBorderRadius: (radius: number) => {
+ set({ backgroundBorderRadius: radius });
+ },
+
+ setAspectRatio: (aspectRatio: AspectRatioKey) => {
+ // trackAspectRatioChange(aspectRatio);
+ set({ selectedAspectRatio: aspectRatio });
+ },
+
+ setCustomDimensions: (width: number, height: number) => {
+ // trackAspectRatioChange("custom");
+ set({
+ selectedAspectRatio: "custom",
+ customDimensions: { width, height },
+ });
+ },
+
+ setBackgroundConfig: (config: BackgroundConfig) => {
+ // trackBackgroundChange(config.type, config.value as string);
+ set({ backgroundConfig: config });
+ },
+
+ setBackgroundType: (type: BackgroundType) => {
+ const { backgroundConfig } = get();
+
+ // If switching to 'image' type and current value is not a valid image, set default to radiant9
+ if (type === "image") {
+ const currentValue = backgroundConfig.value;
+ const isGradientKey = currentValue in gradientColors;
+ const isSolidColorKey = currentValue in solidColors;
+ const isValidImage =
+ typeof currentValue === "string" &&
+ (currentValue.startsWith("blob:") ||
+ currentValue.startsWith("http") ||
+ currentValue.startsWith("data:") ||
+ // Check if it's a Cloudinary public ID (contains '/' but not a gradient/solid key)
+ (currentValue.includes("/") && !isGradientKey && !isSolidColorKey));
+
+ // If current value is a gradient or solid color key, or not a valid image, set default to asset-26
+ const newValue =
+ isGradientKey || isSolidColorKey || !isValidImage
+ ? "backgrounds/raycast/red_distortion_4.webp"
+ : currentValue;
+
+ set({
+ backgroundConfig: {
+ ...backgroundConfig,
+ type,
+ value: newValue,
+ },
+ });
+ } else {
+ set({
+ backgroundConfig: {
+ ...backgroundConfig,
+ type,
+ },
+ });
+ }
+ },
+
+ setBackgroundValue: (value: string) => {
+ const { backgroundConfig } = get();
+ set({
+ backgroundConfig: {
+ ...backgroundConfig,
+ value,
+ },
+ });
+ },
+
+ setBackgroundOpacity: (opacity: number) => {
+ const { backgroundConfig } = get();
+ set({
+ backgroundConfig: {
+ ...backgroundConfig,
+ opacity,
+ },
+ });
+ },
+
+ setBackgroundBlur: (blur: number) => {
+ set({ backgroundBlur: blur });
+ },
+
+ setBackgroundNoise: (noise: number) => {
+ set({ backgroundNoise: noise });
+ },
+
+ addTextOverlay: (overlay) => {
+ // trackOverlayAdd("text");
+ const id = `text-${Date.now()}-${Math.random()
+ .toString(36)
+ // biome-ignore lint/style/noSubstr: ({
+ textOverlays: [...state.textOverlays, { ...overlay, id }],
+ }));
+ },
+
+ updateTextOverlay: (id, updates) => {
+ set((state) => ({
+ textOverlays: state.textOverlays.map((overlay) =>
+ overlay.id === id ? { ...overlay, ...updates } : overlay
+ ),
+ }));
+ },
+
+ removeTextOverlay: (id) => {
+ set((state) => ({
+ textOverlays: state.textOverlays.filter((overlay) => overlay.id !== id),
+ }));
+ },
+
+ clearTextOverlays: () => {
+ set({ textOverlays: [] });
+ },
+
+ addImageOverlay: (overlay) => {
+ // trackOverlayAdd("sticker");
+ const id = `overlay-${Date.now()}-${Math.random()
+ .toString(36)
+ // biome-ignore lint/style/noSubstr: ({
+ imageOverlays: [...state.imageOverlays, { blur: 0, ...overlay, id }],
+ }));
+ },
+
+ updateImageOverlay: (id, updates) => {
+ set((state) => ({
+ imageOverlays: state.imageOverlays.map((overlay) =>
+ overlay.id === id ? { ...overlay, ...updates } : overlay
+ ),
+ }));
+ },
+
+ removeImageOverlay: (id) => {
+ set((state) => ({
+ imageOverlays: state.imageOverlays.filter(
+ (overlay) => overlay.id !== id
+ ),
+ }));
+ },
+
+ clearImageOverlays: () => {
+ set({ imageOverlays: [] });
+ },
+
+ reorderImageOverlay: (id, direction) => {
+ set((state) => {
+ const overlays = [...state.imageOverlays];
+ const index = overlays.findIndex((o) => o.id === id);
+ if (index === -1) {
+ return state;
+ }
+
+ let newIndex: number;
+ switch (direction) {
+ case "up":
+ newIndex = Math.min(overlays.length - 1, index + 1);
+ break;
+ case "down":
+ newIndex = Math.max(0, index - 1);
+ break;
+ case "top":
+ newIndex = overlays.length - 1;
+ break;
+ case "bottom":
+ newIndex = 0;
+ break;
+ default:
+ newIndex = index;
+ }
+
+ if (newIndex === index) {
+ return state;
+ }
+ const [item] = overlays.splice(index, 1);
+ overlays.splice(newIndex, 0, item);
+ return { imageOverlays: overlays };
+ });
+ },
+
+ addMockup: (mockup) => {
+ const id = `mockup-${Date.now()}-${Math.random()
+ .toString(36)
+ // biome-ignore lint/style/noSubstr: ({
+ mockups: [...state.mockups, { ...mockup, id }],
+ }));
+ },
+
+ updateMockup: (id, updates) => {
+ set((state) => ({
+ mockups: state.mockups.map((mockup) =>
+ mockup.id === id ? { ...mockup, ...updates } : mockup
+ ),
+ }));
+ },
+
+ removeMockup: (id) => {
+ set((state) => ({
+ mockups: state.mockups.filter((mockup) => mockup.id !== id),
+ }));
+ },
+
+ clearMockups: () => {
+ set({ mockups: [] });
+ },
+
+ setImageOpacity: (opacity: number) => {
+ set({ imageOpacity: opacity });
+ },
+
+ setImageScale: (scale: number) => {
+ set({ imageScale: scale });
+ },
+
+ setImageBorder: (border: ImageBorder | Partial) => {
+ const currentBorder = get().imageBorder;
+ // Track frame changes
+ if (
+ "type" in border &&
+ border.type &&
+ border.type !== currentBorder.type
+ ) {
+ // trackFrameApply(border.type);
+ }
+ set({
+ imageBorder: {
+ ...currentBorder,
+ ...border,
+ },
+ });
+ },
+
+ setImageShadow: (shadow: ImageShadow | Partial) => {
+ const currentShadow = get().imageShadow;
+ set({
+ imageShadow: {
+ ...currentShadow,
+ ...shadow,
+ },
+ });
+ },
+
+ setImageStylePreset: (preset: ImageStylePreset) => {
+ const borderMap: Record> = {
+ default: { enabled: false, type: "none" },
+ "glass-light": {
+ enabled: true,
+ type: "glass-light",
+ opacity: 0.25,
+ padding: 1,
+ },
+ "glass-dark": {
+ enabled: true,
+ type: "glass-dark",
+ opacity: 0.7,
+ padding: 1,
+ },
+ outline: {
+ enabled: true,
+ type: "outline-light",
+ opacity: 0.35,
+ padding: 0.5,
+ },
+ "border-light": { enabled: true, type: "border-light", padding: 1 },
+ "border-dark": { enabled: true, type: "border-dark", padding: 1 },
+ };
+ const currentBorder = get().imageBorder;
+ set({
+ imageStylePreset: preset,
+ imageBorder: { ...currentBorder, ...borderMap[preset] },
+ });
+ },
+
+ setShadowPreset: (preset: ShadowPreset) => {
+ const shadowMap: Record = {
+ none: {
+ enabled: false,
+ blur: 0,
+ offsetX: 0,
+ offsetY: 0,
+ spread: 0,
+ color: "rgba(0,0,0,0.6)",
+ opacity: 0,
+ },
+ hug: {
+ enabled: true,
+ blur: 10,
+ offsetX: 0,
+ offsetY: 2,
+ spread: 0,
+ color: "rgba(0,0,0,0.6)",
+ opacity: 0.25,
+ },
+ soft: {
+ enabled: true,
+ blur: 30,
+ offsetX: 0,
+ offsetY: 12,
+ spread: 5,
+ color: "rgba(0,0,0,0.6)",
+ opacity: 0.5,
+ },
+ strong: {
+ enabled: true,
+ blur: 60,
+ offsetX: 0,
+ offsetY: 24,
+ spread: 10,
+ color: "rgba(0,0,0,0.6)",
+ opacity: 0.8,
+ },
+ };
+ set({
+ shadowPreset: preset,
+ imageShadow: shadowMap[preset],
+ });
+ },
+
+ setPerspective3D: (perspective: Partial) => {
+ const currentPerspective = get().perspective3D;
+ set({
+ perspective3D: {
+ ...currentPerspective,
+ ...perspective,
+ },
+ });
+ },
+
+ setImageFilter: (key: keyof ImageFilters, value: number) => {
+ const currentFilters = get().imageFilters;
+ set({
+ imageFilters: {
+ ...currentFilters,
+ [key]: value,
+ },
+ });
+ },
+
+ resetImageFilters: () => {
+ set({
+ imageFilters: {
+ brightness: 100,
+ contrast: 100,
+ grayscale: 0,
+ blur: 0,
+ hueRotate: 0,
+ invert: 0,
+ saturate: 100,
+ sepia: 0,
+ },
+ });
+ },
+
+ resetCanvasSettings: () => {
+ set({
+ imageScale: 100,
+ imageOpacity: 1,
+ borderRadius: 10,
+ backgroundBorderRadius: 10,
+ backgroundConfig: {
+ type: "image",
+ value: "backgrounds/raycast/red_distortion_4.webp",
+ opacity: 1,
+ },
+ backgroundBlur: 0,
+ backgroundNoise: 0,
+ selectedGradient: "vibrant_orange_pink",
+ imageShadow: {
+ enabled: true,
+ blur: 15,
+ offsetX: 5,
+ offsetY: 8,
+ spread: 3,
+ color: "rgba(0, 0, 0, 0.6)",
+ opacity: 0.5,
+ },
+ imageBorder: {
+ enabled: false,
+ width: 8,
+ color: "#000000",
+ type: "none",
+ padding: 20,
+ title: "",
+ },
+ imageStylePreset: "default" as ImageStylePreset,
+ shadowPreset: "soft" as ShadowPreset,
+ perspective3D: {
+ perspective: 200,
+ rotateX: 0,
+ rotateY: 0,
+ rotateZ: 0,
+ translateX: 0,
+ translateY: 0,
+ scale: 1,
+ },
+ imageFilters: {
+ brightness: 100,
+ contrast: 100,
+ grayscale: 0,
+ blur: 0,
+ hueRotate: 0,
+ invert: 0,
+ saturate: 100,
+ sepia: 0,
+ },
+ textOverlays: [],
+ imageOverlays: [],
+ mockups: [],
+ annotations: [],
+ activeAnnotationTool: null,
+ blurRegions: [],
+ });
+ },
+
+ setExportSettings: (settings: Partial) => {
+ const currentSettings = get().exportSettings;
+ set({
+ exportSettings: {
+ ...currentSettings,
+ ...settings,
+ },
+ });
+ },
+
+ exportImage: async () => {
+ try {
+ await exportImageWithGradient("image-render-card");
+ } catch (error) {
+ console.error("Export failed:", error);
+ throw error;
+ }
+ },
+ addImages: (files: File[]) => {
+ // const { slides, slideshow, _timeline } = get();
+ const { slides, slideshow } = get();
+
+ const newSlides = files.map((file) => ({
+ id: `slide-${crypto.randomUUID()}`,
+ src: URL.createObjectURL(file),
+ name: file.name,
+ duration: slideshow.defaultDuration,
+ }));
+
+ const allSlides = [...slides, ...newSlides];
+
+ // Calculate total slideshow duration based on slides and their durations
+ // const totalSlideDuration = allSlides.reduce(
+ // (sum, slide) => sum + slide.duration * 1000,
+ // 0
+ // );
+ // const newTimelineDuration = Math.max(
+ // timeline.duration,
+ // totalSlideDuration
+ // );
+
+ set({
+ slides: allSlides,
+ activeSlideId: get().activeSlideId ?? newSlides[0]?.id ?? null,
+ uploadedImageUrl: allSlides[0]?.src ?? null,
+ imageName: allSlides[0]?.name ?? null,
+ // Auto-show timeline when multiple slides are added
+ showTimeline: allSlides.length > 1 ? true : get().showTimeline,
+ // Extend timeline to fit all slides
+ // timeline: {
+ // ...timeline,
+ // duration: newTimelineDuration,
+ // },
+ });
+ },
+
+ setActiveSlide: (id: string) => {
+ const slide = get().slides.find((s) => s.id === id);
+ if (!slide) {
+ return;
+ }
+
+ set({
+ activeSlideId: id,
+ uploadedImageUrl: slide.src,
+ imageName: slide.name,
+ });
+
+ // Also sync to editorStore for export compatibility
+ // (React useEffect sync doesn't run during imperative export)
+ useEditorStore.getState().setScreenshot({ src: slide.src });
+ },
+
+ removeSlide: (id) => {
+ const { slides, activeSlideId } = get();
+ const slide = slides.find((s) => s.id === id);
+ if (slide) {
+ URL.revokeObjectURL(slide.src);
+ }
+
+ const remaining = slides.filter((s) => s.id !== id);
+ const nextActive =
+ activeSlideId === id ? (remaining[0]?.id ?? null) : activeSlideId;
+ const nextSlide = remaining.find((s) => s.id === nextActive);
+
+ set({
+ slides: remaining,
+ activeSlideId: nextActive,
+ uploadedImageUrl: nextSlide?.src ?? null,
+ imageName: nextSlide?.name ?? null,
+ });
+ },
+
+ startPreview: () => {
+ if (!get().slides.length) {
+ return;
+ }
+ set({
+ isPreviewing: true,
+ previewIndex: 0,
+ previewStartedAt: Date.now(),
+ });
+ },
+
+ stopPreview: () => {
+ set({
+ isPreviewing: false,
+ previewIndex: 0,
+ previewStartedAt: null,
+ });
+ },
+
+ // Timeline / Animation state
+ // timeline: { ...DEFAULT_TIMELINE_STATE },
+ showTimeline: false,
+ animationClips: [],
+
+ // setTimeline: (updates) => {
+ // set((state) => ({
+ // timeline: { ...state.timeline, ...updates },
+ // }));
+ // },
+
+ setShowTimeline: (show) => {
+ set({ showTimeline: show });
+ },
+
+ toggleTimeline: () => {
+ set((state) => ({ showTimeline: !state.showTimeline }));
+ },
+
+ setPlayhead: (_time) => {
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // playhead: Math.max(0, Math.min(time, state.timeline.duration)),
+ // },
+ // }));
+ },
+
+ togglePlayback: () => {
+ // set((state) => ({
+ // timeline: { ...state.timeline, isPlaying: !state.timeline.isPlaying },
+ // }));
+ },
+
+ startPlayback: () => {
+ // set((state) => ({
+ // timeline: { ...state.timeline, isPlaying: true },
+ // }));
+ },
+
+ stopPlayback: () => {
+ // set((state) => ({
+ // timeline: { ...state.timeline, isPlaying: false },
+ // }));
+ },
+
+ addKeyframe: (_trackId, _keyframe) => {
+ // const id = `kf-${Date.now()}-${/
+ // Math.random().toString(36).substr(2, 9)}`;
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // tracks: state.timeline.tracks.map((track) =>
+ // track.id === trackId
+ // ? {
+ // ...track,
+ // keyframes: [...track.keyframes, { ...keyframe, id }],
+ // }
+ // : track
+ // ),
+ // },
+ // }));
+ },
+
+ updateKeyframe: (_trackId, _keyframeId, _updates) => {
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // tracks: state.timeline.tracks.map((track) =>
+ // track.id === trackId
+ // ? {
+ // ...track,
+ // keyframes: track.keyframes.map((kf) =>
+ // kf.id === keyframeId ? { ...kf, ...updates } : kf
+ // ),
+ // }
+ // : track
+ // ),
+ // },
+ // }));
+ },
+
+ removeKeyframe: (_trackId, _keyframeId) => {
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // tracks: state.timeline.tracks.map((track) =>
+ // track.id === trackId
+ // ? {
+ // ...track,
+ // keyframes: track.keyframes.filter(
+ // (kf) => kf.id !== keyframeId
+ // ),
+ // }
+ // : track
+ // ),
+ // },
+ // }));
+ },
+
+ addTrack: () => {
+ // _track;
+ // const id = `track-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // tracks: [...state.timeline.tracks, { ...track, id }],
+ // },
+ // }));
+ },
+
+ updateTrack: () => {
+ // _trackId, _updates;
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // tracks: state.timeline.tracks.map((track) =>
+ // track.id === trackId ? { ...track, ...updates } : track
+ // ),
+ // },
+ // }));
+ },
+
+ removeTrack: (_trackId) => {
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // tracks: state.timeline.tracks.filter((track) => track.id !== trackId),
+ // },
+ // }));
+ },
+
+ applyAnimationPreset: (_presetId) => {
+ // const preset = getPresetById(presetId);
+ // if (!preset) {
+ // return;
+ // }
+ // const tracks = clonePresetTracks(preset);
+ // set((state) => ({
+ // timeline: {
+ // ...state.timeline,
+ // duration: preset.duration,
+ // tracks,
+ // playhead: 0,
+ // isPlaying: false,
+ // },
+ // }));
+ },
+
+ clearTimeline: () => {
+ // set({
+ // timeline: { ...DEFAULT_TIMELINE_STATE },
+ // });
+ },
+
+ setTimelineDuration: () => {
+ // _duration;
+ // set((state) => {
+ // const newDuration = Math.max(500, duration);
+ // // Clamp animation clips to fit within the new duration
+ // const clampedClips = state.animationClips.map((clip) => {
+ // // Ensure clip doesn't extend beyond new duration
+ // const maxStartTime = Math.max(0, newDuration - 200); // Minimum clip duration of 200ms
+ // const clampedStart = Math.min(clip.startTime, maxStartTime);
+ // const maxDuration = newDuration - clampedStart;
+ // const clampedDuration = Math.min(clip.duration, maxDuration);
+ // return {
+ // ...clip,
+ // startTime: clampedStart,
+ // duration: Math.max(200, clampedDuration),
+ // };
+ // });
+ // return {
+ // animationClips: clampedClips,
+ // timeline: {
+ // ...state.timeline,
+ // duration: newDuration,
+ // playhead: Math.min(state.timeline.playhead, newDuration),
+ // },
+ // };
+ // });
+ },
+
+ // Animation clips
+ addAnimationClip: (_presetId, _startTime) => {
+ // const preset = ANIMATION_PRESETS.find((p) => p.id === presetId);
+ // if (!preset) {
+ // return;
+ // }
+ // trackAnimationClipAdd();
+ // in above function presetId, preset.name, preset.duration;
+ // const id = `clip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ // Brand-matching green color palette
+ // const colors = ["#c9ff2e", "#10B981", "#22c55e", "#84cc16", "#34d399"];
+ // const color = colors[Math.floor(Math.random() * colors.length)];
+ // const newClip: AnimationClip = {
+ // id,
+ // presetId,
+ // name: preset.name,
+ // startTime,
+ // duration: preset.duration,
+ // color,
+ // };
+ // Clone preset tracks with startTime offset and link to clip
+ // const tracks = clonePresetTracks(preset, { startTime, clipId: id });
+ // set((state) => ({
+ // animationClips: [...state.animationClips, newClip],
+ // timeline: {
+ // ...state.timeline,
+ // tracks: [...state.timeline.tracks, ...tracks],
+ // },
+ // showTimeline: true,
+ // }));
+ },
+
+ updateAnimationClip: () => {
+ // _clipId, _updates;
+ // set((state) => {
+ // const existingClip = state.animationClips.find((c) => c.id === clipId);
+ // if (!existingClip) {
+ // return state;
+ // }
+ // const newClip = { ...existingClip, ...updates };
+ // // If startTime or duration changed, update the corresponding track keyframes
+ // const startTimeChanged =
+ // updates.startTime !== undefined &&
+ // updates.startTime !== existingClip.startTime;
+ // const durationChanged =
+ // updates.duration !== undefined &&
+ // updates.duration !== existingClip.duration;
+ // let updatedTracks = state.timeline.tracks;
+ // if (startTimeChanged || durationChanged) {
+ // updatedTracks = state.timeline.tracks.map((track) => {
+ // if (track.clipId !== clipId) {
+ // return track;
+ // }
+ // const originalDuration =
+ // track.originalDuration || existingClip.duration;
+ // const newStartTime = updates.startTime ?? existingClip.startTime;
+ // const newDuration = updates.duration ?? existingClip.duration;
+ // const oldStartTime = existingClip.startTime;
+ // // Calculate time scaling factor if duration changed
+ // const scaleFactor = durationChanged
+ // ? newDuration / existingClip.duration
+ // : 1;
+ // return {
+ // ...track,
+ // keyframes: track.keyframes.map((kf) => {
+ // // First, get the relative time within the clip (remove old startTime offset)
+ // const relativeTime = kf.time - oldStartTime;
+ // // Scale the relative time if duration changed
+ // const scaledRelativeTime = relativeTime * scaleFactor;
+ // // Add the new start time offset
+ // const newTime = scaledRelativeTime + newStartTime;
+ // return {
+ // ...kf,
+ // time: Math.max(0, newTime),
+ // };
+ // }),
+ // };
+ // });
+ // }
+ // return {
+ // animationClips: state.animationClips.map((clip) =>
+ // clip.id === clipId ? newClip : clip
+ // ),
+ // timeline: {
+ // ...state.timeline,
+ // tracks: updatedTracks,
+ // },
+ // };
+ // });
+ },
+
+ removeAnimationClip: (_clipId) => {
+ // set((state) => ({
+ // animationClips: state.animationClips.filter(
+ // (clip) => clip.id !== clipId
+ // ),
+ // timeline: {
+ // ...state.timeline,
+ // // Remove tracks associated with this clip
+ // tracks: state.timeline.tracks.filter(
+ // (track) => track.clipId !== clipId
+ // ),
+ // },
+ // }));
+ },
+
+ clearAnimationClips: () => {
+ // set({
+ // animationClips: [],
+ // timeline: { ...DEFAULT_TIMELINE_STATE },
+ // });
+ },
+
+ // Annotations (custom SVG)
+ annotations: [],
+ activeAnnotationTool: null,
+ selectedAnnotationId: null,
+ annotationDefaults: {
+ strokeColor: "#ef4444",
+ strokeWidth: 6,
+ fillColor: "transparent",
+ },
+ addAnnotation: (annotation) => {
+ const id = `ann-${Date.now()}-${
+ // biome-ignore lint/style/noSubstr: ({
+ annotations: [...state.annotations, { ...annotation, id }],
+ selectedAnnotationId: id,
+ }));
+ },
+ updateAnnotation: (id, updates) => {
+ set((state) => ({
+ annotations: state.annotations.map((a) =>
+ a.id === id ? { ...a, ...updates } : a
+ ),
+ }));
+ },
+ removeAnnotation: (id) => {
+ set((state) => ({
+ annotations: state.annotations.filter((a) => a.id !== id),
+ selectedAnnotationId:
+ state.selectedAnnotationId === id ? null : state.selectedAnnotationId,
+ }));
+ },
+ clearAnnotations: () =>
+ set({ annotations: [], selectedAnnotationId: null }),
+ setActiveAnnotationTool: (tool) => set({ activeAnnotationTool: tool }),
+ setSelectedAnnotationId: (id) => set({ selectedAnnotationId: id }),
+ setAnnotationDefaults: (defaults) => {
+ set((state) => ({
+ annotationDefaults: { ...state.annotationDefaults, ...defaults },
+ }));
+ },
+
+ // Blur regions
+ blurRegions: [],
+ addBlurRegion: (region) => {
+ const id = `blur-${Date.now()}-${
+ // biome-ignore lint/style/noSubstr: ({
+ blurRegions: [...state.blurRegions, { ...region, id }],
+ }));
+ },
+ updateBlurRegion: (id, updates) => {
+ set((state) => ({
+ blurRegions: state.blurRegions.map((r) =>
+ r.id === id ? { ...r, ...updates } : r
+ ),
+ }));
+ },
+ removeBlurRegion: (id) => {
+ set((state) => ({
+ blurRegions: state.blurRegions.filter((r) => r.id !== id),
+ }));
+ },
+ clearBlurRegions: () => set({ blurRegions: [] }),
+
+ // UI State
+ activeRightPanelTab: "edit",
+ setActiveRightPanelTab: (tab) => {
+ set({ activeRightPanelTab: tab });
+ },
+ showTemplates: false,
+ setShowTemplates: (show) => {
+ set({ showTemplates: show });
+ },
+ editorMode: "screenshot",
+ setEditorMode: (mode) => {
+ const currentBorder = get().imageBorder;
+ if (mode === "browser") {
+ // Apply default browser frame (Chrome Dark) if no browser frame is active
+ const isBrowserFrame = [
+ "macos-light",
+ "macos-dark",
+ "windows-light",
+ "windows-dark",
+ ].includes(currentBorder.type);
+ if (!isBrowserFrame) {
+ set({
+ editorMode: mode,
+ imageBorder: {
+ ...currentBorder,
+ enabled: true,
+ type: "windows-dark",
+ title: get().browserUrl || "",
+ },
+ });
+ return;
+ }
+ } else {
+ // Switching back to screenshot: disable browser frame
+ const isBrowserFrame = [
+ "macos-light",
+ "macos-dark",
+ "windows-light",
+ "windows-dark",
+ ].includes(currentBorder.type);
+ if (isBrowserFrame) {
+ set({
+ editorMode: mode,
+ imageBorder: {
+ ...currentBorder,
+ enabled: false,
+ type: "none",
+ },
+ });
+ return;
+ }
+ }
+ set({ editorMode: mode });
+ },
+ browserUrl: "",
+ setBrowserUrl: (url) => {
+ const currentBorder = get().imageBorder;
+ set({
+ browserUrl: url,
+ imageBorder: { ...currentBorder, title: url },
+ });
+ },
+ browserHeaderSize: 100,
+ setBrowserHeaderSize: (size) => {
+ set({ browserHeaderSize: size });
+ },
+ canvasDimensions: null,
+ setCanvasDimensions: (dims) => {
+ set({ canvasDimensions: dims });
+ },
+
+ showRulers: false,
+ showGrid: false,
+ rulerInterval: 100,
+ toggleRulers: () => set((state) => ({ showRulers: !state.showRulers })),
+ toggleGrid: () => set((state) => ({ showGrid: !state.showGrid })),
+ setRulerInterval: (interval) =>
+ set({
+ rulerInterval: Math.max(
+ 1,
+ Math.round(Number.isFinite(interval) ? interval : 100)
+ ),
+ }),
+ }))
+);
diff --git a/apps/dashboard/lib/workers/export-worker-service.ts b/apps/dashboard/lib/workers/export-worker-service.ts
new file mode 100644
index 0000000..3bda940
--- /dev/null
+++ b/apps/dashboard/lib/workers/export-worker-service.ts
@@ -0,0 +1,657 @@
+/**
+ * Export Worker Service
+ *
+ * Provides a clean API for main thread to communicate with the export web worker.
+ * Handles worker lifecycle, message passing, and provides Promise-based APIs.
+ */
+
+import type {
+ BlurPayload,
+ CompositePayload,
+ ConvertFormatPayload,
+ ConvertFormatResult,
+ ExportWorkerMessageType,
+ ExportWorkerRequest,
+ ExportWorkerResponse,
+ NoisePayload,
+ OpacityPayload,
+} from "./export.worker";
+
+// Re-export types for consumers
+export type {
+ BlurPayload,
+ CompositePayload,
+ ConvertFormatPayload,
+ ConvertFormatResult,
+ NoisePayload,
+ OpacityPayload,
+};
+
+type PendingRequest = {
+ resolve: (result: any) => void;
+ reject: (error: Error) => void;
+};
+
+class ExportWorkerService {
+ private worker: Worker | null = null;
+ private pendingRequests: Map = new Map();
+ private isReady = false;
+ private readyPromise: Promise | null = null;
+ private messageId = 0;
+ private initializationAttempted = false;
+
+ /**
+ * Check if Web Workers are supported in current environment
+ */
+ private isWorkerSupported(): boolean {
+ return typeof window !== "undefined" && typeof Worker !== "undefined";
+ }
+
+ /**
+ * Initialize the worker
+ */
+ private initializeWorker(): Promise {
+ if (this.worker || this.initializationAttempted) {
+ return this.readyPromise || Promise.resolve();
+ }
+
+ this.initializationAttempted = true;
+
+ if (!this.isWorkerSupported()) {
+ console.warn(
+ "Web Workers not supported, operations will run on main thread"
+ );
+ this.isReady = false;
+ return Promise.resolve();
+ }
+
+ this.readyPromise = new Promise((resolve, reject) => {
+ try {
+ // Create worker using Webpack's worker-loader syntax
+ this.worker = new Worker(
+ new URL("./export.worker.ts", import.meta.url),
+ { type: "module" }
+ );
+
+ const timeout = setTimeout(() => {
+ console.warn(
+ "Worker initialization timeout, falling back to main thread"
+ );
+ this.isReady = false;
+ resolve();
+ }, 5000);
+
+ this.worker.onmessage = (event: MessageEvent) => {
+ const data = event.data;
+
+ // Handle ready signal
+ if (data.type === "ready") {
+ clearTimeout(timeout);
+ this.isReady = true;
+ resolve();
+ return;
+ }
+
+ // Handle response messages
+ const response = data as ExportWorkerResponse;
+ const pending = this.pendingRequests.get(response.id);
+
+ if (pending) {
+ this.pendingRequests.delete(response.id);
+
+ if (response.success) {
+ // Reconstruct ImageData from transferred buffer if needed
+ if (
+ response.result &&
+ response.result.data &&
+ response.result.width &&
+ response.result.height
+ ) {
+ const { data: dataArray, width, height } = response.result;
+ try {
+ // Create a new Uint8ClampedArray with a fresh ArrayBuffer
+ let clampedArray: Uint8ClampedArray;
+
+ if (dataArray instanceof Uint8ClampedArray) {
+ // Copy to a new array to ensure we have an ArrayBuffer
+ clampedArray = new Uint8ClampedArray(dataArray.length);
+ clampedArray.set(dataArray);
+ } else if (ArrayBuffer.isView(dataArray)) {
+ // Handle other typed array views
+ const view = dataArray as Uint8Array;
+ clampedArray = new Uint8ClampedArray(view.length);
+ clampedArray.set(view);
+ } else if (Array.isArray(dataArray)) {
+ // Handle plain array
+ clampedArray = new Uint8ClampedArray(dataArray);
+ } else {
+ pending.resolve(response.result);
+ return;
+ }
+
+ // Use type assertion to handle TypeScript strict mode
+ const imageData = new ImageData(
+ clampedArray as unknown as Uint8ClampedArray,
+ width,
+ height
+ );
+ pending.resolve(imageData);
+ } catch {
+ pending.resolve(response.result);
+ }
+ } else {
+ pending.resolve(response.result);
+ }
+ } else {
+ pending.reject(
+ new Error(response.error || "Worker operation failed")
+ );
+ }
+ }
+ };
+
+ this.worker.onerror = (error) => {
+ clearTimeout(timeout);
+ console.error("Worker error:", error);
+ this.isReady = false;
+
+ // Reject all pending requests
+ for (const [id, pending] of this.pendingRequests) {
+ pending.reject(new Error("Worker encountered an error"));
+ this.pendingRequests.delete(id);
+ }
+
+ resolve(); // Resolve initialization so fallback can be used
+ };
+ } catch (error) {
+ console.warn(
+ "Failed to create worker, falling back to main thread:",
+ error
+ );
+ this.isReady = false;
+ resolve();
+ }
+ });
+
+ return this.readyPromise;
+ }
+
+ /**
+ * Generate a unique message ID
+ */
+ private generateId(): string {
+ return `msg_${++this.messageId}_${Date.now()}`;
+ }
+
+ /**
+ * Send a message to the worker and wait for response
+ */
+ private async sendMessage(
+ type: ExportWorkerMessageType,
+ payload: any,
+ transferables?: Transferable[]
+ ): Promise {
+ await this.initializeWorker();
+
+ if (!(this.isReady && this.worker)) {
+ throw new Error("Worker not available");
+ }
+
+ const id = this.generateId();
+ const request: ExportWorkerRequest = { id, type, payload };
+
+ return new Promise((resolve, reject) => {
+ this.pendingRequests.set(id, { resolve, reject });
+
+ // Set timeout for the request
+ const timeout = setTimeout(() => {
+ this.pendingRequests.delete(id);
+ reject(new Error(`Worker request timeout: ${type}`));
+ }, 30_000); // 30 second timeout
+
+ // Update resolve/reject to clear timeout
+ this.pendingRequests.set(id, {
+ resolve: (result) => {
+ clearTimeout(timeout);
+ resolve(result);
+ },
+ reject: (error) => {
+ clearTimeout(timeout);
+ reject(error);
+ },
+ });
+
+ if (transferables && transferables.length > 0) {
+ this.worker!.postMessage(request, transferables);
+ } else {
+ this.worker!.postMessage(request);
+ }
+ });
+ }
+
+ /**
+ * Check if the worker is ready
+ */
+ async isWorkerReady(): Promise {
+ await this.initializeWorker();
+ return this.isReady;
+ }
+
+ /**
+ * Generate noise texture using worker
+ * Falls back to main thread if worker unavailable
+ */
+ async generateNoiseTexture(
+ width: number,
+ height: number,
+ intensity: number
+ ): Promise {
+ await this.initializeWorker();
+
+ // If worker not available, run on main thread
+ if (!(this.isReady && this.worker)) {
+ return this.generateNoiseTextureFallback(width, height, intensity);
+ }
+
+ try {
+ return await this.sendMessage("generateNoise", {
+ width,
+ height,
+ intensity,
+ } as NoisePayload);
+ } catch (error) {
+ console.warn("Worker noise generation failed, using fallback:", error);
+ return this.generateNoiseTextureFallback(width, height, intensity);
+ }
+ }
+
+ /**
+ * Fallback noise generation on main thread
+ */
+ private generateNoiseTextureFallback(
+ width: number,
+ height: number,
+ intensity: number
+ ): ImageData {
+ const imageData = new ImageData(width, height);
+ const data = imageData.data;
+ const stdDev = intensity * 50;
+
+ for (let i = 0; i < data.length; i += 4) {
+ let u = 0,
+ v = 0;
+ while (u === 0) {
+ u = Math.random();
+ }
+ while (v === 0) {
+ v = Math.random();
+ }
+ const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
+ const noise = z * stdDev + 128;
+ const value = Math.max(0, Math.min(255, Math.round(noise)));
+
+ data[i] = value;
+ data[i + 1] = value;
+ data[i + 2] = value;
+ data[i + 3] = 255;
+ }
+
+ return imageData;
+ }
+
+ /**
+ * Apply blur to image data using worker
+ * Falls back to main thread if worker unavailable
+ */
+ async applyBlur(
+ imageData: ImageData,
+ blurAmount: number
+ ): Promise {
+ if (blurAmount <= 0) {
+ return imageData;
+ }
+
+ await this.initializeWorker();
+
+ if (!(this.isReady && this.worker)) {
+ return this.applyBlurFallback(imageData, blurAmount);
+ }
+
+ try {
+ // Create a copy of the data to transfer
+ const dataCopy = new Uint8ClampedArray(imageData.data);
+ const payload: BlurPayload = {
+ imageData: {
+ data: dataCopy,
+ width: imageData.width,
+ height: imageData.height,
+ } as unknown as ImageData,
+ blurAmount,
+ width: imageData.width,
+ height: imageData.height,
+ };
+
+ return await this.sendMessage("applyBlur", payload, [
+ dataCopy.buffer,
+ ]);
+ } catch (error) {
+ console.warn("Worker blur failed, using fallback:", error);
+ return this.applyBlurFallback(imageData, blurAmount);
+ }
+ }
+
+ /**
+ * Fallback blur on main thread
+ */
+ private applyBlurFallback(
+ imageData: ImageData,
+ blurAmount: number
+ ): ImageData {
+ const canvas = document.createElement("canvas");
+ canvas.width = imageData.width;
+ canvas.height = imageData.height;
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ return imageData;
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+
+ const blurredCanvas = document.createElement("canvas");
+ blurredCanvas.width = imageData.width;
+ blurredCanvas.height = imageData.height;
+ const blurredCtx = blurredCanvas.getContext("2d");
+
+ if (!blurredCtx) {
+ return imageData;
+ }
+
+ blurredCtx.filter = `blur(${blurAmount}px)`;
+ blurredCtx.drawImage(canvas, 0, 0);
+
+ return blurredCtx.getImageData(0, 0, imageData.width, imageData.height);
+ }
+
+ /**
+ * Apply opacity to image data using worker
+ * Falls back to main thread if worker unavailable
+ */
+ async applyOpacity(
+ imageData: ImageData,
+ opacity: number
+ ): Promise {
+ if (opacity >= 1) {
+ return imageData;
+ }
+
+ await this.initializeWorker();
+
+ if (!(this.isReady && this.worker)) {
+ return this.applyOpacityFallback(imageData, opacity);
+ }
+
+ try {
+ const dataCopy = new Uint8ClampedArray(imageData.data);
+ const payload: OpacityPayload = {
+ imageData: {
+ data: dataCopy,
+ width: imageData.width,
+ height: imageData.height,
+ } as unknown as ImageData,
+ opacity,
+ width: imageData.width,
+ height: imageData.height,
+ };
+
+ return await this.sendMessage("applyOpacity", payload, [
+ dataCopy.buffer,
+ ]);
+ } catch (error) {
+ console.warn("Worker opacity failed, using fallback:", error);
+ return this.applyOpacityFallback(imageData, opacity);
+ }
+ }
+
+ /**
+ * Fallback opacity on main thread
+ */
+ private applyOpacityFallback(
+ imageData: ImageData,
+ opacity: number
+ ): ImageData {
+ const result = new ImageData(imageData.width, imageData.height);
+ const srcData = imageData.data;
+ const destData = result.data;
+
+ for (let i = 0; i < srcData.length; i += 4) {
+ destData[i] = srcData[i];
+ destData[i + 1] = srcData[i + 1];
+ destData[i + 2] = srcData[i + 2];
+ destData[i + 3] = Math.round(srcData[i + 3] * opacity);
+ }
+
+ return result;
+ }
+
+ /**
+ * Composite two image data layers using worker
+ * Falls back to main thread if worker unavailable
+ */
+ async composite(
+ baseImageData: ImageData,
+ overlayImageData: ImageData,
+ blendMode: "normal" | "overlay" | "multiply" | "screen" = "normal",
+ overlayOpacity = 1
+ ): Promise {
+ await this.initializeWorker();
+
+ if (!(this.isReady && this.worker)) {
+ return this.compositeFallback(
+ baseImageData,
+ overlayImageData,
+ blendMode,
+ overlayOpacity
+ );
+ }
+
+ try {
+ const baseDataCopy = new Uint8ClampedArray(baseImageData.data);
+ const overlayDataCopy = new Uint8ClampedArray(overlayImageData.data);
+
+ const payload: CompositePayload = {
+ baseImageData: {
+ data: baseDataCopy,
+ width: baseImageData.width,
+ height: baseImageData.height,
+ } as unknown as ImageData,
+ overlayImageData: {
+ data: overlayDataCopy,
+ width: overlayImageData.width,
+ height: overlayImageData.height,
+ } as unknown as ImageData,
+ blendMode,
+ overlayOpacity,
+ width: baseImageData.width,
+ height: baseImageData.height,
+ };
+
+ return await this.sendMessage("composite", payload, [
+ baseDataCopy.buffer,
+ overlayDataCopy.buffer,
+ ]);
+ } catch (error) {
+ console.warn("Worker composite failed, using fallback:", error);
+ return this.compositeFallback(
+ baseImageData,
+ overlayImageData,
+ blendMode,
+ overlayOpacity
+ );
+ }
+ }
+
+ /**
+ * Fallback composite on main thread
+ */
+ private compositeFallback(
+ baseImageData: ImageData,
+ overlayImageData: ImageData,
+ blendMode: string,
+ overlayOpacity: number
+ ): ImageData {
+ const canvas = document.createElement("canvas");
+ canvas.width = baseImageData.width;
+ canvas.height = baseImageData.height;
+ const ctx = canvas.getContext("2d");
+
+ if (!ctx) {
+ return baseImageData;
+ }
+
+ // Draw base
+ ctx.putImageData(baseImageData, 0, 0);
+
+ // Create overlay canvas
+ const overlayCanvas = document.createElement("canvas");
+ overlayCanvas.width = overlayImageData.width;
+ overlayCanvas.height = overlayImageData.height;
+ const overlayCtx = overlayCanvas.getContext("2d");
+
+ if (!overlayCtx) {
+ return baseImageData;
+ }
+
+ overlayCtx.putImageData(overlayImageData, 0, 0);
+
+ // Create pattern for tiling
+ const pattern = ctx.createPattern(overlayCanvas, "repeat");
+
+ if (pattern) {
+ ctx.save();
+ ctx.globalCompositeOperation = blendMode as GlobalCompositeOperation;
+ ctx.globalAlpha = overlayOpacity;
+ ctx.fillStyle = pattern;
+ ctx.fillRect(0, 0, baseImageData.width, baseImageData.height);
+ ctx.restore();
+ }
+
+ return ctx.getImageData(0, 0, baseImageData.width, baseImageData.height);
+ }
+
+ /**
+ * Convert image format using worker (OffscreenCanvas)
+ * Falls back to main thread canvas if worker unavailable
+ */
+ async convertFormat(
+ canvas: HTMLCanvasElement,
+ format: "png" | "jpeg" | "webp",
+ quality: number
+ ): Promise<{ blob: Blob; mimeType: string; fileSize: number }> {
+ await this.initializeWorker();
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ throw new Error("Failed to get 2D context from canvas");
+ }
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+ // If worker not available, use main thread fallback
+ if (!(this.isReady && this.worker)) {
+ return this.convertFormatFallback(canvas, format, quality);
+ }
+
+ try {
+ const dataCopy = new Uint8ClampedArray(imageData.data);
+ const payload: ConvertFormatPayload = {
+ imageData: {
+ data: dataCopy,
+ width: imageData.width,
+ height: imageData.height,
+ } as unknown as ImageData,
+ format,
+ quality,
+ width: canvas.width,
+ height: canvas.height,
+ };
+
+ const result = await this.sendMessage(
+ "convertFormat",
+ payload,
+ [dataCopy.buffer]
+ );
+
+ // Convert ArrayBuffer back to Blob
+ const blob = new Blob([result.blob], { type: result.mimeType });
+
+ return {
+ blob,
+ mimeType: result.mimeType,
+ fileSize: result.fileSize,
+ };
+ } catch (error) {
+ console.warn("Worker format conversion failed, using fallback:", error);
+ return this.convertFormatFallback(canvas, format, quality);
+ }
+ }
+
+ /**
+ * Fallback format conversion on main thread
+ */
+ private async convertFormatFallback(
+ canvas: HTMLCanvasElement,
+ format: "png" | "jpeg" | "webp",
+ quality: number
+ ): Promise<{ blob: Blob; mimeType: string; fileSize: number }> {
+ const mimeType =
+ format === "png"
+ ? "image/png"
+ : format === "webp"
+ ? "image/webp"
+ : "image/jpeg";
+
+ return new Promise((resolve, reject) => {
+ canvas.toBlob(
+ (blob) => {
+ if (blob) {
+ resolve({
+ blob,
+ mimeType,
+ fileSize: blob.size,
+ });
+ } else {
+ reject(new Error("Failed to convert canvas to blob"));
+ }
+ },
+ mimeType,
+ format === "png" ? undefined : quality
+ );
+ });
+ }
+
+ /**
+ * Terminate the worker
+ */
+ terminate(): void {
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
+ this.isReady = false;
+ this.initializationAttempted = false;
+ this.readyPromise = null;
+
+ // Reject all pending requests
+ for (const [id, pending] of this.pendingRequests) {
+ pending.reject(new Error("Worker terminated"));
+ this.pendingRequests.delete(id);
+ }
+ }
+ }
+}
+
+// Export singleton instance
+export const exportWorkerService = new ExportWorkerService();
+
+// Export class for testing
+export { ExportWorkerService };
diff --git a/apps/dashboard/lib/workers/export.worker.ts b/apps/dashboard/lib/workers/export.worker.ts
new file mode 100644
index 0000000..87835e4
--- /dev/null
+++ b/apps/dashboard/lib/workers/export.worker.ts
@@ -0,0 +1,435 @@
+/**
+ * Export Web Worker
+ *
+ * Handles heavy image processing operations off the main thread:
+ * - Noise texture generation (Gaussian noise)
+ * - Canvas blur operations
+ * - Canvas opacity operations
+ * - Image compositing
+ *
+ * Uses OffscreenCanvas for canvas operations in the worker context.
+ */
+
+/* eslint-disable no-restricted-globals */
+
+// Worker message types
+export type ExportWorkerMessageType =
+ | 'generateNoise'
+ | 'applyBlur'
+ | 'applyOpacity'
+ | 'composite'
+ | 'processImageData'
+ | 'convertFormat';
+
+export interface ExportWorkerRequest {
+ id: string;
+ type: ExportWorkerMessageType;
+ payload: any;
+}
+
+export interface ExportWorkerResponse {
+ id: string;
+ type: ExportWorkerMessageType;
+ success: boolean;
+ result?: any;
+ error?: string;
+}
+
+// Noise generation payload
+export interface NoisePayload {
+ width: number;
+ height: number;
+ intensity: number;
+}
+
+// Blur payload
+export interface BlurPayload {
+ imageData: ImageData;
+ blurAmount: number;
+ width: number;
+ height: number;
+}
+
+// Opacity payload
+export interface OpacityPayload {
+ imageData: ImageData;
+ opacity: number;
+ width: number;
+ height: number;
+}
+
+// Composite payload
+export interface CompositePayload {
+ baseImageData: ImageData;
+ overlayImageData: ImageData;
+ blendMode: 'normal' | 'overlay' | 'multiply' | 'screen';
+ overlayOpacity: number;
+ width: number;
+ height: number;
+}
+
+// Format conversion payload
+export interface ConvertFormatPayload {
+ imageData: ImageData;
+ format: 'png' | 'jpeg' | 'webp';
+ quality: number; // 0-1 for jpeg/webp
+ width: number;
+ height: number;
+}
+
+// Format conversion result
+export interface ConvertFormatResult {
+ blob: ArrayBuffer;
+ mimeType: string;
+ fileSize: number;
+}
+
+// Worker context type
+const ctx: Worker = self as unknown as Worker;
+
+/**
+ * Generate Gaussian (normal) distributed random number using Box-Muller transform
+ */
+function gaussianRandom(mean: number = 0, stdDev: number = 1): number {
+ let u = 0, v = 0;
+ while (u === 0) u = Math.random();
+ while (v === 0) v = Math.random();
+ const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
+ return z * stdDev + mean;
+}
+
+/**
+ * Generate noise texture ImageData
+ */
+function generateNoiseTexture(width: number, height: number, intensity: number): ImageData {
+ const imageData = new ImageData(width, height);
+ const data = imageData.data;
+
+ const stdDev = intensity * 50;
+
+ for (let i = 0; i < data.length; i += 4) {
+ const noise = gaussianRandom(128, stdDev);
+ const value = Math.max(0, Math.min(255, Math.round(noise)));
+
+ data[i] = value; // R
+ data[i + 1] = value; // G
+ data[i + 2] = value; // B
+ data[i + 3] = 255; // A
+ }
+
+ return imageData;
+}
+
+/**
+ * Apply blur using OffscreenCanvas
+ * Note: OffscreenCanvas filter support varies by browser
+ */
+function applyBlur(
+ imageData: ImageData,
+ blurAmount: number,
+ width: number,
+ height: number
+): ImageData {
+ if (blurAmount <= 0) {
+ return imageData;
+ }
+
+ // Check if OffscreenCanvas is available
+ if (typeof OffscreenCanvas === 'undefined') {
+ // Fallback: return original data if OffscreenCanvas not available
+ console.warn('OffscreenCanvas not available, returning original image data');
+ return imageData;
+ }
+
+ const canvas = new OffscreenCanvas(width, height);
+ const canvasCtx = canvas.getContext('2d');
+
+ if (!canvasCtx) {
+ return imageData;
+ }
+
+ // Put the image data
+ canvasCtx.putImageData(imageData, 0, 0);
+
+ // Create a temporary canvas to draw blurred result
+ const blurredCanvas = new OffscreenCanvas(width, height);
+ const blurredCtx = blurredCanvas.getContext('2d');
+
+ if (!blurredCtx) {
+ return imageData;
+ }
+
+ // Apply blur filter
+ blurredCtx.filter = `blur(${blurAmount}px)`;
+ blurredCtx.drawImage(canvas, 0, 0);
+ blurredCtx.filter = 'none';
+
+ // Get the blurred image data
+ return blurredCtx.getImageData(0, 0, width, height);
+}
+
+/**
+ * Apply opacity to image data
+ */
+function applyOpacity(
+ imageData: ImageData,
+ opacity: number,
+ width: number,
+ height: number
+): ImageData {
+ if (opacity >= 1) {
+ return imageData;
+ }
+
+ const result = new ImageData(width, height);
+ const srcData = imageData.data;
+ const destData = result.data;
+
+ if (opacity <= 0) {
+ // Return transparent image
+ return result;
+ }
+
+ // Apply opacity to alpha channel
+ for (let i = 0; i < srcData.length; i += 4) {
+ destData[i] = srcData[i]; // R
+ destData[i + 1] = srcData[i + 1]; // G
+ destData[i + 2] = srcData[i + 2]; // B
+ destData[i + 3] = Math.round(srcData[i + 3] * opacity); // A
+ }
+
+ return result;
+}
+
+/**
+ * Blend two pixels based on blend mode
+ */
+function blendPixel(
+ baseR: number, baseG: number, baseB: number, baseA: number,
+ overlayR: number, overlayG: number, overlayB: number, overlayA: number,
+ blendMode: string,
+ overlayOpacity: number
+): [number, number, number, number] {
+ // Apply overlay opacity
+ overlayA = overlayA * overlayOpacity;
+
+ if (overlayA === 0) {
+ return [baseR, baseG, baseB, baseA];
+ }
+
+ let r: number, g: number, b: number;
+
+ switch (blendMode) {
+ case 'overlay':
+ // Overlay blend mode
+ r = baseR < 128 ? (2 * baseR * overlayR) / 255 : 255 - (2 * (255 - baseR) * (255 - overlayR)) / 255;
+ g = baseG < 128 ? (2 * baseG * overlayG) / 255 : 255 - (2 * (255 - baseG) * (255 - overlayG)) / 255;
+ b = baseB < 128 ? (2 * baseB * overlayB) / 255 : 255 - (2 * (255 - baseB) * (255 - overlayB)) / 255;
+ break;
+ case 'multiply':
+ r = (baseR * overlayR) / 255;
+ g = (baseG * overlayG) / 255;
+ b = (baseB * overlayB) / 255;
+ break;
+ case 'screen':
+ r = 255 - ((255 - baseR) * (255 - overlayR)) / 255;
+ g = 255 - ((255 - baseG) * (255 - overlayG)) / 255;
+ b = 255 - ((255 - baseB) * (255 - overlayB)) / 255;
+ break;
+ default: // 'normal'
+ r = overlayR;
+ g = overlayG;
+ b = overlayB;
+ }
+
+ // Alpha compositing
+ const alpha = overlayA / 255;
+ const outR = Math.round(r * alpha + baseR * (1 - alpha));
+ const outG = Math.round(g * alpha + baseG * (1 - alpha));
+ const outB = Math.round(b * alpha + baseB * (1 - alpha));
+ const outA = Math.round(baseA + overlayA * (1 - baseA / 255));
+
+ return [
+ Math.max(0, Math.min(255, outR)),
+ Math.max(0, Math.min(255, outG)),
+ Math.max(0, Math.min(255, outB)),
+ Math.max(0, Math.min(255, outA))
+ ];
+}
+
+/**
+ * Composite two image data layers
+ */
+function compositeImageData(
+ baseImageData: ImageData,
+ overlayImageData: ImageData,
+ blendMode: 'normal' | 'overlay' | 'multiply' | 'screen',
+ overlayOpacity: number,
+ width: number,
+ height: number
+): ImageData {
+ const result = new ImageData(width, height);
+ const baseData = baseImageData.data;
+ const overlayData = overlayImageData.data;
+ const resultData = result.data;
+
+ // Handle tiled overlay (noise pattern)
+ const overlayWidth = overlayImageData.width;
+ const overlayHeight = overlayImageData.height;
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const i = (y * width + x) * 4;
+
+ // Tile the overlay
+ const overlayX = x % overlayWidth;
+ const overlayY = y % overlayHeight;
+ const overlayI = (overlayY * overlayWidth + overlayX) * 4;
+
+ const [r, g, b, a] = blendPixel(
+ baseData[i], baseData[i + 1], baseData[i + 2], baseData[i + 3],
+ overlayData[overlayI], overlayData[overlayI + 1], overlayData[overlayI + 2], overlayData[overlayI + 3],
+ blendMode,
+ overlayOpacity
+ );
+
+ resultData[i] = r;
+ resultData[i + 1] = g;
+ resultData[i + 2] = b;
+ resultData[i + 3] = a;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Convert ImageData to a specific format using OffscreenCanvas
+ */
+async function convertFormat(
+ imageData: ImageData,
+ format: 'png' | 'jpeg' | 'webp',
+ quality: number,
+ width: number,
+ height: number
+): Promise {
+ // Check if OffscreenCanvas is available
+ if (typeof OffscreenCanvas === 'undefined') {
+ throw new Error('OffscreenCanvas not available in this environment');
+ }
+
+ const canvas = new OffscreenCanvas(width, height);
+ const canvasCtx = canvas.getContext('2d');
+
+ if (!canvasCtx) {
+ throw new Error('Failed to get 2D context from OffscreenCanvas');
+ }
+
+ // Put the image data onto the canvas
+ canvasCtx.putImageData(imageData, 0, 0);
+
+ // Determine MIME type
+ const mimeType = format === 'png' ? 'image/png' :
+ format === 'webp' ? 'image/webp' : 'image/jpeg';
+
+ // Convert to blob with quality setting
+ const blob = await canvas.convertToBlob({
+ type: mimeType,
+ quality: format === 'png' ? undefined : quality,
+ });
+
+ // Convert blob to ArrayBuffer for transfer
+ const arrayBuffer = await blob.arrayBuffer();
+
+ return {
+ blob: arrayBuffer,
+ mimeType,
+ fileSize: arrayBuffer.byteLength,
+ };
+}
+
+/**
+ * Handle incoming messages
+ */
+ctx.onmessage = async (event: MessageEvent) => {
+ const { id, type, payload } = event.data;
+
+ try {
+ let result: ImageData | ConvertFormatResult | undefined;
+
+ switch (type) {
+ case 'generateNoise': {
+ const { width, height, intensity } = payload as NoisePayload;
+ result = generateNoiseTexture(width, height, intensity);
+ break;
+ }
+
+ case 'applyBlur': {
+ const { imageData, blurAmount, width, height } = payload as BlurPayload;
+ result = applyBlur(imageData, blurAmount, width, height);
+ break;
+ }
+
+ case 'applyOpacity': {
+ const { imageData, opacity, width, height } = payload as OpacityPayload;
+ result = applyOpacity(imageData, opacity, width, height);
+ break;
+ }
+
+ case 'composite': {
+ const { baseImageData, overlayImageData, blendMode, overlayOpacity, width, height } = payload as CompositePayload;
+ result = compositeImageData(baseImageData, overlayImageData, blendMode, overlayOpacity, width, height);
+ break;
+ }
+
+ case 'convertFormat': {
+ const { imageData, format, quality, width, height } = payload as ConvertFormatPayload;
+ // Reconstruct ImageData from transferred data
+ let imgData: ImageData;
+ if (imageData instanceof ImageData) {
+ imgData = imageData;
+ } else {
+ const { data, width: w, height: h } = imageData as unknown as { data: Uint8ClampedArray; width: number; height: number };
+ imgData = new ImageData(new Uint8ClampedArray(data), w, h);
+ }
+ result = await convertFormat(imgData, format, quality, width, height);
+ break;
+ }
+
+ default:
+ throw new Error(`Unknown message type: ${type}`);
+ }
+
+ // Send result back to main thread
+ const response: ExportWorkerResponse = {
+ id,
+ type,
+ success: true,
+ result
+ };
+
+ // Transfer buffers for performance
+ if (result instanceof ImageData) {
+ ctx.postMessage(response, { transfer: [result.data.buffer] });
+ } else if (result && 'blob' in result) {
+ // Transfer ArrayBuffer for convertFormat result
+ ctx.postMessage(response, { transfer: [result.blob] });
+ } else {
+ ctx.postMessage(response);
+ }
+ } catch (error) {
+ const response: ExportWorkerResponse = {
+ id,
+ type,
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ };
+ ctx.postMessage(response);
+ }
+};
+
+// Signal that worker is ready
+ctx.postMessage({ type: 'ready' });
+
+// Export empty object for TypeScript module
+export {};
diff --git a/apps/dashboard/lib/workers/index.ts b/apps/dashboard/lib/workers/index.ts
new file mode 100644
index 0000000..2ca50f1
--- /dev/null
+++ b/apps/dashboard/lib/workers/index.ts
@@ -0,0 +1,13 @@
+/**
+ * Export workers module
+ *
+ * Re-exports the worker service for easy consumption
+ */
+
+export { exportWorkerService, ExportWorkerService } from './export-worker-service';
+export type {
+ NoisePayload,
+ BlurPayload,
+ OpacityPayload,
+ CompositePayload
+} from './export-worker-service';
diff --git a/apps/dashboard/next.config.ts b/apps/dashboard/next.config.ts
index 5f2ec94..f61540d 100644
--- a/apps/dashboard/next.config.ts
+++ b/apps/dashboard/next.config.ts
@@ -5,6 +5,11 @@ const nextConfig: NextConfig = {
reactCompiler: true,
transpilePackages: ["@workspace/ui"],
typedRoutes: true,
+ cacheComponents: true,
+ partialPrefetching: true,
+ experimental: {
+ useOffline: true,
+ },
};
export default nextConfig;
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 8d93f14..b01d601 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -13,26 +13,31 @@
"dependencies": {
"@castfy/ui": "workspace:*",
"@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-slider": "^1.4.7",
"@tanstack/react-form": "^1.32.0",
"@tanstack/react-table": "^8.21.3",
"cmdk": "^1.1.1",
"geist": "^1.7.0",
+ "hugeicons-react": "^0.4.0",
"lucide-react": "^1.16.0",
"motion": "^12.38.0",
- "next": "16.2.10",
+ "next": "16.3.0",
"next-themes": "^0.4.6",
"nuqs": "^2.9.3",
- "react": "19.2.7",
- "react-dom": "19.2.7",
+ "react": "19.2.8",
+ "react-dom": "19.2.8",
+ "react-dropzone": "^20.0.0",
"react-email": "^6.6.0",
"react-hook-form": "^7.76.0",
"react-icons": "^5.6.0",
+ "react-moveable": "^0.56.0",
"resend": "^6.12.3",
"shadcn": "^4.7.0",
"sonner": "^2.0.7",
"tw-animate-css": "^1.4.0",
"vaul": "^1.1.2",
"zod": "^4.4.3",
+ "zundo": "^2.3.0",
"zustand": "^5.0.14"
},
"devDependencies": {
diff --git a/apps/dashboard/types/index.d.ts b/apps/dashboard/types/index.d.ts
index 11475d6..2da84ce 100644
--- a/apps/dashboard/types/index.d.ts
+++ b/apps/dashboard/types/index.d.ts
@@ -20,3 +20,35 @@ export interface Tdemo {
slug: string;
updatedAt: string;
}
+
+export interface AgentStep {
+ action: string;
+ description: string;
+ error?: string;
+ ref?: string;
+ status: "success" | "error";
+ value?: string;
+}
+
+export type AIProvider = "anthropic" | "openai" | "gemini";
+
+// Shape of any SSE event payload the backend emits (status/step/completed/error).
+export interface SseEventData {
+ action?: string;
+ description?: string;
+ error?: string;
+ message?: string;
+ ref?: string;
+ status?: "success" | "error";
+ steps?: AgentStep[];
+ value?: string;
+ videos?: Record;
+ videoUrl?: string;
+}
+
+export interface SseHandlers {
+ onCompleted: (data: SseEventData) => void;
+ onError: (message: string) => void;
+ onStatus: (message: string) => void;
+ onStep: (step: AgentStep) => void;
+}
diff --git a/apps/dashboard/types/mockup.ts b/apps/dashboard/types/mockup.ts
new file mode 100644
index 0000000..35f04c7
--- /dev/null
+++ b/apps/dashboard/types/mockup.ts
@@ -0,0 +1,36 @@
+export type MockupType = "iphone" | "macbook" | "imac" | "iwatch";
+
+export interface MockupScreenArea {
+ borderRadius?: number;
+ height: number;
+ notch?: {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ borderRadius?: number;
+ };
+ width: number;
+ x: number;
+ y: number;
+}
+
+export interface MockupDefinition {
+ id: string;
+ name: string;
+ preview?: string;
+ screenArea: MockupScreenArea;
+ src: string;
+ type: MockupType;
+}
+
+export interface Mockup {
+ definitionId: string;
+ id: string;
+ imageFit: "cover" | "contain" | "fill";
+ isVisible: boolean;
+ opacity: number;
+ position: { x: number; y: number };
+ rotation: number;
+ size: number;
+}
diff --git a/biome.jsonc b/biome.jsonc
index 0aaa366..8a81bdb 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -8,6 +8,9 @@
"suspicious": {
"noUnknownAtRules": "off",
"noArrayIndexKey": "off"
+ },
+ "style": {
+ "noNestedTernary": "off"
}
},
"domains": {
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
index 2de9541..66b992b 100644
--- a/packages/ui/src/components/dialog.tsx
+++ b/packages/ui/src/components/dialog.tsx
@@ -104,18 +104,18 @@ function DialogFooter({
return (
- {children}
{showCloseButton && (
-
+
)}
+ {children}
);
}
diff --git a/packages/ui/src/components/switch.tsx b/packages/ui/src/components/switch.tsx
new file mode 100644
index 0000000..d59ca83
--- /dev/null
+++ b/packages/ui/src/components/switch.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import * as React from "react"
+import { Switch as SwitchPrimitive } from "radix-ui"
+
+import { cn } from "@castfy/ui/lib/utils"
+
+function Switch({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5600589..cacb89a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,70 +31,85 @@ importers:
version: link:../../packages/ui
'@hookform/resolvers':
specifier: ^5.2.2
- version: 5.2.2(react-hook-form@7.76.0(react@19.2.7))
+ version: 5.2.2(react-hook-form@7.76.0(react@19.2.8))
+ '@radix-ui/react-slider':
+ specifier: ^1.4.7
+ version: 1.4.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@tanstack/react-form':
specifier: ^1.32.0
- version: 1.32.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 1.32.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@tanstack/react-table':
specifier: ^8.21.3
- version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
cmdk:
specifier: ^1.1.1
- version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
geist:
specifier: ^1.7.0
- version: 1.7.0(next@16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))
+ version: 1.7.0(next@16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))
+ hugeicons-react:
+ specifier: ^0.4.0
+ version: 0.4.0(react@19.2.8)
lucide-react:
specifier: ^1.16.0
- version: 1.16.0(react@19.2.7)
+ version: 1.16.0(react@19.2.8)
motion:
specifier: ^12.38.0
- version: 12.38.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 12.38.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
next:
- specifier: 16.2.10
- version: 16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ specifier: 16.3.0
+ version: 16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
next-themes:
specifier: ^0.4.6
- version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
nuqs:
specifier: ^2.9.3
- version: 2.9.3(next@16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
+ version: 2.9.3(next@16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)
react:
- specifier: 19.2.7
- version: 19.2.7
+ specifier: 19.2.8
+ version: 19.2.8
react-dom:
- specifier: 19.2.7
- version: 19.2.7(react@19.2.7)
+ specifier: 19.2.8
+ version: 19.2.8(react@19.2.8)
+ react-dropzone:
+ specifier: ^20.0.0
+ version: 20.0.0(@types/react@19.2.14)(react@19.2.8)
react-email:
specifier: ^6.6.0
- version: 6.6.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
react-hook-form:
specifier: ^7.76.0
- version: 7.76.0(react@19.2.7)
+ version: 7.76.0(react@19.2.8)
react-icons:
specifier: ^5.6.0
- version: 5.6.0(react@19.2.7)
+ version: 5.6.0(react@19.2.8)
+ react-moveable:
+ specifier: ^0.56.0
+ version: 0.56.0
resend:
specifier: ^6.12.3
- version: 6.12.3(@react-email/render@2.0.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7))
+ version: 6.12.3(@react-email/render@2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))
shadcn:
specifier: ^4.7.0
version: 4.7.0(@types/node@20.19.41)(typescript@5.9.3)
sonner:
specifier: ^2.0.7
- version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
vaul:
specifier: ^1.1.2
- version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
zod:
specifier: ^4.4.3
version: 4.4.3
+ zundo:
+ specifier: ^2.3.0
+ version: 2.3.0(zustand@5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)))
zustand:
specifier: ^5.0.14
- version: 5.0.14(@types/react@19.2.14)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7))
+ version: 5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
devDependencies:
'@biomejs/biome':
specifier: 2.4.16
@@ -659,6 +674,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@cfcs/core@0.0.6':
+ resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==}
+
'@clack/core@1.4.1':
resolution: {integrity: sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==}
engines: {node: '>= 20.12.0'}
@@ -667,6 +685,9 @@ packages:
resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==}
engines: {node: '>= 20.12.0'}
+ '@daybrush/utils@1.13.0':
+ resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==}
+
'@derhuerst/http-basic@8.2.4':
resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
engines: {node: '>=6.0.0'}
@@ -681,6 +702,21 @@ packages:
peerDependencies:
'@noble/ciphers': ^1.0.0
+ '@egjs/agent@2.4.4':
+ resolution: {integrity: sha512-cvAPSlUILhBBOakn2krdPnOGv5hAZq92f1YHxYcfu0p7uarix2C6Ia3AVizpS1SGRZGiEkIS5E+IVTLg1I2Iog==}
+
+ '@egjs/children-differ@1.0.1':
+ resolution: {integrity: sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ==}
+
+ '@egjs/component@3.0.5':
+ resolution: {integrity: sha512-cLcGizTrrUNA2EYE3MBmEDt2tQv1joVP1Q3oDisZ5nw0MZDx2kcgEXM+/kZpfa/PAkFvYVhRUZwytIQWoN3V/w==}
+
+ '@egjs/list-differ@1.0.1':
+ resolution: {integrity: sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg==}
+
+ '@emnapi/runtime@1.11.3':
+ resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
+
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
@@ -1069,6 +1105,14 @@ packages:
peerDependencies:
react-hook-form: ^7.55.0
+ '@hugeicons/core-free-icons@3.3.0':
+ resolution: {integrity: sha512-qYyr4JQ2eQIHTSTbITvnJvs6ERNK64D9gpwZnf2IyuG0exzqfyABLO/oTB71FB3RZPfu1GbwycdiGSo46apjMQ==}
+
+ '@hugeicons/react@1.1.9':
+ resolution: {integrity: sha512-O+lWSWjbijoAvMCxn4K2bQWCGN5+mP1y5j+X99j23mXMj+s0X25fs71T6t9YJLaBodwmZdaewD27dzS/PiboQw==}
+ peerDependencies:
+ react: '>=16.0.0'
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -1089,139 +1133,285 @@ packages:
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
+ '@img/sharp-darwin-arm64@0.35.3':
+ resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [darwin]
+
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
+ '@img/sharp-darwin-x64@0.35.3':
+ resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-freebsd-wasm32@0.35.3':
+ resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
+ engines: {node: '>=20.9.0'}
+ os: [freebsd]
+
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
+ '@img/sharp-libvips-darwin-arm64@1.3.2':
+ resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
+ cpu: [arm64]
+ os: [darwin]
+
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
+ '@img/sharp-libvips-darwin-x64@1.3.2':
+ resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
+ cpu: [x64]
+ os: [darwin]
+
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ '@img/sharp-libvips-linux-arm64@1.3.2':
+ resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ '@img/sharp-libvips-linux-arm@1.3.2':
+ resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
+ cpu: [arm]
+ os: [linux]
+
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ '@img/sharp-libvips-linux-ppc64@1.3.2':
+ resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
+ cpu: [ppc64]
+ os: [linux]
+
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ '@img/sharp-libvips-linux-riscv64@1.3.2':
+ resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
+ cpu: [riscv64]
+ os: [linux]
+
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ '@img/sharp-libvips-linux-s390x@1.3.2':
+ resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
+ cpu: [s390x]
+ os: [linux]
+
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ '@img/sharp-libvips-linux-x64@1.3.2':
+ resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+ resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+ resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ '@img/sharp-linux-arm64@0.35.3':
+ resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ '@img/sharp-linux-arm@0.35.3':
+ resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm]
+ os: [linux]
+
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ '@img/sharp-linux-ppc64@0.35.3':
+ resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [ppc64]
+ os: [linux]
+
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ '@img/sharp-linux-riscv64@0.35.3':
+ resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [riscv64]
+ os: [linux]
+
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ '@img/sharp-linux-s390x@0.35.3':
+ resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
+ engines: {node: '>=20.9.0'}
+ cpu: [s390x]
+ os: [linux]
+
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ '@img/sharp-linux-x64@0.35.3':
+ resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ '@img/sharp-linuxmusl-arm64@0.35.3':
+ resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ '@img/sharp-linuxmusl-x64@0.35.3':
+ resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
+ '@img/sharp-wasm32@0.35.3':
+ resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
+ engines: {node: '>=20.9.0'}
+
+ '@img/sharp-webcontainers-wasm32@0.35.3':
+ resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
+ engines: {node: '>=20.9.0'}
+ cpu: [wasm32]
+
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
+ '@img/sharp-win32-arm64@0.35.3':
+ resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [win32]
+
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
+ '@img/sharp-win32-ia32@0.35.3':
+ resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
+ engines: {node: ^20.9.0}
+ cpu: [ia32]
+ os: [win32]
+
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
+ '@img/sharp-win32-x64@0.35.3':
+ resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [win32]
+
'@inquirer/ansi@1.0.2':
resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
engines: {node: '>=18'}
@@ -1424,54 +1614,105 @@ packages:
'@next/env@16.2.10':
resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==}
+ '@next/env@16.3.0':
+ resolution: {integrity: sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==}
+
'@next/swc-darwin-arm64@16.2.10':
resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
+ '@next/swc-darwin-arm64@16.3.0':
+ resolution: {integrity: sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
'@next/swc-darwin-x64@16.2.10':
resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
+ '@next/swc-darwin-x64@16.3.0':
+ resolution: {integrity: sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
'@next/swc-linux-arm64-gnu@16.2.10':
resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-gnu@16.3.0':
+ resolution: {integrity: sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-arm64-musl@16.2.10':
resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-musl@16.3.0':
+ resolution: {integrity: sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-x64-gnu@16.2.10':
resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-gnu@16.3.0':
+ resolution: {integrity: sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-linux-x64-musl@16.2.10':
resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-musl@16.3.0':
+ resolution: {integrity: sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-win32-arm64-msvc@16.2.10':
resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
+ '@next/swc-win32-arm64-msvc@16.3.0':
+ resolution: {integrity: sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
'@next/swc-win32-x64-msvc@16.2.10':
resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
+ '@next/swc-win32-x64-msvc@16.3.0':
+ resolution: {integrity: sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@noble/ciphers@1.3.0':
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
engines: {node: ^14.21.3 || >=16}
@@ -1560,9 +1801,15 @@ packages:
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+ '@radix-ui/number@1.1.3':
+ resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==}
+
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+ '@radix-ui/primitive@1.1.7':
+ resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
+
'@radix-ui/react-accessible-icon@1.1.7':
resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==}
peerDependencies:
@@ -1667,6 +1914,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-collection@1.1.15':
+ resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -1689,6 +1949,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-compose-refs@1.1.5':
+ resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-context-menu@2.2.16':
resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==}
peerDependencies:
@@ -1711,6 +1980,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-context@1.2.2':
+ resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
peerDependencies:
@@ -1733,6 +2011,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-direction@1.1.4':
+ resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-dismissable-layer@1.1.11':
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
peerDependencies:
@@ -1946,6 +2233,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-primitive@2.1.10':
+ resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
@@ -2050,6 +2350,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-slider@1.4.7':
+ resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
@@ -2059,6 +2372,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-slot@1.3.3':
+ resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-switch@1.2.6':
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
peerDependencies:
@@ -2168,6 +2490,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-controllable-state@1.2.6':
+ resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
@@ -2177,6 +2508,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-effect-event@0.0.5':
+ resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-use-escape-keydown@1.1.1':
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
@@ -2204,6 +2544,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-layout-effect@1.1.4':
+ resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-use-previous@1.1.1':
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
@@ -2213,6 +2562,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-previous@1.1.4':
+ resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-use-rect@1.1.1':
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies:
@@ -2231,6 +2589,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-size@1.1.4':
+ resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-visually-hidden@1.2.3':
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
peerDependencies:
@@ -2254,6 +2621,15 @@ packages:
react: ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
+ '@scena/dragscroll@1.4.0':
+ resolution: {integrity: sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA==}
+
+ '@scena/event-emitter@1.0.5':
+ resolution: {integrity: sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg==}
+
+ '@scena/matrix@1.1.1':
+ resolution: {integrity: sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==}
+
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -2663,6 +3039,10 @@ packages:
atomically@2.1.1:
resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==}
+ attr-accept@2.2.5:
+ resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
+ engines: {node: '>=4'}
+
babel-plugin-react-compiler@1.0.0:
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
@@ -2948,6 +3328,12 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ css-styled@1.0.8:
+ resolution: {integrity: sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g==}
+
+ css-to-mat@1.1.1:
+ resolution: {integrity: sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==}
+
css-tree@3.2.1:
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -3361,6 +3747,10 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ file-selector@4.1.0:
+ resolution: {integrity: sha512-Io1mP8CI3zec5Bxy3P3TxdrKnt35Cm8vNIHnZsvyj43l4YFjD4NRInBp240S5bDJQ0EP1jnh7nCAwXsO818OCg==}
+ engines: {node: '>= 20'}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -3417,6 +3807,9 @@ packages:
react-dom:
optional: true
+ framework-utils@1.1.0:
+ resolution: {integrity: sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg==}
+
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@@ -3469,6 +3862,9 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ gesto@1.19.4:
+ resolution: {integrity: sha512-hfr/0dWwh0Bnbb88s3QVJd1ZRJeOWcgHPPwmiH6NnafDYvhTsxg+SLYu+q/oPNh9JS3V+nlr6fNs8kvPAtcRDQ==}
+
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -3620,6 +4016,11 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
+ hugeicons-react@0.4.0:
+ resolution: {integrity: sha512-HA2UI3VkDd1dNi9P4JGjaMODdGEs4QVMwQcc34W0aoGHptdsIex/756Jik4GFYN/023jGFKNVvQKhx2IpZtV+g==}
+ peerDependencies:
+ react: '>=16.0.0'
+
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -3863,6 +4264,12 @@ packages:
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
+ keycode@2.2.1:
+ resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==}
+
+ keycon@1.4.0:
+ resolution: {integrity: sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A==}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4153,6 +4560,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanoid@3.3.17:
+ resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -4198,6 +4610,27 @@ packages:
sass:
optional: true
+ next@16.3.0:
+ resolution: {integrity: sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==}
+ engines: {node: '>=20.9.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.51.1
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
no-case@2.3.2:
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
@@ -4326,6 +4759,9 @@ packages:
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ overlap-area@1.1.0:
+ resolution: {integrity: sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw==}
+
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -4469,6 +4905,10 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.23:
+ resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
+ engines: {node: ^10 || ^12 || >=14}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -4571,6 +5011,9 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
+ react-css-styled@1.1.9:
+ resolution: {integrity: sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==}
+
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -4581,6 +5024,21 @@ packages:
peerDependencies:
react: ^19.2.7
+ react-dom@19.2.8:
+ resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
+ peerDependencies:
+ react: ^19.2.8
+
+ react-dropzone@20.0.0:
+ resolution: {integrity: sha512-Xw8tvvVPJQzj8ir5wivUMzA+G6R+aGhdU5KQzUMvVBlJNb26AW/0137VoYVmb5UgZcbhM9OCpjE4KOqqSL9QuQ==}
+ engines: {node: '>= 22'}
+ peerDependencies:
+ '@types/react': '*'
+ react: '>= 18'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react-email@6.6.0:
resolution: {integrity: sha512-YNOLCMcGqwcvRTzFo2qMVD5D8Ro9aw0Y+PxO1LhqQT+qbkRvdUxmrVXffUxEcDeIFAcoOe7luCbkwIZPpPDFxQ==}
engines: {node: '>=20.0.0'}
@@ -4600,6 +5058,9 @@ packages:
peerDependencies:
react: '*'
+ react-moveable@0.56.0:
+ resolution: {integrity: sha512-FmJNmIOsOA36mdxbrc/huiE4wuXSRlmon/o+/OrfNhSiYYYL0AV5oObtPluEhb2Yr/7EfYWBHTxF5aWAvjg1SA==}
+
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
@@ -4620,6 +5081,9 @@ packages:
'@types/react':
optional: true
+ react-selecto@1.26.3:
+ resolution: {integrity: sha512-Ubik7kWSnZyQEBNro+1k38hZaI1tJarE+5aD/qsqCOA1uUBSjgKVBy3EWRzGIbdmVex7DcxznFZLec/6KZNvwQ==}
+
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -4647,6 +5111,10 @@ packages:
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
engines: {node: '>=0.10.0'}
+ react@19.2.8:
+ resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
+ engines: {node: '>=0.10.0'}
+
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -4753,6 +5221,9 @@ packages:
selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
+ selecto@1.26.3:
+ resolution: {integrity: sha512-gZHgqMy5uyB6/2YDjv3Qqaf7bd2hTDOpPdxXlrez4R3/L0GiEWDCFaUfrflomgqdb3SxHF2IXY0Jw0EamZi7cw==}
+
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -4767,6 +5238,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
send@0.19.2:
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
engines: {node: '>= 0.8.0'}
@@ -4800,6 +5276,15 @@ packages:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ sharp@0.35.3:
+ resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -5320,6 +5805,11 @@ packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+ zundo@2.3.0:
+ resolution: {integrity: sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ==}
+ peerDependencies:
+ zustand: ^4.3.0 || ^5.0.0
+
zustand@5.0.14:
resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
engines: {node: '>=12.20.0'}
@@ -5628,6 +6118,10 @@ snapshots:
'@biomejs/cli-win32-x64@2.4.16':
optional: true
+ '@cfcs/core@0.0.6':
+ dependencies:
+ '@egjs/component': 3.0.5
+
'@clack/core@1.4.1':
dependencies:
fast-wrap-ansi: 0.2.0
@@ -5640,6 +6134,8 @@ snapshots:
fast-wrap-ansi: 0.2.0
sisteransi: 1.0.5
+ '@daybrush/utils@1.13.0': {}
+
'@derhuerst/http-basic@8.2.4':
dependencies:
caseless: 0.12.0
@@ -5664,6 +6160,21 @@ snapshots:
dependencies:
'@noble/ciphers': 1.3.0
+ '@egjs/agent@2.4.4': {}
+
+ '@egjs/children-differ@1.0.1':
+ dependencies:
+ '@egjs/list-differ': 1.0.1
+
+ '@egjs/component@3.0.5': {}
+
+ '@egjs/list-differ@1.0.1': {}
+
+ '@emnapi/runtime@1.11.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
'@emnapi/runtime@1.7.1':
dependencies:
tslib: 2.8.1
@@ -5910,6 +6421,17 @@ snapshots:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.76.0(react@19.2.7)
+ '@hookform/resolvers@5.2.2(react-hook-form@7.76.0(react@19.2.8))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.76.0(react@19.2.8)
+
+ '@hugeicons/core-free-icons@3.3.0': {}
+
+ '@hugeicons/react@1.1.9(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@@ -5924,79 +6446,162 @@ snapshots:
'@img/colour@1.0.0':
optional: true
+ '@img/colour@1.1.0':
+ optional: true
+
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
+ '@img/sharp-darwin-arm64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.3.2
+ optional: true
+
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
+ '@img/sharp-darwin-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.3.2
+ optional: true
+
+ '@img/sharp-freebsd-wasm32@0.35.3':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.3
+ optional: true
+
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
+ '@img/sharp-libvips-darwin-arm64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
+ '@img/sharp-libvips-darwin-x64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
+ '@img/sharp-libvips-linux-arm64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
+ '@img/sharp-libvips-linux-arm@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
+ '@img/sharp-libvips-linux-ppc64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
+ '@img/sharp-libvips-linux-riscv64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
+ '@img/sharp-libvips-linux-s390x@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
+ '@img/sharp-libvips-linux-x64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+ optional: true
+
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
+ '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+ optional: true
+
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
+ '@img/sharp-linux-arm64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.3.2
+ optional: true
+
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
+ '@img/sharp-linux-arm@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.3.2
+ optional: true
+
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
+ '@img/sharp-linux-ppc64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.3.2
+ optional: true
+
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
+ '@img/sharp-linux-riscv64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.3.2
+ optional: true
+
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
+ '@img/sharp-linux-s390x@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.3.2
+ optional: true
+
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
+ '@img/sharp-linux-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.3.2
+ optional: true
+
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
@@ -6004,20 +6609,44 @@ snapshots:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
+ '@img/sharp-linuxmusl-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+ optional: true
+
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.7.1
optional: true
+ '@img/sharp-wasm32@0.35.3':
+ dependencies:
+ '@emnapi/runtime': 1.11.3
+ optional: true
+
+ '@img/sharp-webcontainers-wasm32@0.35.3':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.3
+ optional: true
+
'@img/sharp-win32-arm64@0.34.5':
optional: true
+ '@img/sharp-win32-arm64@0.35.3':
+ optional: true
+
'@img/sharp-win32-ia32@0.34.5':
optional: true
+ '@img/sharp-win32-ia32@0.35.3':
+ optional: true
+
'@img/sharp-win32-x64@0.34.5':
optional: true
+ '@img/sharp-win32-x64@0.35.3':
+ optional: true
+
'@inquirer/ansi@1.0.2': {}
'@inquirer/ansi@2.0.5': {}
@@ -6245,30 +6874,56 @@ snapshots:
'@next/env@16.2.10': {}
+ '@next/env@16.3.0': {}
+
'@next/swc-darwin-arm64@16.2.10':
optional: true
+ '@next/swc-darwin-arm64@16.3.0':
+ optional: true
+
'@next/swc-darwin-x64@16.2.10':
optional: true
+ '@next/swc-darwin-x64@16.3.0':
+ optional: true
+
'@next/swc-linux-arm64-gnu@16.2.10':
optional: true
+ '@next/swc-linux-arm64-gnu@16.3.0':
+ optional: true
+
'@next/swc-linux-arm64-musl@16.2.10':
optional: true
+ '@next/swc-linux-arm64-musl@16.3.0':
+ optional: true
+
'@next/swc-linux-x64-gnu@16.2.10':
optional: true
+ '@next/swc-linux-x64-gnu@16.3.0':
+ optional: true
+
'@next/swc-linux-x64-musl@16.2.10':
optional: true
+ '@next/swc-linux-x64-musl@16.3.0':
+ optional: true
+
'@next/swc-win32-arm64-msvc@16.2.10':
optional: true
+ '@next/swc-win32-arm64-msvc@16.3.0':
+ optional: true
+
'@next/swc-win32-x64-msvc@16.2.10':
optional: true
+ '@next/swc-win32-x64-msvc@16.3.0':
+ optional: true
+
'@noble/ciphers@1.3.0': {}
'@noble/curves@1.9.7':
@@ -6342,8 +6997,12 @@ snapshots:
'@radix-ui/number@1.1.1': {}
+ '@radix-ui/number@1.1.3': {}
+
'@radix-ui/primitive@1.1.3': {}
+ '@radix-ui/primitive@1.1.7': {}
+
'@radix-ui/react-accessible-icon@1.1.7(@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)':
dependencies:
'@radix-ui/react-visually-hidden': 1.2.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)
@@ -6447,6 +7106,18 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-collection@1.1.7(@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)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -6465,9 +7136,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- react: 19.2.7
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -6491,9 +7168,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- react: 19.2.7
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-context@1.2.2(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -6519,24 +7202,24 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.8)
aria-hidden: 1.2.6
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
- react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.7)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
@@ -6547,6 +7230,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-direction@1.1.4(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-dismissable-layer@1.1.11(@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)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -6560,15 +7249,15 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
@@ -6594,9 +7283,9 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- react: 19.2.7
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -6611,13 +7300,13 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
@@ -6660,10 +7349,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -6829,12 +7518,12 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
@@ -6849,12 +7538,21 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
@@ -6868,11 +7566,11 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
@@ -6996,6 +7694,25 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@radix-ui/number': 1.1.3
+ '@radix-ui/primitive': 1.1.7
+ '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -7003,10 +7720,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-slot@1.3.3(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -7128,9 +7852,9 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- react: 19.2.7
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -7142,11 +7866,20 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.7
+ '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -7157,10 +7890,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -7171,10 +7911,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- react: 19.2.7
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -7191,9 +7931,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.7)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.8)':
dependencies:
- react: 19.2.7
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.14
@@ -7203,6 +7949,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/rect': 1.1.1
@@ -7217,6 +7969,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-use-size@1.1.4(@types/react@19.2.14)(react@19.2.8)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8)
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-visually-hidden@1.2.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)':
dependencies:
'@radix-ui/react-primitive': 2.1.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)
@@ -7235,6 +7994,26 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
+ '@react-email/render@2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ html-to-text: 9.0.5
+ prettier: 3.8.4
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+
+ '@scena/dragscroll@1.4.0':
+ dependencies:
+ '@daybrush/utils': 1.13.0
+ '@scena/event-emitter': 1.0.5
+
+ '@scena/event-emitter@1.0.5':
+ dependencies:
+ '@daybrush/utils': 1.13.0
+
+ '@scena/matrix@1.1.1':
+ dependencies:
+ '@daybrush/utils': 1.13.0
+
'@sec-ant/readable-stream@0.4.1': {}
'@selderee/plugin-htmlparser2@0.11.0':
@@ -7348,6 +8127,14 @@ snapshots:
transitivePeerDependencies:
- react-dom
+ '@tanstack/react-form@1.32.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@tanstack/form-core': 1.32.0
+ '@tanstack/react-store': 0.9.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react: 19.2.8
+ transitivePeerDependencies:
+ - react-dom
+
'@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@tanstack/store': 0.9.3
@@ -7355,11 +8142,18 @@ snapshots:
react-dom: 19.2.7(react@19.2.7)
use-sync-external-store: 1.6.0(react@19.2.7)
- '@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ '@tanstack/react-store@0.9.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@tanstack/store': 0.9.3
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ use-sync-external-store: 1.6.0(react@19.2.8)
+
+ '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@tanstack/table-core': 8.21.3
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
'@tanstack/store@0.9.3': {}
@@ -7645,6 +8439,8 @@ snapshots:
stubborn-fs: 2.0.0
when-exit: 2.1.5
+ attr-accept@2.2.5: {}
+
babel-plugin-react-compiler@1.0.0:
dependencies:
'@babel/types': 7.29.0
@@ -7835,14 +8631,14 @@ snapshots:
clsx@2.1.1: {}
- cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.7)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -7945,6 +8741,15 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ css-styled@1.0.8:
+ dependencies:
+ '@daybrush/utils': 1.13.0
+
+ css-to-mat@1.1.1:
+ dependencies:
+ '@daybrush/utils': 1.13.0
+ '@scena/matrix': 1.1.1
+
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
@@ -8481,6 +9286,8 @@ snapshots:
dependencies:
flat-cache: 4.0.1
+ file-selector@4.1.0: {}
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -8550,6 +9357,17 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
+ framer-motion@12.38.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ motion-dom: 12.38.0
+ motion-utils: 12.36.0
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+
+ framework-utils@1.1.0: {}
+
fresh@0.5.2: {}
fresh@2.0.0: {}
@@ -8598,8 +9416,17 @@ snapshots:
dependencies:
next: 16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ geist@1.7.0(next@16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)):
+ dependencies:
+ next: 16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+
gensync@1.0.0-beta.2: {}
+ gesto@1.19.4:
+ dependencies:
+ '@daybrush/utils': 1.13.0
+ '@scena/event-emitter': 1.0.5
+
get-caller-file@2.0.5: {}
get-east-asian-width@1.6.0: {}
@@ -8790,6 +9617,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ hugeicons-react@0.4.0(react@19.2.8):
+ dependencies:
+ '@hugeicons/core-free-icons': 3.3.0
+ '@hugeicons/react': 1.1.9(react@19.2.8)
+ react: 19.2.8
+
human-signals@2.1.0: {}
human-signals@8.0.1: {}
@@ -8983,6 +9816,15 @@ snapshots:
jwa: 2.0.1
safe-buffer: 5.2.1
+ keycode@2.2.1: {}
+
+ keycon@1.4.0:
+ dependencies:
+ '@cfcs/core': 0.0.6
+ '@daybrush/utils': 1.13.0
+ '@scena/event-emitter': 1.0.5
+ keycode: 2.2.1
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -9097,6 +9939,10 @@ snapshots:
dependencies:
react: 19.2.7
+ lucide-react@1.16.0(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -9178,6 +10024,14 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
+ motion@12.38.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ framer-motion: 12.38.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+
ms@2.0.0: {}
ms@2.1.3: {}
@@ -9240,6 +10094,8 @@ snapshots:
nanoid@3.3.11: {}
+ nanoid@3.3.17: {}
+
natural-compare@1.4.0: {}
negotiator@0.6.3: {}
@@ -9260,6 +10116,11 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
+ next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+
next@16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
'@next/env': 16.2.10
@@ -9285,6 +10146,32 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
+ next@16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ '@next/env': 16.3.0
+ '@swc/helpers': 0.5.15
+ baseline-browser-mapping: 2.9.19
+ caniuse-lite: 1.0.30001760
+ postcss: 8.5.23
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.8)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 16.3.0
+ '@next/swc-darwin-x64': 16.3.0
+ '@next/swc-linux-arm64-gnu': 16.3.0
+ '@next/swc-linux-arm64-musl': 16.3.0
+ '@next/swc-linux-x64-gnu': 16.3.0
+ '@next/swc-linux-x64-musl': 16.3.0
+ '@next/swc-win32-arm64-msvc': 16.3.0
+ '@next/swc-win32-x64-msvc': 16.3.0
+ babel-plugin-react-compiler: 1.0.0
+ sharp: 0.35.3(@types/node@20.19.41)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - '@types/node'
+ - babel-plugin-macros
+
no-case@2.3.2:
dependencies:
lower-case: 1.1.4
@@ -9328,12 +10215,12 @@ snapshots:
path-key: 4.0.0
unicorn-magic: 0.3.0
- nuqs@2.9.3(next@16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
+ nuqs@2.9.3(next@16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8):
dependencies:
'@standard-schema/spec': 1.1.0
- react: 19.2.7
+ react: 19.2.8
optionalDependencies:
- next: 16.2.10(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ next: 16.3.0(@babel/core@7.29.0)(@types/node@20.19.41)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
nypm@0.6.6:
dependencies:
@@ -9413,6 +10300,10 @@ snapshots:
outvariant@1.4.3: {}
+ overlap-area@1.1.0:
+ dependencies:
+ '@daybrush/utils': 1.13.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -9545,6 +10436,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ postcss@8.5.23:
+ dependencies:
+ nanoid: 3.3.17
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -9717,6 +10614,11 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
+ react-css-styled@1.1.9:
+ dependencies:
+ css-styled: 1.0.8
+ framework-utils: 1.1.0
+
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
@@ -9727,6 +10629,19 @@ snapshots:
react: 19.2.7
scheduler: 0.27.0
+ react-dom@19.2.8(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ scheduler: 0.27.0
+
+ react-dropzone@20.0.0(@types/react@19.2.14)(react@19.2.8):
+ dependencies:
+ attr-accept: 2.2.5
+ file-selector: 4.1.0
+ react: 19.2.8
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-email@6.6.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
'@babel/parser': 7.27.0
@@ -9758,13 +10673,64 @@ snapshots:
- supports-color
- utf-8-validate
+ react-email@6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ '@babel/parser': 7.27.0
+ '@babel/traverse': 7.27.0
+ '@react-email/render': 2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ chokidar: 4.0.3
+ commander: 13.1.0
+ conf: 15.1.0
+ css-tree: 3.2.1
+ debounce: 2.2.0
+ esbuild: 0.28.1
+ glob: 13.0.6
+ jiti: 2.4.2
+ log-symbols: 7.0.1
+ marked: 15.0.12
+ mime-types: 3.0.2
+ normalize-path: 3.0.0
+ nypm: 0.6.6
+ picospinner: 3.0.0
+ prismjs: 1.30.0
+ prompts: 2.4.2
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ socket.io: 4.8.3
+ tailwindcss: 4.1.18
+ tsconfig-paths: 4.2.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
react-hook-form@7.76.0(react@19.2.7):
dependencies:
react: 19.2.7
- react-icons@5.6.0(react@19.2.7):
+ react-hook-form@7.76.0(react@19.2.8):
dependencies:
- react: 19.2.7
+ react: 19.2.8
+
+ react-icons@5.6.0(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+
+ react-moveable@0.56.0:
+ dependencies:
+ '@daybrush/utils': 1.13.0
+ '@egjs/agent': 2.4.4
+ '@egjs/children-differ': 1.0.1
+ '@egjs/list-differ': 1.0.1
+ '@scena/dragscroll': 1.4.0
+ '@scena/event-emitter': 1.0.5
+ '@scena/matrix': 1.1.1
+ css-to-mat: 1.1.1
+ framework-utils: 1.1.0
+ gesto: 1.19.4
+ overlap-area: 1.1.0
+ react-css-styled: 1.1.9
+ react-selecto: 1.26.3
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
dependencies:
@@ -9774,10 +10740,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.7):
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.8):
dependencies:
- react: 19.2.7
- react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.7)
+ react: 19.2.8
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.8)
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
@@ -9793,17 +10759,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.7):
+ react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.8):
dependencies:
- react: 19.2.7
- react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.7)
- react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.7)
+ react: 19.2.8
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.8)
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.8)
tslib: 2.8.1
- use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.7)
- use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.7)
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.8)
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.14
+ react-selecto@1.26.3:
+ dependencies:
+ selecto: 1.26.3
+
react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
get-nonce: 1.0.1
@@ -9812,10 +10782,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.7):
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.8):
dependencies:
get-nonce: 1.0.1
- react: 19.2.7
+ react: 19.2.8
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
@@ -9830,6 +10800,8 @@ snapshots:
react@19.2.7: {}
+ react@19.2.8: {}
+
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -9868,6 +10840,13 @@ snapshots:
optionalDependencies:
'@react-email/render': 2.0.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ resend@6.12.3(@react-email/render@2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8)):
+ dependencies:
+ postal-mime: 2.7.4
+ svix: 1.92.2
+ optionalDependencies:
+ '@react-email/render': 2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -9930,12 +10909,28 @@ snapshots:
dependencies:
parseley: 0.12.1
+ selecto@1.26.3:
+ dependencies:
+ '@daybrush/utils': 1.13.0
+ '@egjs/children-differ': 1.0.1
+ '@scena/dragscroll': 1.4.0
+ '@scena/event-emitter': 1.0.5
+ css-styled: 1.0.8
+ css-to-mat: 1.1.1
+ framework-utils: 1.1.0
+ gesto: 1.19.4
+ keycon: 1.4.0
+ overlap-area: 1.1.0
+
semver@6.3.1: {}
semver@7.6.2: {}
semver@7.7.3: {}
+ semver@7.8.5:
+ optional: true
+
send@0.19.2:
dependencies:
debug: 2.6.9
@@ -10115,6 +11110,40 @@ snapshots:
'@img/sharp-win32-x64': 0.34.5
optional: true
+ sharp@0.35.3(@types/node@20.19.41):
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.8.5
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.35.3
+ '@img/sharp-darwin-x64': 0.35.3
+ '@img/sharp-freebsd-wasm32': 0.35.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.2
+ '@img/sharp-libvips-darwin-x64': 1.3.2
+ '@img/sharp-libvips-linux-arm': 1.3.2
+ '@img/sharp-libvips-linux-arm64': 1.3.2
+ '@img/sharp-libvips-linux-ppc64': 1.3.2
+ '@img/sharp-libvips-linux-riscv64': 1.3.2
+ '@img/sharp-libvips-linux-s390x': 1.3.2
+ '@img/sharp-libvips-linux-x64': 1.3.2
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+ '@img/sharp-linux-arm': 0.35.3
+ '@img/sharp-linux-arm64': 0.35.3
+ '@img/sharp-linux-ppc64': 0.35.3
+ '@img/sharp-linux-riscv64': 0.35.3
+ '@img/sharp-linux-s390x': 0.35.3
+ '@img/sharp-linux-x64': 0.35.3
+ '@img/sharp-linuxmusl-arm64': 0.35.3
+ '@img/sharp-linuxmusl-x64': 0.35.3
+ '@img/sharp-webcontainers-wasm32': 0.35.3
+ '@img/sharp-win32-arm64': 0.35.3
+ '@img/sharp-win32-ia32': 0.35.3
+ '@img/sharp-win32-x64': 0.35.3
+ '@types/node': 20.19.41
+ optional: true
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -10216,6 +11245,11 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
+ sonner@2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+
source-map-js@1.2.1: {}
source-map@0.6.1: {}
@@ -10279,6 +11313,13 @@ snapshots:
stubborn-utils@1.0.2: {}
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.8):
+ dependencies:
+ client-only: 0.0.1
+ react: 19.2.8
+ optionalDependencies:
+ '@babel/core': 7.29.0
+
styled-jsx@5.1.6(react@19.2.7):
dependencies:
client-only: 0.0.1
@@ -10473,9 +11514,9 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.7):
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.8):
dependencies:
- react: 19.2.7
+ react: 19.2.8
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
@@ -10488,10 +11529,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
- use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.7):
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.8):
dependencies:
detect-node-es: 1.1.0
- react: 19.2.7
+ react: 19.2.8
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
@@ -10504,6 +11545,10 @@ snapshots:
dependencies:
react: 19.2.7
+ use-sync-external-store@1.6.0(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
@@ -10516,11 +11561,11 @@ snapshots:
vary@1.1.2: {}
- vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
- '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- react: 19.2.7
- react-dom: 19.2.7(react@19.2.7)
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -10613,8 +11658,12 @@ snapshots:
zod@4.4.3: {}
- zustand@5.0.14(@types/react@19.2.14)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)):
+ zundo@2.3.0(zustand@5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))):
+ dependencies:
+ zustand: 5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
+
+ zustand@5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)):
optionalDependencies:
'@types/react': 19.2.14
- react: 19.2.7
- use-sync-external-store: 1.6.0(react@19.2.7)
+ react: 19.2.8
+ use-sync-external-store: 1.6.0(react@19.2.8)