Loading Creative Canvas Studio…
`; return /* @__PURE__ */ jsx( MemoizedIframe, { srcDoc, title: source.name, className: "w-full h-full border-none", sandbox: "allow-scripts", isInteractable } ); case "color": return /* @__PURE__ */ jsx("div", { className: "w-full h-full", style: { backgroundColor: source.content || "transparent" } }); case "gallery": const galleryImages = Array.isArray(source.content) ? source.content : []; const visibleImages = galleryImages.filter((img) => img && img.visible); const currentImage = visibleImages[galleryIndex]; return currentImage ? /* @__PURE__ */ jsx("img", { src: currentImage.url, alt: source.name, className: `${commonClasses} transition-opacity duration-1000`, draggable: false }) : /* @__PURE__ */ jsx("div", { className: "w-full h-full bg-gray-700 flex items-center justify-center text-sm", children: "No visible images" }); case "paint": return /* @__PURE__ */ jsx(PaintCanvas, { source, isLocked, onUpdateContent: (content) => onUpdate({ content }) }); case "camera": case "capture": if (streamError) { return /* @__PURE__ */ jsxs("div", { className: "w-full h-full bg-red-950/45 flex flex-col items-center justify-center p-4 text-center text-xs text-red-200 border border-red-800/40 rounded font-mono", children: [ /* @__PURE__ */ jsxs("strong", { children: [ "\u26A0\uFE0F ", source.type === "camera" ? "Camera" : "Capture", " Error" ] }), /* @__PURE__ */ jsx("span", { className: "opacity-75 mt-1.5 text-[10px] scale-95 leading-normal max-w-[90%] text-center overflow-hidden text-ellipsis line-clamp-3", children: streamError }) ] }); } return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "video", { ref: videoRef, style: { display: "none" }, playsInline: true, muted: true } ), /* @__PURE__ */ jsx( "canvas", { ref: canvasRef, className: commonClasses } ) ] }); case "youtube": if (!source.content && (!source.style.youtubePlaylist || source.style.youtubePlaylist.length === 0)) { return /* @__PURE__ */ jsxs("div", { className: "w-full h-full flex flex-col items-center justify-center bg-zinc-800/80 text-zinc-400 select-none border border-zinc-700/50 p-4 text-center pb-8", children: [ /* @__PURE__ */ jsx("span", { className: "text-3xl mb-1.5", children: "\u{1F37F}" }), /* @__PURE__ */ jsx("span", { className: "font-semibold text-[11px] uppercase tracking-wide", children: "YouTube Source" }), /* @__PURE__ */ jsx("span", { className: "text-[9px] opacity-70 mt-1", children: "Double-click or open Settings to set YouTube URL / ID or create a playlist" }) ] }); } const hasPlaylist = Array.isArray(source.style.youtubePlaylist) && source.style.youtubePlaylist.length > 0; const showPanel = hasPlaylist && source.style.youtubeShowPlaylistPanel === true; const activeIdx = source.style.youtubeActivePlaylistItemIndex ?? 0; const activeVideoUrl = hasPlaylist && source.style.youtubePlaylist && source.style.youtubePlaylist[activeIdx] ? source.style.youtubePlaylist[activeIdx].url || source.style.youtubePlaylist[activeIdx].id : source.content; const embedUrl = getYouTubeEmbedUrl(activeVideoUrl); return /* @__PURE__ */ jsxs("div", { className: "w-full h-full relative bg-black overflow-hidden select-none flex flex-row", children: [ /* @__PURE__ */ jsxs("div", { className: "w-full h-full relative", children: [ /* @__PURE__ */ jsx( MemoizedIframe, { id: `youtube-iframe-${source.id}`, url: embedUrl, title: source.name, className: "w-full h-full border-none", isInteractable } ), !showPanel && hasPlaylist && /* @__PURE__ */ jsxs( "button", { onClick: (e) => { e.stopPropagation(); onUpdate?.((s) => ({ style: { ...s.style, youtubeShowPlaylistPanel: true } })); }, className: "absolute top-2 right-2 px-2.5 py-1 bg-zinc-900/90 text-zinc-200 text-[10px] font-bold rounded shadow hover:bg-zinc-805 hover:text-white border border-zinc-700/50 flex items-center gap-1 cursor-pointer transition-all pointer-events-auto z-10", title: "Show Playlist Panel", children: [ "\u{1F4CB} Show Playlist (", source.style.youtubePlaylist?.length, ")" ] } ) ] }), /* @__PURE__ */ jsxs( "div", { className: "absolute top-0 right-0 bottom-0 w-[240px] max-w-[40%] bg-[#141416]/95 border-l border-zinc-800 text-zinc-100 flex flex-col pointer-events-auto z-20 transition-transform duration-300 ease-in-out overflow-hidden", style: { transform: showPanel ? "translateX(0)" : "translateX(100%)" }, children: [ /* @__PURE__ */ jsxs("div", { className: "p-1.5 px-2 pb-2 border-b border-zinc-800 flex items-center justify-between bg-zinc-950/90 shrink-0", children: [ /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-bold text-indigo-400 tracking-wide uppercase truncate", children: [ "Playlist (", source.style.youtubePlaylist?.length, ")" ] }), /* @__PURE__ */ jsx( "button", { onClick: (e) => { e.stopPropagation(); onUpdate?.((s) => ({ style: { ...s.style, youtubeShowPlaylistPanel: false } })); }, className: "text-[11px] text-zinc-400 hover:text-white cursor-pointer hover:bg-zinc-800 px-1 rounded transition-all animate-none", title: "Hide Playlist Panel", children: "\u2715" } ) ] }), /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden p-1 space-y-1", children: source.style.youtubePlaylist?.map((item, idx) => { const isItemActive = idx === activeIdx; let videoId = ""; const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; const match = item.url.match(regExp); if (match && match[2].length === 11) { videoId = match[2]; } else if (item.url.length === 11 && !item.url.includes("http")) { videoId = item.url; } else { const embedReg = /\/embed\/([a-zA-Z0-9_-]{11})/; const embedMatch = item.url.match(embedReg); if (embedMatch) { videoId = embedMatch[1]; } } const thumbUrl = videoId ? `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` : ""; return /* @__PURE__ */ jsxs( "button", { onClick: (e) => { e.stopPropagation(); onUpdate?.((s) => ({ style: { ...s.style, youtubeActivePlaylistItemIndex: idx } })); }, className: `w-full text-left p-1 rounded flex items-start gap-1.5 transition-all text-[9.5px] leading-tight cursor-pointer ${isItemActive ? "bg-indigo-650/30 text-indigo-300 border border-indigo-500/30 font-semibold" : "hover:bg-zinc-800 text-zinc-300 border border-transparent"}`, children: [ thumbUrl ? /* @__PURE__ */ jsx( "img", { src: thumbUrl, alt: "", className: "w-10 h-7 object-cover rounded bg-black flex-shrink-0 border border-zinc-700/50", referrerPolicy: "no-referrer" } ) : /* @__PURE__ */ jsx("div", { className: "w-10 h-7 font-mono flex items-center justify-center bg-zinc-850 text-[8px] rounded text-zinc-500 border border-zinc-750 flex-shrink-0", children: "\u25B6" }), /* @__PURE__ */ jsx("span", { className: "line-clamp-2 select-none flex-1 truncate text-left", children: item.name || `Video ${idx + 1}` }) ] }, item.id ); }) }) ] } ) ] }); case "twitch": return /* @__PURE__ */ jsx( TwitchEmbedPlayer, { source, isSelected, isInteractable } ); case "visualizer": return /* @__PURE__ */ jsx(AudioVisualizer, { source, isLocked, framerateLimit }); case "emoji": return /* @__PURE__ */ jsx( "div", { className: "w-full h-full flex items-center justify-center select-none", style: { fontSize: `${source.style.fontSize || 120}px`, textAlign: source.style.textAlign || "center", lineHeight: 1 }, children: source.content } ); default: return /* @__PURE__ */ jsx("div", { className: "w-full h-full bg-red-500 flex items-center justify-center", children: "Unknown Source Type" }); } }; var ResizeHandle = ({ position, cursor, onMouseDown, onTouchStart, isMobileMode }) => /* @__PURE__ */ jsx( "div", { className: `absolute bg-white border-2 border-indigo-600 rounded-full shadow-md transform -translate-x-1/2 -translate-y-1/2 ${position} z-[100] transition-transform active:scale-125`, style: { cursor, width: isMobileMode ? "24px" : "12px", height: isMobileMode ? "24px" : "12px" }, onMouseDown, onTouchStart } ); var InteractionOverlay = ({ onMouseDown, onTouchStart, isMobileMode }) => { return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 bg-black/20 z-10 flex items-center justify-center group pointer-events-auto", children: [ /* @__PURE__ */ jsx( "div", { className: `absolute cursor-grab active:cursor-grabbing rounded-full flex items-center justify-center transition-all bg-indigo-600/35 border border-indigo-500/50 ${isMobileMode ? "w-24 h-24 opacity-100" : "w-20 h-20 opacity-0 group-hover:opacity-100 shadow-lg"}`, onMouseDown: (e) => onMouseDown(e, "move"), onTouchStart: (e) => onTouchStart(e, "move"), title: "Move Source", children: /* @__PURE__ */ jsx("svg", { className: `text-white transition-opacity ${isMobileMode ? "w-12 h-12 opacity-95" : "w-10 h-10 opacity-70"}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M4 8h16M4 16h16M8 4l4 4 4-4M8 20l4-4 4 4" }) }) } ), /* @__PURE__ */ jsx("div", { className: "absolute top-0 left-0 w-10 h-10 cursor-nwse-resize", onMouseDown: (e) => onMouseDown(e, "resize-tl"), onTouchStart: (e) => onTouchStart(e, "resize-tl") }), /* @__PURE__ */ jsx("div", { className: "absolute top-0 right-0 w-10 h-10 cursor-nesw-resize", onMouseDown: (e) => onMouseDown(e, "resize-tr"), onTouchStart: (e) => onTouchStart(e, "resize-tr") }), /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 w-10 h-10 cursor-nesw-resize", onMouseDown: (e) => onMouseDown(e, "resize-bl"), onTouchStart: (e) => onTouchStart(e, "resize-bl") }), /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 right-0 w-10 h-10 cursor-nwse-resize", onMouseDown: (e) => onMouseDown(e, "resize-br"), onTouchStart: (e) => onTouchStart(e, "resize-br") }), /* @__PURE__ */ jsx("div", { className: `absolute top-1 left-1 border-t-4 border-l-4 border-indigo-500 opacity-85 pointer-events-none ${isMobileMode ? "w-8 h-8 rounded-tl-xl" : "w-6 h-6 rounded-tl-lg"}` }), /* @__PURE__ */ jsx("div", { className: `absolute top-1 right-1 border-t-4 border-r-4 border-indigo-500 opacity-85 pointer-events-none ${isMobileMode ? "w-8 h-8 rounded-tr-xl" : "w-6 h-6 rounded-tr-lg"}` }), /* @__PURE__ */ jsx("div", { className: `absolute bottom-1 left-1 border-b-4 border-l-4 border-indigo-500 opacity-85 pointer-events-none ${isMobileMode ? "w-8 h-8 rounded-bl-xl" : "w-6 h-6 rounded-bl-lg"}` }), /* @__PURE__ */ jsx("div", { className: `absolute bottom-1 right-1 border-b-4 border-r-4 border-indigo-500 opacity-85 pointer-events-none ${isMobileMode ? "w-8 h-8 rounded-br-xl" : "w-6 h-6 rounded-br-lg"}` }) ] }); }; var TwitchEmbedPlayer = ({ source, isSelected, isInteractable }) => { const embedRef = useRef(null); const isPlayerReadyRef = useRef(false); const containerId = useMemo(() => `twitch-embed-${source.id}`, [source.id]); let twitchChannel = ""; let twitchParent = window.location.hostname || "localhost"; let layout = "video-with-chat"; let autoplay = true; let muted = false; let twitchTheme = "dark"; if (source.content) { if (typeof source.content === "object") { const tc = source.content; twitchChannel = tc.channel || ""; twitchParent = tc.parent || window.location.hostname || "localhost"; layout = tc.layout || "video-with-chat"; autoplay = tc.autoplay !== false; muted = tc.muted === true; twitchTheme = tc.theme || "dark"; } else if (typeof source.content === "string") { try { const parsed = JSON.parse(source.content); twitchChannel = parsed.channel || ""; twitchParent = parsed.parent || window.location.hostname || "localhost"; layout = parsed.layout || "video-with-chat"; autoplay = parsed.autoplay !== false; muted = parsed.muted === true; twitchTheme = parsed.theme || "dark"; } catch { const trimmed = source.content.trim(); if (trimmed.includes("twitch.tv/")) { const urlParts = trimmed.split("twitch.tv/"); if (urlParts[1]) { const subParts = urlParts[1].split(/[/?#]/); twitchChannel = subParts[0]; } } else { twitchChannel = trimmed; } } } } const [scriptLoaded, setScriptLoaded] = useState(false); const [useIframeFallback, setUseIframeFallback] = useState(false); const hostnames = useMemo(() => { return Array.from(new Set([ twitchParent, window.location.hostname, "localhost", "aistudio.google.com", "ai.studio", "googleusercontent.com", "google.com" ].filter(Boolean))); }, [twitchParent]); const parentParams = useMemo(() => { return hostnames.map((p) => `parent=${p}`).join("&"); }, [hostnames]); useEffect(() => { let script = document.getElementById("twitch-embed-script"); const handleScriptLoad = () => { setScriptLoaded(true); }; const handleScriptError = () => { setUseIframeFallback(true); }; if (window.Twitch) { setScriptLoaded(true); } else { if (!script) { script = document.createElement("script"); script.id = "twitch-embed-script"; script.src = "https://embed.twitch.tv/embed/v1.js"; script.async = true; document.body.appendChild(script); } script.addEventListener("load", handleScriptLoad); script.addEventListener("error", handleScriptError); } const timer = setTimeout(() => { if (!window.Twitch) { setUseIframeFallback(true); } }, 1500); return () => { if (script) { script.removeEventListener("load", handleScriptLoad); script.removeEventListener("error", handleScriptError); } clearTimeout(timer); }; }, []); useEffect(() => { if (!scriptLoaded || !twitchChannel || useIframeFallback) return; const container = document.getElementById(containerId); if (container) { container.innerHTML = ""; } isPlayerReadyRef.current = false; try { const embed = new window.Twitch.Embed(containerId, { width: "100%", height: "100%", channel: twitchChannel, autoplay: isSelected && autoplay, // Only autoplay initially if actually selected muted, theme: twitchTheme, layout: layout === "chat-only" ? "video" : layout === "video" ? "video" : "video-with-chat", parent: hostnames }); embedRef.current = embed; embed.addEventListener(window.Twitch.Embed.VIDEO_READY, () => { isPlayerReadyRef.current = true; const player = embed.getPlayer(); if (player) { if (isSelected) { player.play(); } else { player.pause(); } } }); } catch (error) { console.error("Error creating Twitch embed:", error); setUseIframeFallback(true); } return () => { const container2 = document.getElementById(containerId); if (container2) { container2.innerHTML = ""; } embedRef.current = null; isPlayerReadyRef.current = false; }; }, [scriptLoaded, twitchChannel, hostnames, layout, autoplay, muted, twitchTheme, containerId, useIframeFallback]); useEffect(() => { if (embedRef.current && isPlayerReadyRef.current) { try { const player = embedRef.current.getPlayer(); if (player) { if (isSelected) { player.play(); } else { player.pause(); } } } catch (err) { console.warn("Failed to toggle Twitch playback:", err); } } }, [isSelected]); if (!twitchChannel) { return /* @__PURE__ */ jsxs("div", { className: "w-full h-full flex flex-col items-center justify-center bg-zinc-900 text-zinc-400 select-none border border-zinc-800 p-4 text-center", children: [ /* @__PURE__ */ jsx("span", { className: "text-4xl mb-2", children: "\u{1F3AE}" }), /* @__PURE__ */ jsx("span", { className: "font-semibold text-xs uppercase tracking-wide text-zinc-200", children: "Twitch Source" }), /* @__PURE__ */ jsx("span", { className: "text-[10px] opacity-70 mt-1 max-w-[200px]", children: "No channel selected. Enter a Twitch URL or channel name in options." }) ] }); } if (useIframeFallback || !scriptLoaded) { if (layout === "chat-only") { return /* @__PURE__ */ jsx( "iframe", { src: `https://www.twitch.tv/embed/${twitchChannel}/chat?${parentParams}`, title: source.name, className: "w-full h-full border-none bg-black", style: { pointerEvents: isInteractable ? "auto" : "none" }, sandbox: "allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups" } ); } if (layout === "video") { return /* @__PURE__ */ jsx( "iframe", { src: `https://player.twitch.tv/?channel=${twitchChannel}&autoplay=${autoplay}&muted=${muted}&theme=${twitchTheme}&${parentParams}`, title: source.name, className: "w-full h-full border-none bg-black", style: { pointerEvents: isInteractable ? "auto" : "none" }, allowFullScreen: true } ); } return /* @__PURE__ */ jsxs("div", { className: "w-full h-full flex flex-col md:flex-row bg-black", style: { pointerEvents: isInteractable ? "auto" : "none" }, children: [ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 min-w-0", children: /* @__PURE__ */ jsx( "iframe", { src: `https://player.twitch.tv/?channel=${twitchChannel}&autoplay=${autoplay}&muted=${muted}&theme=${twitchTheme}&${parentParams}`, title: `${source.name}-video`, className: "w-full h-full border-none", allowFullScreen: true } ) }), /* @__PURE__ */ jsx("div", { className: "w-full md:w-[320px] h-[180px] md:h-full border-t md:border-t-0 md:border-l border-zinc-800 shrink-0", children: /* @__PURE__ */ jsx( "iframe", { src: `https://www.twitch.tv/embed/${twitchChannel}/chat?${parentParams}`, title: `${source.name}-chat`, className: "w-full h-full border-none", sandbox: "allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups" } ) }) ] }); } if (layout === "chat-only") { return /* @__PURE__ */ jsx( "iframe", { src: `https://www.twitch.tv/embed/${twitchChannel}/chat?${parentParams}`, title: source.name, className: "w-full h-full border-none bg-black", style: { pointerEvents: isInteractable ? "auto" : "none" }, sandbox: "allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups" } ); } return /* @__PURE__ */ jsx( "div", { id: containerId, className: "w-full h-full bg-black relative", style: { pointerEvents: isInteractable ? "auto" : "none" } } ); }; var SourceRenderer = ({ source, isLocked, isSelected, isHovered, isInteracting, onMouseDown, onTouchStart, isMobileMode, onMouseEnter, onMouseLeave, onUpdate, onContextMenu, framerateLimit = "uncapped", isTransitioning = false, currentPlayingTransition = null }) => { const [isInteractable, setIsInteractable] = useState(false); const [showYtInput, setShowYtInput] = useState(false); const [ytInputVal, setYtInputVal] = useState(source.content); useEffect(() => { if (!isHovered || !isSelected) { setIsInteractable(false); setShowYtInput(false); } }, [isHovered, isSelected]); useEffect(() => { setYtInputVal(source.content); }, [source.content]); const { x, y, width, height, zIndex, opacity, scale, rotation, scaleX, scaleY, boxShadow, shadowColor, shadowBlur, stroke, strokeColor, strokeWidth, filters } = source.style; const getSvgKeyMatrix = (hexColor) => { if (!hexColor || hexColor.length < 7) return "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"; const r = parseInt(hexColor.slice(1, 3), 16) / 255; const g = parseInt(hexColor.slice(3, 5), 16) / 255; const b = parseInt(hexColor.slice(5, 7), 16) / 255; if (g > r && g > b) { return `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -1.2 2.4 -1.2 0 0`; } else if (b > r && b > g) { return `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -1.2 -1.2 2.4 0 0`; } else if (r > g && r > b) { return `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 2.4 -1.2 -1.2 0 0`; } else if (r < 0.25 && g < 0.25 && b < 0.25) { return `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 1 1 1 -1 0`; } else { return `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -1 -1 -1 1.5 0`; } }; const outlineColor = isLocked ? "border-transparent" : isInteracting ? "border-green-500" : isSelected ? "border-blue-500" : isHovered ? "border-red-500" : "border-transparent"; const isInteractiveType = source.type === "iframe" || source.type === "code" || source.type === "camera" || source.type === "capture" || source.type === "youtube" || source.type === "twitch"; const { wrapperFilter, contentClipPath, needsKeySvg, svgKeyMatrix } = useMemo(() => { const filterParts = []; if (filters && filters.hue > 0) filterParts.push(`hue-rotate(${filters.hue}deg)`); if (filters && filters.blur > 0) filterParts.push(`blur(${filters.blur}px)`); if (filters && filters.sharpness > 0) { filterParts.push(`contrast(${1 + filters.sharpness / 50})`); } const chromaKey = source.style.chromaKey; const colorKey = source.style.colorKey; const isHtmlSource = source.type === "iframe" || source.type === "youtube" || source.type === "code" || source.type === "twitch"; const chromaOn = chromaKey && chromaKey.enabled; const colorKeyOn = colorKey && colorKey.enabled; const hasKeyFilter = isHtmlSource && (chromaOn || colorKeyOn); let matrix = ""; if (hasKeyFilter) { filterParts.push(`url(#svg-chroma-filter-${source.id})`); const targetHex = chromaOn ? chromaKey.color : colorKey ? colorKey.color : "#00ff00"; matrix = getSvgKeyMatrix(targetHex); } let clipPathValue = "none"; if (filters) { switch (filters.shape) { case "circle": clipPathValue = "circle(50% at 50% 50%)"; break; case "oval": clipPathValue = "ellipse(50% 35% at 50% 50%)"; break; case "triangle": clipPathValue = "polygon(50% 0%, 0% 100%, 100% 100%)"; break; case "diamond": clipPathValue = "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"; break; case "star": clipPathValue = "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)"; break; case "hexagon": clipPathValue = "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)"; break; } } return { wrapperFilter: filterParts.join(" ") || "none", contentClipPath: clipPathValue, needsKeySvg: hasKeyFilter, svgKeyMatrix: matrix }; }, [filters, source.type, source.id, source.style.chromaKey, source.style.colorKey]); const [audioLevel, setAudioLevel] = useState(0); useEffect(() => { if (!filters?.audioReactEnabled) { setAudioLevel(0); return; } let stream = null; let audioCtx = null; let analyser = null; let animationId = null; const startMic = async () => { try { const AudioCtx = window.AudioContext || window.webkitAudioContext; stream = await navigator.mediaDevices.getUserMedia({ audio: true }); audioCtx = new AudioCtx(); analyser = audioCtx.createAnalyser(); analyser.fftSize = 64; const sourceNode = audioCtx.createMediaStreamSource(stream); sourceNode.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); const update = () => { if (!analyser) return; analyser.getByteFrequencyData(dataArray); let total = 0; for (let i = 0; i < dataArray.length; i++) { total += dataArray[i]; } const avg = total / dataArray.length; setAudioLevel(avg); animationId = requestAnimationFrame(update); }; update(); } catch (err) { console.warn("Error accessing mic for audio scale:", err); } }; startMic(); return () => { if (animationId) cancelAnimationFrame(animationId); if (stream) { stream.getTracks().forEach((track) => track.stop()); } if (audioCtx && audioCtx.state !== "closed") { audioCtx.close().catch(console.error); } }; }, [filters?.audioReactEnabled]); const audioSensitivity = filters?.audioSensitivity ?? 5; const normalizedLevel = audioLevel / 255; const scaleBoost = filters?.audioReactEnabled ? normalizedLevel * (audioSensitivity / 10) * 0.45 : 0; const finalScale = (scale || 1) + scaleBoost; let shakeX = 0; let shakeY = 0; if (filters?.audioReactEnabled && normalizedLevel > 0.05) { const shakeMax = normalizedLevel * audioSensitivity * 10; shakeX = (Math.random() - 0.5) * shakeMax; shakeY = (Math.random() - 0.5) * shakeMax; } const transformString = `scale(${finalScale}) rotate(${rotation || 0}deg) scale(${scaleX || 1}, ${scaleY || 1})`; const showOutline = !isLocked && (isSelected || isHovered || isInteracting); const isMoveTransitionOn = isTransitioning && currentPlayingTransition?.type === "move"; const customTransitionStyle = isMoveTransitionOn ? `left ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}, top ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}, width ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}, height ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}, transform ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}, opacity ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}` : "none"; return /* @__PURE__ */ jsxs(Fragment, { children: [ needsKeySvg && /* @__PURE__ */ jsx("svg", { style: { position: "absolute", width: 0, height: 0, pointerEvents: "none" }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("filter", { id: `svg-chroma-filter-${source.id}`, children: /* @__PURE__ */ jsx("feColorMatrix", { type: "matrix", values: svgKeyMatrix }) }) }) }), /* @__PURE__ */ jsxs( "div", { className: `absolute transition-colors duration-100 ease-in-out`, style: { left: x + shakeX, top: y + shakeY, width, height, zIndex, opacity, transform: transformString, transition: customTransitionStyle, boxShadow: boxShadow ? `0 0 ${shadowBlur || 15}px ${shadowColor || "rgba(0,0,0,0.5)"}` : "none", outline: stroke ? `${strokeWidth || 2}px solid ${strokeColor || "#ffffff"}` : "none", outlineOffset: "2px", cursor: isLocked || source.locked ? "default" : isInteractiveType && isSelected && isInteractable ? "default" : "grab", filter: wrapperFilter, pointerEvents: source.locked ? "none" : "auto" }, onMouseDown: (e) => { if (isInteractable) { return; } if ((!isInteractiveType || !isSelected) && !source.locked) { onMouseDown(e, "move"); } }, onTouchStart: (e) => { if (isInteractable) { return; } if ((!isInteractiveType || !isSelected) && !source.locked) { onTouchStart(e, "move"); } }, onMouseEnter, onMouseLeave, onContextMenu, children: [ /* @__PURE__ */ jsx( "div", { className: `w-full h-full relative overflow-hidden ${showOutline ? `${outlineColor} border-2` : ""}`, style: { backgroundColor: source.type === "color" ? source.content : "transparent", clipPath: contentClipPath }, children: /* @__PURE__ */ jsx(SourceContent, { source, isLocked, isInteractable, isSelected, onUpdate, framerateLimit }) } ), isHovered && isSelected && !isLocked && !source.locked && (source.type === "iframe" || source.type === "code" || source.type === "youtube" || source.type === "twitch") && /* @__PURE__ */ jsx("div", { className: "absolute top-2 left-1/2 transform -translate-x-1/2 z-[200]", children: /* @__PURE__ */ jsxs( "button", { onClick: (e) => { e.stopPropagation(); e.preventDefault(); setIsInteractable(!isInteractable); }, className: `px-3 py-1 text-xs font-semibold rounded-full shadow-lg border transition-all duration-200 cursor-pointer flex items-center gap-1.5 ${isInteractable ? "bg-green-600 border-green-700 text-white hover:bg-green-500 hover:scale-105" : "bg-black/75 border-white/20 text-white hover:bg-black/90 hover:scale-105"}`, title: isInteractable ? "Disable element interaction" : "Enable direct element interaction", children: [ /* @__PURE__ */ jsx("span", { children: isInteractable ? "\u{1F7E2}" : "\u{1F5B1}\uFE0F" }), /* @__PURE__ */ jsx("span", { children: isInteractable ? "Interact Active" : "Interact" }) ] } ) }), isHovered && !isLocked && !source.locked && source.type === "youtube" && /* @__PURE__ */ jsx("div", { className: "absolute bottom-2 left-1/2 transform -translate-x-1/2 z-[300] flex items-center gap-1.5 bg-zinc-950/90 backdrop-blur border border-zinc-700/80 px-2.5 py-1.5 rounded-full shadow-2xl transition-all duration-300 animate-slide-down origin-bottom", children: showYtInput ? /* @__PURE__ */ jsxs( "form", { onSubmit: (e) => { e.preventDefault(); e.stopPropagation(); let finalUrl = ytInputVal.trim(); if (finalUrl) { let videoId = ""; const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; const match = finalUrl.match(regExp); if (match && match[2].length === 11) { videoId = match[2]; finalUrl = `https://www.youtube.com/embed/${videoId}?enablejsapi=1`; } else if (finalUrl.length === 11 && !finalUrl.includes("http")) { videoId = finalUrl; finalUrl = `https://www.youtube.com/embed/${videoId}?enablejsapi=1`; } else if (!finalUrl.includes("enablejsapi=1")) { if (finalUrl.includes("?")) { finalUrl = `${finalUrl}&enablejsapi=1`; } else { finalUrl = `${finalUrl}?enablejsapi=1`; } } onUpdate({ content: finalUrl }); } setShowYtInput(false); }, className: "flex items-center gap-1 text-xs", onClick: (e) => e.stopPropagation(), children: [ /* @__PURE__ */ jsx( "input", { type: "text", value: ytInputVal, onChange: (e) => setYtInputVal(e.target.value), className: "px-2 py-1 text-[10px] w-48 bg-zinc-900 border border-zinc-700 rounded text-white focus:outline-none focus:ring-1 focus:ring-indigo-500", placeholder: "Paste YouTube Link or ID", autoFocus: true } ), /* @__PURE__ */ jsx( "button", { type: "submit", className: "px-2 py-1 bg-green-600 hover:bg-green-500 text-white rounded text-[10px] font-bold cursor-pointer transition-colors active:scale-95", children: "\u2713" } ), /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => setShowYtInput(false), className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] cursor-pointer transition-colors active:scale-95", children: "\u2715" } ) ] } ) : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "button", { onClick: (e) => { e.stopPropagation(); e.preventDefault(); const iframe = document.getElementById(`youtube-iframe-${source.id}`); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', "*"); } }, className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-green-600 hover:text-white text-zinc-300 rounded-full transition-colors text-xs cursor-pointer active:scale-95", title: "Play Video", children: "\u25B6\uFE0F" } ), /* @__PURE__ */ jsx( "button", { onClick: (e) => { e.stopPropagation(); e.preventDefault(); const iframe = document.getElementById(`youtube-iframe-${source.id}`); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', "*"); } }, className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-yellow-600 hover:text-white text-zinc-300 rounded-full transition-colors text-xs cursor-pointer active:scale-95", title: "Pause Video", children: "\u23F8\uFE0F" } ), /* @__PURE__ */ jsx("span", { className: "w-[1px] h-4 bg-zinc-800" }), /* @__PURE__ */ jsx( "button", { onClick: (e) => { e.stopPropagation(); e.preventDefault(); setShowYtInput(true); }, className: "px-2.5 py-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded-full text-[10px] font-bold transition-all flex items-center gap-1 cursor-pointer active:scale-95", title: "Add New YouTube Link", children: "\u{1F517} New Link" } ) ] }) }), !isLocked && isSelected && !source.locked && /* @__PURE__ */ jsx(Fragment, { children: isInteractiveType ? !isInteractable && /* @__PURE__ */ jsx(InteractionOverlay, { onMouseDown, onTouchStart, isMobileMode }) : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(ResizeHandle, { position: "top-0 left-0", cursor: "nwse-resize", onMouseDown: (e) => onMouseDown(e, "resize-tl"), onTouchStart: (e) => onTouchStart(e, "resize-tl"), isMobileMode }), /* @__PURE__ */ jsx(ResizeHandle, { position: "top-0 right-0", cursor: "nesw-resize", onMouseDown: (e) => onMouseDown(e, "resize-tr"), onTouchStart: (e) => onTouchStart(e, "resize-tr"), isMobileMode }), /* @__PURE__ */ jsx(ResizeHandle, { position: "bottom-0 left-0", cursor: "nesw-resize", onMouseDown: (e) => onMouseDown(e, "resize-bl"), onTouchStart: (e) => onTouchStart(e, "resize-bl"), isMobileMode }), /* @__PURE__ */ jsx(ResizeHandle, { position: "bottom-0 right-0", cursor: "nwse-resize", onMouseDown: (e) => onMouseDown(e, "resize-br"), onTouchStart: (e) => onTouchStart(e, "resize-br"), isMobileMode }) ] }) }) ] } ) ] }); }; var SourceRenderer_default = SourceRenderer; // ws:scratch/cc/components/Canvas.tsx import { jsx as jsx2, jsxs as jsxs2 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var GRID_SIZE = 20; var Grid = () => /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 w-full h-full", style: { backgroundSize: `${GRID_SIZE}px ${GRID_SIZE}px`, backgroundImage: ` linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px) `, zIndex: -1 } }); var Canvas = ({ scene, isLocked, selectedSourceId, onSelectSource, onUpdateSourceStyle, onUpdateSource, onInteractionEnd, isSnapToGridEnabled, canvasScale = 1, canvasWidth = 1920, canvasHeight = 1080, onSourceContextMenu, onCanvasContextMenu, isMobileMode = false, left, top, interfaceAnimations = "enabled", framerateLimit = "uncapped", isTransitioning = false, currentPlayingTransition = null }) => { const [hoveredSourceId, setHoveredSourceId] = useState2(null); const [interaction, setInteraction] = useState2({ type: "none", sourceId: null, startX: 0, startY: 0, startWidth: 0, startHeight: 0, startSourceX: 0, startSourceY: 0 }); const canvasRef = useRef2(null); const animationFrameRef = useRef2(null); const onInteractionEndRef = useRef2(onInteractionEnd); useEffect2(() => { onInteractionEndRef.current = onInteractionEnd; }, [onInteractionEnd]); const prevSceneRef = useRef2(void 0); const [currentSceneId, setCurrentSceneId] = useState2(scene?.id); const [moveStyles, setMoveStyles] = useState2({}); const [triggerMove, setTriggerMove] = useState2(false); const [outgoingSources, setOutgoingSources] = useState2([]); useEffect2(() => { if (!isTransitioning) { setOutgoingSources([]); } }, [isTransitioning]); if (scene && scene.id !== currentSceneId) { const prevScene = prevSceneRef.current; if (prevScene && currentPlayingTransition?.type === "move" && isTransitioning) { const initialOverrideStyles = {}; const claimedPrevIds = /* @__PURE__ */ new Set(); const unclaimedPrev = prevScene.sources.filter((prevSrc) => { const isMatched = scene.sources.some((newSrc) => { return prevSrc.id === newSrc.id || prevSrc.type === newSrc.type && prevSrc.content === newSrc.content && newSrc.content || prevSrc.name.trim().toLowerCase() === newSrc.name.trim().toLowerCase() && prevSrc.type === newSrc.type; }); return !isMatched; }); const outgoingWithExitTarget = unclaimedPrev.map((prevSrc) => { const w = prevSrc.style.width ?? 100; const h = prevSrc.style.height ?? 100; const cx = (prevSrc.style.x ?? 0) + w / 2; const cy = (prevSrc.style.y ?? 0) + h / 2; const dLeft = cx; const dRight = canvasWidth - cx; const dTop = cy; const dBottom = canvasHeight - cy; const minD = Math.min(dLeft, dRight, dTop, dBottom); let targetX = prevSrc.style.x ?? 0; let targetY = prevSrc.style.y ?? 0; if (minD === dLeft) { targetX = -w - 150; } else if (minD === dRight) { targetX = canvasWidth + 150; } else if (minD === dTop) { targetY = -h - 150; } else { targetY = canvasHeight + 150; } initialOverrideStyles[prevSrc.id] = { x: prevSrc.style.x, y: prevSrc.style.y, width: w, height: h, opacity: prevSrc.style.opacity ?? 1, scale: prevSrc.style.scale ?? 1, rotation: prevSrc.style.rotation ?? 0, scaleX: prevSrc.style.scaleX ?? 1, scaleY: prevSrc.style.scaleY ?? 1 }; return { ...prevSrc, style: { ...prevSrc.style, x: targetX, y: targetY, opacity: 0 } }; }); setOutgoingSources(outgoingWithExitTarget); scene.sources.forEach((newSrc) => { const match = prevScene.sources.find((prevSrc) => { if (claimedPrevIds.has(prevSrc.id)) return false; return prevSrc.id === newSrc.id || prevSrc.type === newSrc.type && prevSrc.content === newSrc.content && newSrc.content || prevSrc.name.trim().toLowerCase() === newSrc.name.trim().toLowerCase() && prevSrc.type === newSrc.type; }); if (match) { claimedPrevIds.add(match.id); initialOverrideStyles[newSrc.id] = { x: match.style.x, y: match.style.y, width: match.style.width, height: match.style.height, opacity: match.style.opacity, scale: match.style.scale, rotation: match.style.rotation, scaleX: match.style.scaleX, scaleY: match.style.scaleY }; } else { const w = newSrc.style.width ?? 100; const h = newSrc.style.height ?? 100; const cx = (newSrc.style.x ?? 0) + w / 2; const cy = (newSrc.style.y ?? 0) + h / 2; const dLeft = cx; const dRight = canvasWidth - cx; const dTop = cy; const dBottom = canvasHeight - cy; const minD = Math.min(dLeft, dRight, dTop, dBottom); let startX = newSrc.style.x ?? 0; let startY = newSrc.style.y ?? 0; if (minD === dLeft) { startX = -w - 150; } else if (minD === dRight) { startX = canvasWidth + 150; } else if (minD === dTop) { startY = -h - 150; } else { startY = canvasHeight + 150; } initialOverrideStyles[newSrc.id] = { x: startX, y: startY, width: w, height: h, opacity: 0, scale: newSrc.style.scale ?? 1, rotation: newSrc.style.rotation ?? 0, scaleX: newSrc.style.scaleX ?? 1, scaleY: newSrc.style.scaleY ?? 1 }; } }); if (Object.keys(initialOverrideStyles).length > 0) { setMoveStyles(initialOverrideStyles); setTriggerMove(true); } else { setMoveStyles({}); setTriggerMove(false); } } else { setMoveStyles({}); setTriggerMove(false); setOutgoingSources([]); } setCurrentSceneId(scene.id); } useEffect2(() => { prevSceneRef.current = scene; }, [scene]); useEffect2(() => { if (triggerMove) { const timer = setTimeout(() => { setMoveStyles({}); setTriggerMove(false); }, 50); return () => clearTimeout(timer); } }, [triggerMove]); const handleMouseDown = (e, source, interactionType) => { if (isLocked || source.locked) { onSelectSource(source.id); return; } e.preventDefault(); e.stopPropagation(); onSelectSource(source.id); if (interactionType === "none") { setInteraction({ type: "none", sourceId: source.id, startX: 0, startY: 0, startWidth: 0, startHeight: 0, startSourceX: 0, startSourceY: 0 }); return; } setInteraction({ type: interactionType, sourceId: source.id, startX: e.clientX, startY: e.clientY, startWidth: source.style.width, startHeight: source.style.height, startSourceX: source.style.x, startSourceY: source.style.y }); }; const handleTouchStart = (e, source, interactionType) => { if (isLocked || source.locked) { onSelectSource(source.id); return; } e.stopPropagation(); onSelectSource(source.id); if (interactionType === "none") { setInteraction({ type: "none", sourceId: source.id, startX: 0, startY: 0, startWidth: 0, startHeight: 0, startSourceX: 0, startSourceY: 0 }); return; } const touch = e.touches[0]; if (!touch) return; setInteraction({ type: interactionType, sourceId: source.id, startX: touch.clientX, startY: touch.clientY, startWidth: source.style.width, startHeight: source.style.height, startSourceX: source.style.x, startSourceY: source.style.y }); }; const handleMouseMove = (e) => { if (interaction.type === "none" || !interaction.sourceId) return; e.preventDefault(); if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); } animationFrameRef.current = requestAnimationFrame(() => { const dx = (e.clientX - interaction.startX) / canvasScale; const dy = (e.clientY - interaction.startY) / canvasScale; let updates = {}; const snap = (value) => isSnapToGridEnabled ? Math.round(value / GRID_SIZE) * GRID_SIZE : value; switch (interaction.type) { case "move": updates.x = snap(interaction.startSourceX + dx); updates.y = snap(interaction.startSourceY + dy); break; case "resize-br": updates.width = snap(Math.max(50, interaction.startWidth + dx)); updates.height = snap(Math.max(50, interaction.startHeight + dy)); break; case "resize-bl": updates.width = snap(Math.max(50, interaction.startWidth - dx)); updates.height = snap(Math.max(50, interaction.startHeight + dy)); updates.x = snap(interaction.startSourceX + dx); break; case "resize-tr": updates.width = snap(Math.max(50, interaction.startWidth + dx)); updates.height = snap(Math.max(50, interaction.startHeight - dy)); updates.y = snap(interaction.startSourceY + dy); break; case "resize-tl": updates.width = snap(Math.max(50, interaction.startWidth - dx)); updates.height = snap(Math.max(50, interaction.startHeight - dy)); updates.x = snap(interaction.startSourceX + dx); updates.y = snap(interaction.startSourceY + dy); break; } if (Object.keys(updates).length > 0) { onUpdateSourceStyle(interaction.sourceId, updates); } }); }; const handleTouchMove = (e) => { if (interaction.type === "none" || !interaction.sourceId) return; if (e.cancelable) { e.preventDefault(); } if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); } animationFrameRef.current = requestAnimationFrame(() => { const touch = e.touches[0]; if (!touch) return; const dx = (touch.clientX - interaction.startX) / canvasScale; const dy = (touch.clientY - interaction.startY) / canvasScale; let updates = {}; const snap = (value) => isSnapToGridEnabled ? Math.round(value / GRID_SIZE) * GRID_SIZE : value; switch (interaction.type) { case "move": updates.x = snap(interaction.startSourceX + dx); updates.y = snap(interaction.startSourceY + dy); break; case "resize-br": updates.width = snap(Math.max(50, interaction.startWidth + dx)); updates.height = snap(Math.max(50, interaction.startHeight + dy)); break; case "resize-bl": updates.width = snap(Math.max(50, interaction.startWidth - dx)); updates.height = snap(Math.max(50, interaction.startHeight + dy)); updates.x = snap(interaction.startSourceX + dx); break; case "resize-tr": updates.width = snap(Math.max(50, interaction.startWidth + dx)); updates.height = snap(Math.max(50, interaction.startHeight - dy)); updates.y = snap(interaction.startSourceY + dy); break; case "resize-tl": updates.width = snap(Math.max(50, interaction.startWidth - dx)); updates.height = snap(Math.max(50, interaction.startHeight - dy)); updates.x = snap(interaction.startSourceX + dx); updates.y = snap(interaction.startSourceY + dy); break; } if (Object.keys(updates).length > 0) { onUpdateSourceStyle(interaction.sourceId, updates); } }); }; const handleMouseUp = () => { if (interaction.type !== "none" && interaction.sourceId) { onInteractionEndRef.current(); } setInteraction({ type: "none", sourceId: null, startX: 0, startY: 0, startWidth: 0, startHeight: 0, startSourceX: 0, startSourceY: 0 }); if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); } }; const handleCanvasClick = (e) => { if (e.target === e.currentTarget || e.target === canvasRef.current) { onSelectSource(null); } }; useEffect2(() => { if (interaction.type !== "none" && interaction.sourceId) { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp, { once: true }); window.addEventListener("touchmove", handleTouchMove, { passive: false }); window.addEventListener("touchend", handleMouseUp, { once: true }); } return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("touchmove", handleTouchMove); window.removeEventListener("touchend", handleMouseUp); if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); } }; }, [interaction, isSnapToGridEnabled]); if (!scene) { return /* @__PURE__ */ jsx2("div", { className: "w-full h-full flex items-center justify-center", children: "Loading scene..." }); } const isInteracting = interaction.type !== "none"; const interactingSourceId = interaction.sourceId; const curLeft = left !== void 0 ? left : window.innerWidth / 2; const curTop = top !== void 0 ? top : window.innerHeight / 2; const halfW = canvasWidth * canvasScale / 2; const halfH = canvasHeight * canvasScale / 2; const xStart = curLeft - halfW; const xEnd = curLeft + halfW; const yStart = curTop - halfH; const yEnd = curTop + halfH; return /* @__PURE__ */ jsxs2( "div", { className: "absolute inset-0 overflow-hidden select-none bg-black flex items-center justify-center", onClick: handleCanvasClick, onContextMenu: (e) => { if (e.target === e.currentTarget || canvasRef.current && !canvasRef.current.contains(e.target)) { e.preventDefault(); if (onCanvasContextMenu) { onCanvasContextMenu(e); } } }, children: [ /* @__PURE__ */ jsx2( "div", { className: "absolute bg-black pointer-events-none z-30", style: { top: 0, bottom: 0, left: 0, width: Math.max(0, xStart) } } ), /* @__PURE__ */ jsx2( "div", { className: "absolute bg-black pointer-events-none z-30", style: { top: 0, bottom: 0, left: `${Math.max(0, xEnd)}px`, right: 0 } } ), /* @__PURE__ */ jsx2( "div", { className: "absolute bg-black pointer-events-none z-30", style: { top: 0, left: 0, right: 0, height: Math.max(0, yStart) } } ), /* @__PURE__ */ jsx2( "div", { className: "absolute bg-black pointer-events-none z-30", style: { top: `${Math.max(0, yEnd)}px`, bottom: 0, left: 0, right: 0 } } ), /* @__PURE__ */ jsxs2( "div", { ref: canvasRef, className: `absolute origin-center ${interfaceAnimations === "enabled" ? "transition-all duration-300" : "transition-none"}`, onContextMenu: (e) => { if (e.target === canvasRef.current) { e.preventDefault(); if (onCanvasContextMenu) { onCanvasContextMenu(e); } } }, style: { left: left !== void 0 ? `${left}px` : "50%", top: top !== void 0 ? `${top}px` : "50%", width: canvasWidth, height: canvasHeight, transform: `translate(-50%, -50%) scale(${canvasScale})`, backgroundColor: scene.backgroundColor || "#000000", boxShadow: isLocked ? "none" : "0 25px 50px -12px rgba(0, 0, 0, 0.75), 0 0 0 1px rgba(255, 255, 255, 0.1)", overflow: "hidden" }, children: [ isSnapToGridEnabled && /* @__PURE__ */ jsx2(Grid, {}), (() => { const usedKeys = /* @__PURE__ */ new Set(); const activeSources = scene.sources.filter((source) => source.visible); const allSourcesToRender = isTransitioning && currentPlayingTransition?.type === "move" ? [...activeSources, ...outgoingSources] : activeSources; return allSourcesToRender.sort((a, b) => a.style.zIndex - b.style.zIndex).map((source) => { const overrideStyle = moveStyles[source.id]; const renderSource = overrideStyle ? { ...source, style: { ...source.style, ...overrideStyle } } : source; let reactKey = `stable-source-${source.id}`; if (source.type === "video" || source.type === "image" || source.type === "iframe") { const contentStr = typeof source.content === "string" ? source.content.trim() : ""; const nameStr = source.name.trim().toLowerCase(); if (contentStr) { const candidate = `stable-media-${source.type}-${nameStr}-${contentStr}`; if (!usedKeys.has(candidate)) { reactKey = candidate; } } } usedKeys.add(reactKey); const isOutgoing = !scene.sources.some((s) => s.id === source.id); return /* @__PURE__ */ jsx2( SourceRenderer_default, { source: renderSource, isLocked: isLocked || isOutgoing, isSelected: !isOutgoing && selectedSourceId === source.id, isHovered: !isOutgoing && hoveredSourceId === source.id, isInteracting: !isOutgoing && isInteracting && interactingSourceId === source.id, interactionType: interaction.type, onMouseDown: (e, type) => !isOutgoing && handleMouseDown(e, source, type), onTouchStart: (e, type) => !isOutgoing && handleTouchStart(e, source, type), isMobileMode, onMouseEnter: () => !isOutgoing && !isInteracting && setHoveredSourceId(source.id), onMouseLeave: () => !isOutgoing && setHoveredSourceId(null), onUpdateStyle: (style) => !isOutgoing && onUpdateSourceStyle(source.id, style), onUpdate: (updates) => !isOutgoing && onUpdateSource(source.id, updates), onContextMenu: (e) => { if (!isOutgoing && onSourceContextMenu) { onSourceContextMenu(e, source.id); } }, framerateLimit, isTransitioning, currentPlayingTransition }, reactKey ); }); })() ] } ) ] } ); }; var Canvas_default = Canvas; // ws:scratch/cc/components/UI.tsx import { useState as useState8, useEffect as useEffect7, useRef as useRef8, useCallback as useCallback2 } from "https://esm.sh/react@19.2.0"; // ws:scratch/cc/components/SourceManager.tsx import React3, { useState as useState3, useRef as useRef3 } from "https://esm.sh/react@19.2.0"; import { jsx as jsx3, jsxs as jsxs3 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var sourceTypes = [ { type: "text", label: "Text", icon: "\u{1F4DD}" }, { type: "image", label: "Image", icon: "\u{1F5BC}\uFE0F" }, { type: "video", label: "Video", icon: "\u{1F4F9}" }, { type: "camera", label: "Camera", icon: "\u{1F4F8}" }, { type: "capture", label: "Capture", icon: "\u{1F5A5}\uFE0F" }, { type: "iframe", label: "Web", icon: "\u{1F310}" }, { type: "color", label: "Color", icon: "\u{1F3A8}" }, { type: "gallery", label: "Gallery", icon: "\u{1F39E}\uFE0F" }, { type: "code", label: "Code", icon: "\u{1F4BB}" }, { type: "paint", label: "Paint", icon: "\u{1F58C}\uFE0F" }, { type: "visualizer", label: "Visualizer", icon: "\u{1F4CA}" }, { type: "emoji", label: "Emoji", icon: "\u{1F600}" }, { type: "youtube", label: "YouTube", icon: "\u{1F4FA}" }, { type: "twitch", label: "Twitch", icon: "\u{1F3AE}" } ]; var GridIcon = () => /* @__PURE__ */ jsx3("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6a2 2 0 012-2h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM10 4v16m4-16v16M4 10h16M4 14h16" }) }); var CenterIcon = () => /* @__PURE__ */ jsx3("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-5 w-5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx3("path", { d: "M5 12h14M12 5l-7 7 7 7" }) }); var FitToScreenIcon = () => /* @__PURE__ */ jsx3("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-5 w-5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx3("path", { d: "M4 8V4h4M20 8V4h-4M4 16v4h4M20 16v4h-4" }) }); var SourceManager = (props) => { const { sources, onAddSource, onDeleteSource, onUpdateSourceLayer, onImportSources, onExportSources, onCenterSource, onFullscreenSource, selectedSourceId, onSelectSource, setActiveTab, isSnapToGridEnabled, onToggleSnapToGrid, theme, onOpenFilterPanel, onUndo, onRedo, canUndo, canRedo, onToggleSourceVisibility, onToggleSourceLock, onReorderSources, onSourceContextMenu, copiedSource, onCopySource, onPasteSource } = props; const dragItem = useRef3(null); const dragOverItem = useRef3(null); const [dropPosition, setDropPosition] = useState3(null); const [isCopiedIndicatorDismissed, setCopiedIndicatorDismissed] = useState3(false); React3.useEffect(() => { if (copiedSource) { setCopiedIndicatorDismissed(false); } else { setCopiedIndicatorDismissed(true); } }, [copiedSource?.id]); const sortedSources = [...sources].sort((a, b) => b.style.zIndex - a.style.zIndex); const zIndexMinMax = sources.reduce((acc, s) => ({ min: Math.min(acc.min, s.style.zIndex), max: Math.max(acc.max, s.style.zIndex) }), { min: Infinity, max: -Infinity }); const selectedSourceZIndex = sources.find((s) => s.id === selectedSourceId)?.style.zIndex; const handleDragStart = (id) => { dragItem.current = id; }; const handleDragEnter = (e, id) => { e.preventDefault(); dragOverItem.current = id; const rect = e.currentTarget.getBoundingClientRect(); const midpoint = rect.top + rect.height / 2; setDropPosition(e.clientY < midpoint ? "top" : "bottom"); }; const handleDragLeave = (e) => { if (e.relatedTarget !== e.currentTarget) { dragOverItem.current = null; setDropPosition(null); } }; const handleDrop = () => { if (dragItem.current && dragOverItem.current && dragItem.current !== dragOverItem.current) { const currentIds = sortedSources.map((s) => s.id); const dragItemIndex = currentIds.indexOf(dragItem.current); const dragOverItemIndex = currentIds.indexOf(dragOverItem.current); const newIds = [...currentIds]; const [draggedItem] = newIds.splice(dragItemIndex, 1); let insertIndex = dragOverItemIndex; if (dropPosition === "bottom") { insertIndex = dragOverItemIndex + 1; if (dragItemIndex < dragOverItemIndex) { insertIndex -= 1; } } else { if (dragItemIndex < dragOverItemIndex) { } else { insertIndex = dragOverItemIndex; } } newIds.splice(insertIndex, 0, draggedItem); onReorderSources(newIds); } dragItem.current = null; dragOverItem.current = null; setDropPosition(null); }; const themeClasses = themes[theme]; const buttonClass = `p-1.5 rounded ${themeClasses.button} disabled:opacity-30 disabled:cursor-not-allowed`; const iconButtonClass = `${buttonClass} flex items-center justify-center text-lg`; return /* @__PURE__ */ jsxs3("div", { children: [ /* @__PURE__ */ jsxs3("div", { className: `flex justify-between items-center p-3 border-b ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx3("h2", { className: "text-lg font-bold", children: "Sources" }), /* @__PURE__ */ jsxs3("div", { className: "flex items-center space-x-1", children: [ /* @__PURE__ */ jsx3("button", { onClick: onUndo, disabled: !canUndo, title: "Undo", className: buttonClass, children: "\u21A9\uFE0F" }), /* @__PURE__ */ jsx3("button", { onClick: onRedo, disabled: !canRedo, title: "Redo", className: buttonClass, children: "\u21AA\uFE0F" }), /* @__PURE__ */ jsx3( "button", { onClick: onToggleSnapToGrid, title: "Toggle Snap to Grid", className: `p-1 rounded ${isSnapToGridEnabled ? "bg-blue-600 text-white" : themeClasses.button}`, children: /* @__PURE__ */ jsx3(GridIcon, {}) } ) ] }) ] }), /* @__PURE__ */ jsx3("div", { className: `p-2 border-b ${themeClasses.border} grid grid-cols-5 gap-1`, children: sourceTypes.map(({ type, label, icon }) => /* @__PURE__ */ jsxs3( "button", { onClick: () => onAddSource(type), title: `Add ${label} Source`, className: `flex flex-col items-center justify-center p-1 rounded text-xs ${themeClasses.button} ${themeClasses.itemHover}`, children: [ /* @__PURE__ */ jsx3("span", { className: "text-lg", children: icon }), /* @__PURE__ */ jsx3("span", { className: "mt-1 text-[10px]", children: label }) ] }, type )) }), copiedSource && !isCopiedIndicatorDismissed && /* @__PURE__ */ jsxs3("div", { className: `p-2 px-3 border-b ${themeClasses.border} flex items-center justify-between text-xs font-sans ${theme === "dark" ? "bg-emerald-950/20 text-emerald-300 border-emerald-900/40" : "bg-emerald-50 text-emerald-800 border-emerald-200"} font-medium`, children: [ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1.5 truncate mr-2 flex-1", children: [ /* @__PURE__ */ jsx3("span", { className: "w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse shrink-0" }), /* @__PURE__ */ jsx3("span", { className: "shrink-0 font-semibold text-emerald-600", children: "Can Paste:" }), /* @__PURE__ */ jsx3("span", { className: "truncate underline font-mono text-[10.5px] max-w-[120px] bg-emerald-500/10 px-1.5 py-0.5 rounded text-emerald-300", children: copiedSource.name }) ] }), /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 shrink-0", children: [ /* @__PURE__ */ jsx3( "button", { onClick: onPasteSource, className: "px-2.5 py-0.5 rounded text-[10.5px] font-bold bg-emerald-600 hover:bg-emerald-500 text-white shadow-sm hover:scale-[1.03] transition-all cursor-pointer", title: "Paste Into Active Scene", children: "Paste" } ), /* @__PURE__ */ jsx3( "button", { onClick: () => setCopiedIndicatorDismissed(true), className: "p-1 rounded text-red-500 hover:text-red-400 hover:bg-red-500/10 font-bold leading-none cursor-pointer text-sm", title: "Dismiss Clipboard", children: "\xD7" } ) ] }) ] }), /* @__PURE__ */ jsx3("div", { className: `border-b ${themeClasses.border}`, onDrop: handleDrop, onDragOver: (e) => e.preventDefault(), children: sources.length > 0 ? /* @__PURE__ */ jsx3("ul", { children: sortedSources.map((source) => /* @__PURE__ */ jsxs3( "li", { onClick: () => onSelectSource(source.id), onContextMenu: (e) => { if (onSourceContextMenu) { onSourceContextMenu(e, source.id); } }, className: `relative group flex items-center justify-between px-3 py-2 cursor-pointer transition-colors border-l-4 ${!source.visible ? "opacity-50" : ""} ${source.locked ? "bg-red-500/10" : ""} ${selectedSourceId === source.id ? `bg-blue-500/30 border-blue-500 ${themeClasses.text}` : `border-transparent ${themeClasses.itemHover}`}`, draggable: true, onDragStart: () => handleDragStart(source.id), onDragEnter: (e) => handleDragEnter(e, source.id), onDragLeave: handleDragLeave, onDragEnd: handleDrop, children: [ dragOverItem.current === source.id && dropPosition === "top" && /* @__PURE__ */ jsx3("div", { className: "absolute top-0 left-0 right-0 h-0.5 bg-blue-500 z-10" }), /* @__PURE__ */ jsxs3("div", { className: "flex items-center truncate", children: [ /* @__PURE__ */ jsx3( "button", { onClick: (e) => { e.stopPropagation(); onToggleSourceLock(source.id); }, title: source.locked ? "Unlock Source" : "Lock Source", className: `mr-2 p-1 rounded-full ${themeClasses.icon}`, children: source.locked ? "\u{1F512}" : "\u{1F513}" } ), /* @__PURE__ */ jsx3( "button", { onClick: (e) => { e.stopPropagation(); onToggleSourceVisibility(source.id); }, title: source.visible ? "Hide Source" : "Show Source", className: `mr-2 p-1 rounded-full ${themeClasses.icon}`, children: source.visible ? "\u{1F441}\uFE0F" : "\u{1F648}" } ), /* @__PURE__ */ jsx3("span", { className: "truncate text-sm font-medium", children: source.name }) ] }), /* @__PURE__ */ jsxs3("div", { className: "flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: [ /* @__PURE__ */ jsx3( "button", { onClick: (e) => { e.stopPropagation(); onCopySource(source.id); }, title: "Copy Source Configuration", className: `p-1 rounded ${themeClasses.icon}`, children: "\u{1F4CB}" } ), /* @__PURE__ */ jsx3( "button", { onClick: (e) => { e.stopPropagation(); onSelectSource(source.id); setActiveTab("settings"); }, title: "Settings", className: `p-1 rounded ${themeClasses.icon}`, children: "\u2699\uFE0F" } ), /* @__PURE__ */ jsx3( "button", { onClick: (e) => { e.stopPropagation(); onOpenFilterPanel(source.id); }, title: "Filters", className: `p-1 rounded ${themeClasses.icon}`, children: "\u{1F58C}\uFE0F" } ), /* @__PURE__ */ jsx3( "button", { onClick: (e) => { e.stopPropagation(); onDeleteSource(source.id); }, title: "Delete Source", className: "ml-2 text-red-500 hover:text-red-400 text-lg", children: "\xD7" } ) ] }), dragOverItem.current === source.id && dropPosition === "bottom" && /* @__PURE__ */ jsx3("div", { className: "absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 z-10" }) ] }, source.id )) }) : /* @__PURE__ */ jsx3("p", { className: `text-sm p-3 text-center ${themeClasses.text} opacity-50`, children: "No sources in this scene." }) }), /* @__PURE__ */ jsxs3("div", { className: `p-2 flex items-center justify-between`, children: [ /* @__PURE__ */ jsxs3("div", { className: "flex items-center space-x-1", children: [ /* @__PURE__ */ jsx3( "button", { onClick: () => selectedSourceId && onDeleteSource(selectedSourceId), disabled: !selectedSourceId, className: "p-1.5 rounded bg-gray-700 hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center text-lg", title: "Delete Source", children: "\u{1F5D1}\uFE0F" } ), /* @__PURE__ */ jsx3( "button", { onClick: () => selectedSourceId && onCopySource(selectedSourceId), disabled: !selectedSourceId, className: "px-2 py-1.5 rounded bg-gray-700 hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center text-[11px] font-bold text-zinc-100", title: "Copy Selected", children: "\u{1F4CB} Copy" } ), copiedSource && /* @__PURE__ */ jsx3( "button", { onClick: onPasteSource, className: "px-2 py-1.5 rounded bg-emerald-700 hover:bg-emerald-600 flex items-center justify-center text-[11px] font-bold text-white animate-pulse", title: "Paste Clipboard Source", children: "\u{1F4CB} Paste" } ) ] }), /* @__PURE__ */ jsxs3("div", { className: "flex items-center space-x-1", children: [ /* @__PURE__ */ jsx3("button", { onClick: () => selectedSourceId && onCenterSource(selectedSourceId), disabled: !selectedSourceId, className: buttonClass, title: "Center Source", children: /* @__PURE__ */ jsx3(CenterIcon, {}) }), /* @__PURE__ */ jsx3("button", { onClick: () => selectedSourceId && onFullscreenSource(selectedSourceId), disabled: !selectedSourceId, className: buttonClass, title: "Fit to Screen", children: /* @__PURE__ */ jsx3(FitToScreenIcon, {}) }), /* @__PURE__ */ jsx3("button", { onClick: () => selectedSourceId && onUpdateSourceLayer(selectedSourceId, "up"), disabled: !selectedSourceId || selectedSourceZIndex === zIndexMinMax.max, className: iconButtonClass, title: "Move Up", children: "\u2191" }), /* @__PURE__ */ jsx3("button", { onClick: () => selectedSourceId && onUpdateSourceLayer(selectedSourceId, "down"), disabled: !selectedSourceId || selectedSourceZIndex === zIndexMinMax.min, className: iconButtonClass, title: "Move Down", children: "\u2193" }) ] }) ] }) ] }); }; var SourceManager_default = SourceManager; // ws:scratch/cc/components/SceneManager.tsx import { useRef as useRef4, useState as useState4, useEffect as useEffect3 } from "https://esm.sh/react@19.2.0"; import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var TRANSITION_EMOJIS = { cut: "\u2702\uFE0F", fade: "\u{1F32B}\uFE0F", move: "\u27A1\uFE0F", stinger: "\u{1F3AC}" }; var SceneManager = ({ scenes, activeSceneId, onSelectScene, onAddScene, onDeleteScene, onImportProject, onExportProject, theme, sceneTransition, onSetSceneTransition, onOpenEditSceneModal, onDuplicateScene }) => { const themeClasses = themes[theme]; const stingerFileInputRef = useRef4(null); const [isTransitionSettingsOpen, setIsTransitionSettingsOpen] = useState4(false); const [position, setPosition] = useState4({ x: 0, y: 0 }); const modalRef = useRef4(null); const dragOffset = useRef4({ x: 0, y: 0 }); useEffect3(() => { if (isTransitionSettingsOpen) { const x = Math.max(20, (window.innerWidth - 384) / 2); const y = Math.max(20, (window.innerHeight - 450) / 2); setPosition({ x, y }); } }, [isTransitionSettingsOpen]); const handleMouseDown = (e) => { if (e.target.closest("button, select, input")) return; if (modalRef.current) { const rect = modalRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const handleMouseMove = (moveEvent) => { const newX = Math.max(0, Math.min(window.innerWidth - 180, moveEvent.clientX - dragOffset.current.x)); const newY = Math.max(0, Math.min(window.innerHeight - 100, moveEvent.clientY - dragOffset.current.y)); setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } }; const handleTransitionChange = (type) => { onSetSceneTransition({ ...sceneTransition, type }); }; const handleStingerFileChange = (event) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target?.result; const newFileObj = { name: file.name, value: dataUrl }; const currentFiles = sceneTransition.stingerFiles ? [...sceneTransition.stingerFiles] : []; const updatedFiles = [...currentFiles, newFileObj]; const newIndex = updatedFiles.length - 1; onSetSceneTransition({ ...sceneTransition, stingerFile: dataUrl, stingerFiles: updatedFiles, stingerFileIndex: newIndex }); }; reader.readAsDataURL(file); } }; const transitionTypes = [ { id: "cut", label: "Cut" }, { id: "fade", label: "Fade" }, { id: "move", label: "Move" }, { id: "stinger", label: "Stinger" } ]; return /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsxs4("div", { className: `flex justify-between items-center p-3 border-b ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx4("h2", { className: "text-lg font-bold", children: "Scenes" }), /* @__PURE__ */ jsx4("div", { className: "flex items-center space-x-2", children: /* @__PURE__ */ jsx4("button", { onClick: onAddScene, title: "Add Scene", className: "bg-blue-600 hover:bg-blue-500 text-white font-bold w-6 h-6 rounded-full flex items-center justify-center text-lg active:scale-95 cursor-pointer", children: "+" }) }) ] }), /* @__PURE__ */ jsx4("div", { className: `border-b ${themeClasses.border}`, children: scenes.length > 0 ? /* @__PURE__ */ jsx4("ul", { children: scenes.map((scene) => /* @__PURE__ */ jsxs4( "li", { onClick: () => onSelectScene(scene.id), className: `relative group flex items-center justify-between px-3 py-2 cursor-pointer transition-colors border-l-4 ${activeSceneId === scene.id ? `bg-blue-500/30 border-blue-500 ${themeClasses.text}` : `border-transparent ${themeClasses.itemHover}`}`, children: [ /* @__PURE__ */ jsxs4("div", { className: "flex items-center truncate", children: [ /* @__PURE__ */ jsx4("span", { className: "text-2xl mr-3 w-8 text-center", children: scene.icon || "\u{1F3AC}" }), /* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-medium", children: scene.name }) ] }), /* @__PURE__ */ jsxs4("div", { className: "flex items-center opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0", children: [ scene.transition && /* @__PURE__ */ jsx4( "span", { className: "text-xs mr-1 bg-zinc-800/80 border border-zinc-700 font-bold px-1 rounded", title: `Scene-specific Transition Override Set: ${scene.transition.type}`, children: TRANSITION_EMOJIS[scene.transition.type] || "\u2728" } ), /* @__PURE__ */ jsx4( "button", { onClick: (e) => { e.stopPropagation(); onOpenEditSceneModal(scene.id); }, title: `Edit ${scene.name}`, className: `p-1 rounded ${themeClasses.icon}`, children: "\u2699\uFE0F" } ), /* @__PURE__ */ jsx4( "button", { onClick: (e) => { e.stopPropagation(); onDuplicateScene(scene.id); }, title: `Duplicate layout of ${scene.name}`, className: `p-1 rounded ${themeClasses.icon} ml-1`, children: "\u{1F46F}" } ), scenes.length > 1 && /* @__PURE__ */ jsx4( "button", { onClick: (e) => { e.stopPropagation(); onDeleteScene(scene.id); }, title: `Delete ${scene.name}`, className: "ml-2 text-red-500 hover:text-red-400 text-lg", children: "\xD7" } ) ] }) ] }, scene.id )) }) : /* @__PURE__ */ jsx4("p", { className: `text-sm p-3 text-center ${themeClasses.text} opacity-50`, children: "No scenes available." }) }), /* @__PURE__ */ jsxs4("div", { className: `p-3 flex flex-col border-b ${themeClasses.border} pb-3`, children: [ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between mb-2", children: [ /* @__PURE__ */ jsx4("label", { className: `text-xs font-bold ${themeClasses.label} uppercase`, children: "Scene Transitions" }), /* @__PURE__ */ jsx4( "button", { type: "button", onClick: () => setIsTransitionSettingsOpen(true), className: "text-[10px] text-blue-400 hover:text-blue-300 font-bold flex items-center gap-1 bg-transparent border-none cursor-pointer", children: "\u2699\uFE0F Customize" } ) ] }), /* @__PURE__ */ jsxs4("div", { className: "flex items-center space-x-1.5 flex-wrap gap-y-1.5", children: [ transitionTypes.map((t) => /* @__PURE__ */ jsxs4( "button", { onClick: () => handleTransitionChange(t.id), className: `px-2.5 py-1 text-[10.5px] font-semibold rounded-full active:scale-95 transition-all text-center flex items-center gap-1 cursor-pointer ${sceneTransition.type === t.id ? "bg-blue-600 text-white shadow-md" : themeClasses.buttonSecondary}`, children: [ /* @__PURE__ */ jsx4("span", { children: TRANSITION_EMOJIS[t.id] }), /* @__PURE__ */ jsx4("span", { children: t.label }) ] }, t.id )), sceneTransition.type === "move" && /* @__PURE__ */ jsxs4("div", { className: "w-full mt-2.5 p-2 rounded bg-zinc-800/40 border border-zinc-700/20 text-left", children: [ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between mb-1", children: [ /* @__PURE__ */ jsx4("span", { className: "text-[9.5px] text-zinc-400 font-bold uppercase", children: "Move Speed / Duration" }), /* @__PURE__ */ jsxs4("span", { className: "text-[10.5px] font-mono font-bold text-sky-400", children: [ sceneTransition.speed ?? 500, "ms (", ((sceneTransition.speed ?? 500) / 1e3).toFixed(1), "s)" ] }) ] }), /* @__PURE__ */ jsx4( "input", { type: "range", min: "50", max: "30000", step: "50", value: sceneTransition.speed ?? 500, onChange: (e) => onSetSceneTransition({ ...sceneTransition, speed: parseInt(e.target.value, 10) }), className: "w-full h-1 bg-zinc-700/80 rounded-lg appearance-none cursor-pointer accent-blue-500" } ) ] }), sceneTransition.type === "stinger" && /* @__PURE__ */ jsxs4("div", { className: "w-full mt-2.5 flex flex-col gap-1.5 text-left", children: [ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 flex-wrap", children: [ /* @__PURE__ */ jsx4( "button", { onClick: () => stingerFileInputRef.current?.click(), title: "Upload Stinger Video", className: "p-1 px-[7px] py-[6px] rounded-full bg-green-600 hover:bg-green-500 text-white transition cursor-pointer flex items-center justify-center font-bold text-xs", children: "\u{1F4BD}" } ), /* @__PURE__ */ jsx4("input", { type: "file", ref: stingerFileInputRef, onChange: handleStingerFileChange, accept: "video/*", className: "hidden" }), (() => { const list = sceneTransition.stingerFiles || (sceneTransition.stingerFile ? [{ name: "StingerVideo.mp4", value: sceneTransition.stingerFile }] : []); return list.map((file, idx) => { const isActive = sceneTransition.stingerFile === file.value; return /* @__PURE__ */ jsxs4("div", { className: "relative group/stinger flex items-center", children: [ /* @__PURE__ */ jsx4( "button", { onClick: () => { onSetSceneTransition({ ...sceneTransition, stingerFile: file.value, stingerFileIndex: idx }); }, className: `w-7 h-7 rounded-full flex items-center justify-center transition-all cursor-pointer ${isActive ? "bg-blue-650 text-white scale-110 ring-2 ring-blue-400 rotate-12 shadow-sm" : "bg-zinc-800 border border-zinc-700 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700 hover:scale-105"}`, children: /* @__PURE__ */ jsx4("span", { className: "text-sm select-none", children: "\u{1F4BF}" }) } ), /* @__PURE__ */ jsxs4("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2.5 hidden group-hover/stinger:flex flex-col items-center animate-bounce z-50", children: [ /* @__PURE__ */ jsxs4("div", { className: "bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1 shadow-2xl text-[9px] text-zinc-100 font-sans whitespace-nowrap", children: [ /* @__PURE__ */ jsx4("span", { className: "font-semibold text-emerald-400", children: isActive ? "\u2713 Active: " : "Click to Load: " }), file.name ] }), /* @__PURE__ */ jsx4("div", { className: "w-1.5 h-1.5 bg-zinc-950 border-r border-b border-zinc-800 rotate-45 -mt-1" }) ] }) ] }, idx); }); })() ] }), (() => { const list = sceneTransition.stingerFiles || (sceneTransition.stingerFile ? [{ name: "StingerVideo.mp4", value: sceneTransition.stingerFile }] : []); if (list.length > 0) { const currentActiveName = list[sceneTransition.stingerFileIndex ?? 0]?.name || "StingerLoaded.mp4"; return /* @__PURE__ */ jsxs4("span", { className: "text-[9px] text-zinc-450 italic truncate max-w-[200px]", title: currentActiveName, children: [ "Active: ", currentActiveName ] }); } return /* @__PURE__ */ jsx4("span", { className: "text-[9px] text-zinc-550 italic", children: "No files loaded yet" }); })() ] }) ] }) ] }), isTransitionSettingsOpen && /* @__PURE__ */ jsxs4(Fragment2, { children: [ /* @__PURE__ */ jsx4("div", { className: "fixed inset-0 bg-black/80 z-[190000] pointer-events-auto cursor-default", onClick: () => setIsTransitionSettingsOpen(false) }), /* @__PURE__ */ jsxs4( "div", { ref: modalRef, style: { position: "fixed", left: `${position.x}px`, top: `${position.y}px`, zIndex: 2e5 }, className: `rounded-lg shadow-2xl max-w-sm w-full p-5 border text-white pointer-events-auto ${themeClasses.bg} ${themeClasses.border}`, children: [ /* @__PURE__ */ jsxs4( "div", { onMouseDown: handleMouseDown, className: "cursor-move select-none flex justify-between items-center mb-4 border-b pb-2 border-zinc-800", children: [ /* @__PURE__ */ jsx4("h3", { className: "text-xs font-bold uppercase tracking-wider flex items-center gap-1", children: "\u2699\uFE0F Transition Settings Adjustment" }), /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx4("span", { className: "text-[9px] text-zinc-500 font-mono", children: "\u2725 Drag" }), /* @__PURE__ */ jsx4("button", { onClick: () => setIsTransitionSettingsOpen(false), className: "text-zinc-400 hover:text-white font-bold text-lg cursor-pointer", children: "\xD7" }) ] }) ] } ), /* @__PURE__ */ jsxs4("div", { className: "space-y-4 text-xs", children: [ /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 uppercase text-zinc-400 text-[10px]", children: "Transition Type" }), /* @__PURE__ */ jsx4("div", { className: "grid grid-cols-4 gap-1.5", children: ["cut", "fade", "move", "stinger"].map((type) => /* @__PURE__ */ jsxs4( "button", { type: "button", onClick: () => onSetSceneTransition({ ...sceneTransition, type }), className: `py-1 px-1.5 rounded border text-[10px] font-bold text-center transition cursor-pointer active:scale-95 ${sceneTransition.type === type ? "bg-blue-600 border-blue-500 text-white shadow" : `${themeClasses.buttonSecondary}`}`, children: [ TRANSITION_EMOJIS[type], /* @__PURE__ */ jsx4("span", { className: "block text-[8px] uppercase", children: type }) ] }, type )) }) ] }), /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsxs4("label", { className: "block font-semibold mb-1 uppercase text-zinc-400 text-[10px]", children: [ "Duration (Speed): ", sceneTransition.speed ?? 500, "ms (", ((sceneTransition.speed ?? 500) / 1e3).toFixed(1), "s)" ] }), /* @__PURE__ */ jsx4( "input", { type: "range", min: "50", max: "30000", step: "50", value: sceneTransition.speed ?? 500, onChange: (e) => onSetSceneTransition({ ...sceneTransition, speed: parseInt(e.target.value, 10) }), className: "w-full h-2 rounded bg-zinc-800 appearance-none cursor-pointer" } ) ] }), /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 uppercase text-zinc-400 text-[10px]", children: "Motion Easing Style" }), /* @__PURE__ */ jsxs4( "select", { value: sceneTransition.style || "ease-in-out", onChange: (e) => onSetSceneTransition({ ...sceneTransition, style: e.target.value }), className: "w-full bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 text-zinc-300", children: [ /* @__PURE__ */ jsx4("option", { value: "ease", children: "Ease" }), /* @__PURE__ */ jsx4("option", { value: "linear", children: "Linear" }), /* @__PURE__ */ jsx4("option", { value: "ease-in", children: "Ease In" }), /* @__PURE__ */ jsx4("option", { value: "ease-out", children: "Ease Out" }), /* @__PURE__ */ jsx4("option", { value: "ease-in-out", children: "Ease In-Out" }) ] } ) ] }), /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 uppercase text-zinc-400 text-[10px]", children: "Transition Accent Color" }), /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 items-center", children: [ /* @__PURE__ */ jsx4( "input", { type: "color", value: sceneTransition.color || "#000000", onChange: (e) => onSetSceneTransition({ ...sceneTransition, color: e.target.value }), className: "w-9 h-8 rounded border-none bg-transparent cursor-pointer" } ), /* @__PURE__ */ jsx4( "input", { type: "text", value: sceneTransition.color || "#000000", onChange: (e) => onSetSceneTransition({ ...sceneTransition, color: e.target.value }), className: "flex-1 bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1 text-zinc-300 text-[11px] font-mono" } ) ] }) ] }), sceneTransition.type === "stinger" && /* @__PURE__ */ jsxs4("div", { className: "border-t border-zinc-800 pt-3 space-y-3", children: [ /* @__PURE__ */ jsx4("h4", { className: "font-bold text-yellow-500 uppercase tracking-widest text-[9px]", children: "\u{1F39E}\uFE0F Stinger Clip Adjustments" }), /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 text-zinc-400 text-[10px]", children: "Stinger File" }), /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx4( "button", { type: "button", onClick: () => stingerFileInputRef.current?.click(), className: "py-1 px-2 text-[10px] bg-emerald-600 hover:bg-emerald-500 text-white rounded font-bold cursor-pointer active:scale-95", children: "\u{1F4C2} Select Video File" } ), /* @__PURE__ */ jsx4("input", { type: "file", ref: stingerFileInputRef, onChange: handleStingerFileChange, accept: "video/*", className: "hidden" }), sceneTransition.stingerFile ? /* @__PURE__ */ jsx4("span", { className: "text-zinc-500 truncate text-[10px] max-w-[200px]", children: "Active Custom Clip Loaded" }) : /* @__PURE__ */ jsx4("span", { className: "text-zinc-650 italic text-[10px]", children: "No file loaded yet" }) ] }) ] }), /* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-2 gap-2", children: [ /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 text-zinc-400 text-[10px]", children: "Clipping Start (sec)" }), /* @__PURE__ */ jsx4( "input", { type: "number", min: "0", step: "0.1", value: sceneTransition.stingerStart || 0, onChange: (e) => onSetSceneTransition({ ...sceneTransition, stingerStart: parseFloat(e.target.value) || 0 }), className: "w-full bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-white text-[11px]" } ) ] }), /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 text-zinc-400 text-[10px]", children: "Clipping Stop (sec)" }), /* @__PURE__ */ jsx4( "input", { type: "number", min: "0", step: "0.1", value: sceneTransition.stingerStop || 0, placeholder: "End of Clip", onChange: (e) => onSetSceneTransition({ ...sceneTransition, stingerStop: parseFloat(e.target.value) || void 0 }), className: "w-full bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-white text-[11px]" } ) ] }) ] }), /* @__PURE__ */ jsxs4("div", { children: [ /* @__PURE__ */ jsx4("label", { className: "block font-semibold mb-1 text-zinc-400 text-[10px]", children: "Playback velocity multiplier" }), /* @__PURE__ */ jsxs4( "select", { value: sceneTransition.stingerSpeed || 1, onChange: (e) => onSetSceneTransition({ ...sceneTransition, stingerSpeed: parseFloat(e.target.value) }), className: "w-full bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1 text-zinc-300 text-[11px]", children: [ /* @__PURE__ */ jsx4("option", { value: "0.25", children: "0.25x SlowMo" }), /* @__PURE__ */ jsx4("option", { value: "0.5", children: "0.5x Half Speed" }), /* @__PURE__ */ jsx4("option", { value: "0.75", children: "0.75x Speed" }), /* @__PURE__ */ jsx4("option", { value: "1.0", children: "1.0x Normal Velocity" }), /* @__PURE__ */ jsx4("option", { value: "1.25", children: "1.25x Speed" }), /* @__PURE__ */ jsx4("option", { value: "1.5", children: "1.5x Fast" }), /* @__PURE__ */ jsx4("option", { value: "2.0", children: "2.0x Double speed" }), /* @__PURE__ */ jsx4("option", { value: "3.0", children: "3.0x Hyper speed" }) ] } ) ] }) ] }) ] }), /* @__PURE__ */ jsx4("div", { className: "mt-5 flex justify-end", children: /* @__PURE__ */ jsx4( "button", { type: "button", onClick: () => setIsTransitionSettingsOpen(false), className: "px-4 py-2 text-xs rounded font-bold bg-blue-600 hover:bg-blue-500 text-white cursor-pointer active:scale-95", children: "Apply & Save" } ) }) ] } ) ] }) ] }); }; var SceneManager_default = SceneManager; // ws:scratch/cc/components/SettingsPanel.tsx import { useState as useState5, useRef as useRef5, useEffect as useEffect4 } from "https://esm.sh/react@19.2.0"; import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var SettingsInput = ({ label, children, className, theme }) => { const themeClasses = themes[theme]; return /* @__PURE__ */ jsxs5("div", { className: `mb-3 ${className}`, children: [ /* @__PURE__ */ jsx5("label", { className: `block text-xs font-bold mb-1 uppercase ${themeClasses.label}`, children: label }), children ] }); }; var textStyleOptions = [ { id: "none", name: "Normal / None" }, { id: "boldSerif", name: "\u{1D41B}\u{1D428}\u{1D425}\u{1D41D} \u{1D42C}\u{1D41E}\u{1D42B}\u{1D422}\u{1D41F} (Bold Serif)" }, { id: "boldSans", name: "\u{1D5EF}\u{1D5FC}\u{1D5F9}\u{1D5F1} \u{1D600}\u{1D5EE}\u{1D5FB}\u{1D600} (Bold Sans)" }, { id: "italic", name: "\u{1D456}\u{1D461}\u{1D44E}\u{1D459}\u{1D456}\u{1D450} (Italic)" }, { id: "boldItalic", name: "\u{1D483}\u{1D490}\u{1D48D}\u{1D485} \u{1D48A}\u{1D495}\u{1D482}\u{1D48D}\u{1D48A}\u{1D484} (Bold Italic)" }, { id: "cursiveScript", name: "\u{1D4B8}\u{1D4CA}\u{1D4C7}\u{1D4C8}\u{1D4BE}\u{1D4CB}\u212F \u{1D4C8}\u{1D4B8}\u{1D4C7}\u{1D4BE}\u{1D4C5}\u{1D4C9} (Cursive Script)" }, { id: "boldCursive", name: "\u{1D4EB}\u{1D4F8}\u{1D4F5}\u{1D4ED} \u{1D4EC}\u{1D4FE}\u{1D4FB}\u{1D4FC}\u{1D4F2}\u{1D4FF}\u{1D4EE} (Bold Cursive)" }, { id: "frakturGothic", name: "\u{1D523}\u{1D52F}\u{1D51E}\u{1D528}\u{1D531}\u{1D532}\u{1D52F} \u{1D524}\u{1D52C}\u{1D531}\u{1D525}\u{1D526}\u{1D520} (Fraktur Gothic)" }, { id: "boldGothic", name: "\u{1D587}\u{1D594}\u{1D591}\u{1D589} \u{1D58C}\u{1D594}\u{1D599}\u{1D58D}\u{1D58E}\u{1D588} (Bold Gothic)" }, { id: "doubleStruck", name: "\u{1D555}\u{1D560}\u{1D566}\u{1D553}\u{1D55D}\u{1D556} \u{1D564}\u{1D565}\u{1D563}\u{1D566}\u{1D554}\u{1D55C} (Outline)" }, { id: "monospaceTypewriter", name: "\u{1D696}\u{1D698}\u{1D697}\u{1D698}\u{1D69C}\u{1D699}\u{1D68A}\u{1D68C}\u{1D68E} (Typewriter)" }, { id: "smallCaps", name: "s\u1D0D\u1D00\u029F\u029F \u1D04\u1D00\u1D18s (Small Caps)" }, { id: "fullwidth", name: "\uFF46\uFF55\uFF4C\uFF4C\uFF57\uFF49\uFF44\uFF54\uFF48 (Fullwidth)" }, { id: "squared", name: "\u{1F142}\u{1F140}\u{1F144}\u{1F130}\u{1F141}\u{1F134}\u{1F133} (Squared)" }, { id: "upsideDown", name: "Un\u028Dop \u01DDp\u1D09sd\u2229 (Upside Down)" }, { id: "boldFraktur", name: "\u{1D587}\u{1D594}\u{1D591}\u{1D589} \u{1D58B}\u{1D597}\u{1D586}\u{1D590}\u{1D599}\u{1D59A}\u{1D597} (Bold Fraktur)" }, { id: "scriptElegant", name: "\u{1D4C8}\u{1D4B8}\u{1D4C7}\u{1D4BE}\u{1D4C5}\u{1D4C9} (Elegant Script)" }, { id: "boldScript", name: "\u{1D4EB}\u{1D4F8}\u{1D4F5}\u{1D4ED} \u{1D4FC}\u{1D4EC}\u{1D4FB}\u{1D4F2}\u{1D4F9}\u{1D4FD} (Bold Script)" }, { id: "monospaceCode", name: "\u{1D68C}\u{1D698}\u{1D68D}\u{1D68E} \u{1D696}\u{1D698}\u{1D697}\u{1D698}\u{1D69C}\u{1D699}\u{1D68A}\u{1D68C}\u{1D68E}" }, { id: "sansSerif", name: "\u{1D5CC}\u{1D5BA}\u{1D5C7}\u{1D5CC}-\u{1D5CC}\u{1D5BE}\u{1D5CB}\u{1D5C2}\u{1D5BF} (Sans-Serif)" }, { id: "boldSansSerif", name: "\u{1D5EF}\u{1D5FC}\u{1D5F9}\u{1D5F1} \u{1D600}\u{1D5EE}\u{1D5FB}\u{1D600}-\u{1D600}\u{1D5F2}\u{1D5FF}\u{1D5F6}\u{1D5F3}" }, { id: "boldSansSerifItalic", name: "\u{1D657}\u{1D664}\u{1D661}\u{1D659} \u{1D668}\u{1D656}\u{1D663}\u{1D668}-\u{1D668}\u{1D65A}\u{1D667}\u{1D65E}\u{1D65B} \u{1D65E}\u{1D669}\u{1D656}\u{1D661}\u{1D65E}\u{1D658}" }, { id: "circledBubble", name: "\u24D2\u24D8\u24E1\u24D2\u24DB\u24D4\u24D3 \u24D1\u24E4\u24D1\u24D1\u24DB\u24D4" }, { id: "negativeCircled", name: "\u{1F15D}\u{1F154}\u{1F156}\u{1F150}\u{1F163}\u{1F158}\u{1F165}\u{1F154} \u{1F152}\u{1F158}\u{1F161}\u{1F152}\u{1F15B}\u{1F154}\u{1F153}" }, { id: "parenthesized", name: "\u249C\u249D\u249E\u249F\u24A0\u24A1\u24A2\u24A3\u24A4\u24A5\u24A6\u24A7" }, { id: "superscriptTiny", name: "\u02E2\u1D58\u1D56\u1D49\u02B3\u02E2\u1D9C\u02B3\u2071\u1D56\u1D57" }, { id: "tagSpecial", name: "\u{1D69D}\u{1D68A}\u{1D690} \u{1D69C}\u{1D699}\u{1D68E}\u{1D68C}\u{1D692}\u{1D68A}\u{1D695}" } ]; var matrixRainHTML = ``; var matrixRainCSS = `body, html { margin: 0; padding: 0; overflow: hidden; background-color: transparent; } canvas { display: block; }`; var matrixRainJS_template = (size, speed, color) => `const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const letters = 'ABCDEFGHIJKLMNOPQRSTUVXYZ'; const fontSize = ${size}; const columns = canvas.width / fontSize; const drops = Array.from({ length: columns }, () => ({ y: Math.random() * -100, // Start above the screen randomly speed: (Math.random() * 0.5 + 0.5) * ${speed} // Varied speeds look nicer })); function draw() { // 1. Clear the canvas completely so it stays transparent ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < drops.length; i++) { const drop = drops[i]; // 2. Loop backwards to draw the trail behind the head of the drop const trailLength = 15; for (let j = 0; j < trailLength; j++) { const trailY = drop.y - j; if (trailY < 0) continue; // Don't draw off-screen // Calculate opacity: the head (j=0) is bright, the tail (j=14) fades out const opacity = 1 - (j / trailLength); const text = letters[Math.floor(Math.random() * letters.length)]; // Apply the opacity to the custom color using globalAlpha ctx.save(); ctx.globalAlpha = opacity; ctx.fillStyle = '${color}'; ctx.fillText(text, i * fontSize, trailY * fontSize); ctx.restore(); } // Move the drop down drop.y += drop.speed; // Reset drop to the top if it goes off screen if (drop.y * fontSize > canvas.height && Math.random() > 0.975) { drop.y = 0; } } } const interval = setInterval(draw, 33); window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; });`; var snowHTML = ``; var snowCSS = `body { background-color: transparent; margin: 0; overflow: hidden; } canvas { display: block; }`; var snowJS_template = (size, speed, color) => `const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let particles = []; function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; particles = []; const particleCount = 200; for (let i = 0; i < particleCount; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, radius: Math.random() * ${size} + 1, speed: (Math.random() * 2 + 1) * ${speed}, }); } } function animate() { requestAnimationFrame(animate); ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.y += p.speed; if (p.y > canvas.height) { p.y = -p.radius; p.x = Math.random() * canvas.width; } ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.fillStyle = '${color}'; ctx.fill(); }); } window.addEventListener('resize', resize); resize(); animate();`; var starTunnelHTML = ``; var starTunnelCSS = `body { background-color: transparent; margin: 0; overflow: hidden; } canvas { display: block; }`; var starTunnelJS_template = (size, speed, color) => `const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let width, height; let stars = []; const numStars = 400; function resize() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; stars = []; for (let i = 0; i < numStars; i++) { stars.push({ x: (Math.random() - 0.5) * width, y: (Math.random() - 0.5) * height, z: Math.random() * width, }); } } function draw() { ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; ctx.fillRect(0, 0, width, height); ctx.save(); ctx.translate(width / 2, height / 2); for (let i = 0; i < numStars; i++) { const star = stars[i]; star.z -= 2 * ${speed}; if (star.z <= 0) { star.x = (Math.random() - 0.5) * width; star.y = (Math.random() - 0.5) * height; star.z = width; } const k = 128.0 / star.z; const px = star.x * k; const py = star.y * k; const s = (1 - star.z / width) * ${size}; ctx.beginPath(); ctx.fillStyle = '${color}'; ctx.arc(px, py, Math.max(0.1, s), 0, Math.PI * 2); ctx.fill(); } ctx.restore(); requestAnimationFrame(draw); } window.addEventListener('resize', resize); resize(); draw();`; var getDefaultPresetSize = (presetName) => { if (presetName === "matrixRain") return 10; if (presetName === "snow") return 4; if (presetName === "starTunnel") return 5; return 10; }; var getDefaultPresetColor = (presetName) => { if (presetName === "matrixRain") return "#00ff00"; return "#ffffff"; }; var detectPresetType = (content) => { if (!content || !content.js) return "custom"; if (content.js.includes("letters") && (content.js.includes("drops") || content.js.includes("drop.y") || content.js.includes("fontSize"))) return "matrixRain"; if (content.js.includes("particles") && (content.js.includes("p.radius") || content.js.includes("particleCount"))) return "snow"; if (content.js.includes("numStars") && (content.js.includes("star.z") || content.js.includes("stars") || content.js.includes("starTunnel"))) return "starTunnel"; return "custom"; }; var SettingsPanel = ({ source, onUpdate, onClose, theme }) => { const [localSource, setLocalSource] = useState5(source); const [ytInputValue, setYtInputValue] = useState5(source.type === "youtube" ? source.content : ""); const [playlistItemUrl, setPlaylistItemUrl] = useState5(""); const [playlistItemName, setPlaylistItemName] = useState5(""); useEffect4(() => { const trimmed = playlistItemUrl.trim(); if (!trimmed) return; let videoId = ""; const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; const match = trimmed.match(regExp); if (match && match[2].length === 11) { videoId = match[2]; } else if (trimmed.length === 11 && !trimmed.includes("http")) { videoId = trimmed; } else { const embedReg = /\/embed\/([a-zA-Z0-9_-]{11})/; const embedMatch = trimmed.match(embedReg); if (embedMatch) { videoId = embedMatch[1]; } } if (videoId && videoId.length === 11) { const controller = new AbortController(); let isMounted = true; const fetchTitle = async () => { try { const url = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`; const response = await fetch(url, { signal: controller.signal }); if (response.ok) { const data = await response.json(); if (data && data.title && isMounted) { setPlaylistItemName(data.title); } } } catch (e) { } }; const timeoutId = setTimeout(fetchTitle, 350); return () => { isMounted = false; clearTimeout(timeoutId); controller.abort(); }; } }, [playlistItemUrl]); const initialTwitchContent = source.type === "twitch" ? source.content : null; const [twitchChannel, setTwitchChannel] = useState5(initialTwitchContent?.channel || "albiar"); const [twitchParent, setTwitchParent] = useState5(initialTwitchContent?.parent || window.location.hostname || "localhost"); const [twitchUrlInput, setTwitchUrlInput] = useState5(initialTwitchContent?.urlInput || (initialTwitchContent?.channel ? `https://www.twitch.tv/${initialTwitchContent.channel}` : "https://www.twitch.tv/albiar")); const [twitchLayout, setTwitchLayout] = useState5(initialTwitchContent?.layout || "video-with-chat"); const [twitchAutoplay, setTwitchAutoplay] = useState5(initialTwitchContent?.autoplay !== false); const [twitchMuted, setTwitchMuted] = useState5(initialTwitchContent?.muted === true); const [twitchTheme, setTwitchTheme] = useState5(initialTwitchContent?.theme || "dark"); const fileInputRef = useRef5(null); const galleryFileInputRef = useRef5(null); const [galleryUrl, setGalleryUrl] = useState5(""); const [activePreset, setActivePreset] = useState5("custom"); const [videoDuration, setVideoDuration] = useState5(100); useEffect4(() => { setLocalSource(source); if (source.type === "youtube") { setYtInputValue(source.content); } if (source.type === "twitch") { const tc = source.content; const chan = tc?.channel || ""; setTwitchChannel(chan); setTwitchParent(tc?.parent || window.location.hostname || "localhost"); setTwitchUrlInput(tc?.urlInput || (chan ? `https://www.twitch.tv/${chan}` : "")); setTwitchLayout(tc?.layout || "video-with-chat"); setTwitchAutoplay(tc?.autoplay !== false); setTwitchMuted(tc?.muted === true); setTwitchTheme(tc?.theme || "dark"); } }, [source]); useEffect4(() => { if (source.type === "video" && source.content) { const tempVideo = document.createElement("video"); tempVideo.src = source.content; const handleLoadedMetadata = () => { if (tempVideo.duration) { setVideoDuration(tempVideo.duration); } }; tempVideo.addEventListener("loadedmetadata", handleLoadedMetadata); return () => { tempVideo.removeEventListener("loadedmetadata", handleLoadedMetadata); }; } }, [source.content, source.type]); const themeClasses = themes[theme]; const [customPresets, setCustomPresets] = useState5([]); const [isCreatingPreset, setIsCreatingPreset] = useState5(false); const [newPresetName, setNewPresetName] = useState5(""); useEffect4(() => { try { const stored = localStorage.getItem("scene_builder_custom_presets"); if (stored) { setCustomPresets(JSON.parse(stored)); } } catch (e) { console.error("Failed to load custom presets", e); } }, []); const saveCustomPresets = (updated) => { setCustomPresets(updated); try { localStorage.setItem("scene_builder_custom_presets", JSON.stringify(updated)); } catch (e) { console.error("Failed to save custom presets", e); } }; const detectedPreset = source.type === "code" ? detectPresetType(source.content) : "custom"; const presets = { matrixRain: { html: matrixRainHTML, css: matrixRainCSS, js: matrixRainJS_template( localSource.style.codeSize ?? getDefaultPresetSize("matrixRain"), localSource.style.codeSpeed ?? 1, localSource.style.codeColor ?? getDefaultPresetColor("matrixRain") ) }, snow: { html: snowHTML, css: snowCSS, js: snowJS_template( localSource.style.codeSize ?? getDefaultPresetSize("snow"), localSource.style.codeSpeed ?? 1, localSource.style.codeColor ?? getDefaultPresetColor("snow") ) }, starTunnel: { html: starTunnelHTML, css: starTunnelCSS, js: starTunnelJS_template( localSource.style.codeSize ?? getDefaultPresetSize("starTunnel"), localSource.style.codeSpeed ?? 1, localSource.style.codeColor ?? getDefaultPresetColor("starTunnel") ) } }; useEffect4(() => { if (source.type === "code") { const currentContent = source.content; const customMatched = customPresets.find((p) => p.html === currentContent.html && p.css === currentContent.css && p.js === currentContent.js); if (customMatched) { setActivePreset(customMatched.id); return; } const presetType = detectPresetType(currentContent); if (presetType !== "custom") { setActivePreset(presetType); return; } } setActivePreset("custom"); }, [source.content, source.type, customPresets]); const handleStyleChange = (key, value) => { const updatedStyle = { ...localSource.style, [key]: value }; setLocalSource({ ...localSource, style: updatedStyle }); onUpdate(source.id, (s) => ({ style: { ...s.style, [key]: value } })); }; const handleMultipleStylesChange = (updates) => { const updatedStyle = { ...localSource.style, ...updates }; setLocalSource({ ...localSource, style: updatedStyle }); onUpdate(source.id, (s) => ({ style: { ...s.style, ...updates } })); }; const applySizePreset = (preset) => { let w = localSource.style.width; let h = localSource.style.height; let x = localSource.style.x; let y = localSource.style.y; if (preset === "full") { w = window.innerWidth; h = window.innerHeight; x = 0; y = 0; } else if (preset === "240") { w = 426; h = 240; } else if (preset === "480") { w = 854; h = 480; } else if (preset === "720") { w = 1280; h = 720; } else if (preset === "1080") { w = 1920; h = 1080; } if (preset !== "full") { const currentCenterX = localSource.style.x + localSource.style.width / 2; const currentCenterY = localSource.style.y + localSource.style.height / 2; x = currentCenterX - w / 2; y = currentCenterY - h / 2; } handleMultipleStylesChange({ width: w, height: h, x, y }); }; const centerSourceOnCanvas = () => { const w = localSource.style.width; const h = localSource.style.height; const x = (window.innerWidth - w) / 2; const y = (window.innerHeight - h) / 2; handleMultipleStylesChange({ x, y }); }; const handleSourceChange = (key, value) => { const updatedSource = { ...localSource, [key]: value }; setLocalSource(updatedSource); onUpdate(source.id, { [key]: value }); }; const handleFileChange = (event) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target?.result; handleSourceChange("content", dataUrl); }; reader.readAsDataURL(file); } }; const handleGalleryImageUpload = (event) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target?.result; addGalleryImage(dataUrl); }; reader.readAsDataURL(file); } }; const addGalleryImage = (url) => { if (!url) return; const newImage = { id: `img-${Date.now()}`, url, visible: true }; const newContent = [...localSource.content, newImage]; handleSourceChange("content", newContent); setGalleryUrl(""); }; const updateGalleryImage = (imgId, updates) => { const newContent = localSource.content.map((img) => img.id === imgId ? { ...img, ...updates } : img); handleSourceChange("content", newContent); }; const removeGalleryImage = (imgId) => { const newContent = localSource.content.filter((img) => img.id !== imgId); handleSourceChange("content", newContent); }; const moveGalleryImage = (index, direction) => { const images = [...localSource.content]; const targetIndex = direction === "up" ? index - 1 : index + 1; if (targetIndex < 0 || targetIndex >= images.length) return; [images[index], images[targetIndex]] = [images[targetIndex], images[index]]; handleSourceChange("content", images); }; const handleCodeContentChange = (field, value) => { const newContent = { ...localSource.content, [field]: value }; handleSourceChange("content", newContent); }; const updatePresetJS = (presetName, sizeVal, speedVal, colorVal) => { let jsCode = ""; if (presetName === "matrixRain") { jsCode = matrixRainJS_template(sizeVal, speedVal, colorVal); } else if (presetName === "snow") { jsCode = snowJS_template(sizeVal, speedVal, colorVal); } else if (presetName === "starTunnel") { jsCode = starTunnelJS_template(sizeVal, speedVal, colorVal); } else { return; } const currentCode = localSource.content; handleSourceChange("content", { ...currentCode, js: jsCode }); }; const handlePresetSliderChange = (field, value) => { handleMultipleStylesChange({ [field]: value }); const size = field === "codeSize" ? Number(value) : localSource.style.codeSize ?? getDefaultPresetSize(detectedPreset); const speed = field === "codeSpeed" ? Number(value) : localSource.style.codeSpeed ?? 1; const color = field === "codeColor" ? value : localSource.style.codeColor ?? getDefaultPresetColor(detectedPreset); updatePresetJS(detectedPreset, size, speed, color); }; const applyPreset = (presetName) => { const size = getDefaultPresetSize(presetName); const speed = 1; const color = getDefaultPresetColor(presetName); handleMultipleStylesChange({ codeSize: size, codeSpeed: speed, codeColor: color }); const preset = presets[presetName]; const jsCode = presetName === "matrixRain" ? matrixRainJS_template(size, speed, color) : presetName === "snow" ? snowJS_template(size, speed, color) : presetName === "starTunnel" ? starTunnelJS_template(size, speed, color) : preset.js; handleSourceChange("content", { html: preset.html, css: preset.css, js: jsCode }); setActivePreset(presetName); }; const commonUrlInput = (label, type) => /* @__PURE__ */ jsxs5(SettingsInput, { label, theme, children: [ /* @__PURE__ */ jsxs5("div", { className: "flex space-x-2", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", value: typeof localSource.content === "string" && localSource.content.startsWith("data:") ? "(Local File)" : localSource.content, onChange: (e) => handleSourceChange("content", e.target.value), readOnly: typeof localSource.content === "string" && localSource.content.startsWith("data:"), className: `w-full ${themeClasses.inputBg} border ${themeClasses.border} rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500` } ), /* @__PURE__ */ jsx5( "button", { onClick: () => fileInputRef.current?.click(), className: `${themeClasses.button} text-xs px-2 rounded ${type === "image" && !localSource.content ? "pulse-glow-yellow" : ""}`, children: "Upload" } ) ] }), /* @__PURE__ */ jsx5("input", { type: "file", ref: fileInputRef, onChange: handleFileChange, accept: `${type}/*`, className: "hidden" }) ] }); const textInputClass = `w-full ${themeClasses.inputBg} border ${themeClasses.border} rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`; const codeTextareaClass = `${textInputClass} h-32 resize-y font-mono text-xs`; const getActiveFilterCount = () => { let count = 0; const { chromaKey, colorKey, filters } = source.style; if (chromaKey?.enabled) count++; if (colorKey?.enabled) count++; if (filters) { if (filters.blur > 0) count++; if (filters.hue > 0) count++; if (filters.sharpness > 0) count++; if (filters.shape !== "none") count++; } return count; }; const activeFilterCount = getActiveFilterCount(); return /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("div", { className: `p-3 border-b ${themeClasses.border}`, children: /* @__PURE__ */ jsxs5("h2", { className: "text-lg font-bold truncate", children: [ "Settings: ", localSource.name ] }) }), activeFilterCount > 0 && /* @__PURE__ */ jsxs5("div", { className: `p-2 bg-blue-900/50 text-blue-300 text-xs rounded mx-3 mt-3 text-center border border-blue-800`, children: [ "This source has ", /* @__PURE__ */ jsxs5("strong", { children: [ activeFilterCount, " active filter(s)" ] }), ". Edit them in the ", /* @__PURE__ */ jsx5("span", { className: "font-bold", children: "Filters" }), " tab." ] }), /* @__PURE__ */ jsxs5("div", { className: "p-3", children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Name", theme, children: /* @__PURE__ */ jsx5("input", { type: "text", value: localSource.name, onChange: (e) => handleSourceChange("name", e.target.value), className: textInputClass }) }), source.type === "image" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ commonUrlInput("Image URL", "image"), /* @__PURE__ */ jsx5(SettingsInput, { label: "Resize Behavior on Window Resize", theme, children: /* @__PURE__ */ jsxs5( "select", { value: localSource.style.resizeBehavior || "static", onChange: (e) => handleStyleChange("resizeBehavior", e.target.value), className: textInputClass, children: [ /* @__PURE__ */ jsx5("option", { value: "static", children: "Static (Stay at exact size/position)" }), /* @__PURE__ */ jsx5("option", { value: "fill", children: "Fill Screen (Cover entire viewport)" }), /* @__PURE__ */ jsx5("option", { value: "full-width", children: "Full Width (Stretches horizontal)" }), /* @__PURE__ */ jsx5("option", { value: "full-height", children: "Full Height (Stretches vertical)" }), /* @__PURE__ */ jsx5("option", { value: "center", children: "Stay Centered (Keep size but center)" }), /* @__PURE__ */ jsx5("option", { value: "proportional", children: "Stay Aligned (Proportional scale & position)" }) ] } ) }) ] }), source.type === "iframe" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsxs5(SettingsInput, { label: "Website URL", theme, children: [ /* @__PURE__ */ jsx5("input", { type: "text", value: localSource.content, onChange: (e) => handleSourceChange("content", e.target.value), className: textInputClass }), /* @__PURE__ */ jsx5("p", { className: `text-xs mt-1 ${themeClasses.label}`, children: "Note: Some websites may not allow embedding." }) ] }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Zoom (${(localSource.style.scale || 1).toFixed(2)}x)`, theme, children: /* @__PURE__ */ jsx5("input", { type: "range", min: "0.1", max: "5", step: "0.01", value: localSource.style.scale || 1, onChange: (e) => handleStyleChange("scale", parseFloat(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }) ] }), source.type === "youtube" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsxs5(SettingsInput, { label: "YouTube Link, Embed URL or Video ID", theme, children: [ /* @__PURE__ */ jsxs5( "form", { onSubmit: (e) => { e.preventDefault(); const typed = ytInputValue.trim(); if (!typed) return; let ytUrl = typed; let videoId = ""; const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; const match = typed.match(regExp); if (match && match[2].length === 11) { videoId = match[2]; ytUrl = `https://www.youtube.com/embed/${videoId}?enablejsapi=1`; } else if (typed.length === 11 && !typed.includes("http")) { videoId = typed; ytUrl = `https://www.youtube.com/embed/${videoId}?enablejsapi=1`; } else if (typed && !typed.includes("enablejsapi=1")) { if (typed.includes("?")) { ytUrl = `${typed}&enablejsapi=1`; } else { ytUrl = `${typed}?enablejsapi=1`; } } setYtInputValue(ytUrl); handleSourceChange("content", ytUrl); const currentPlaylist = source.style.youtubePlaylist || []; if (currentPlaylist.length === 0) { handleStyleChange("youtubePlaylist", [{ id: "main-item", url: ytUrl, name: "Main Video" }]); handleStyleChange("youtubeActivePlaylistItemIndex", 0); } }, className: "flex gap-2", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", value: ytInputValue, onChange: (e) => setYtInputValue(e.target.value), className: `${textInputClass} flex-1`, placeholder: "e.g. dQw4w9WgXcQ or https://www.youtube.com/watch?v=..." } ), /* @__PURE__ */ jsx5( "button", { type: "submit", className: "bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-3 py-1.5 text-xs rounded transition-colors active:scale-95 cursor-pointer", children: "Apply" } ) ] } ), /* @__PURE__ */ jsxs5("p", { className: `text-[10px] mt-1.5 ${themeClasses.label} opacity-85 leading-normal`, children: [ "Paste any YouTube watch link, copy-paste the share URL, or enter the 11-caracter Video ID and click ", /* @__PURE__ */ jsx5("strong", { children: "Apply" }), "." ] }) ] }), /* @__PURE__ */ jsxs5("div", { className: `p-3 border ${themeClasses.border} rounded-xl bg-indigo-950/10 border-indigo-500/20 mb-4 space-y-3`, children: [ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsxs5("span", { className: "text-[11px] font-bold text-indigo-400 tracking-wider uppercase flex items-center gap-1", children: [ /* @__PURE__ */ jsx5("span", { children: "\u{1F4CB}" }), " YouTube Playlist Builder" ] }), /* @__PURE__ */ jsx5("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs5("label", { className: "text-[10px] items-center flex gap-1 cursor-pointer", children: [ /* @__PURE__ */ jsx5( "input", { type: "checkbox", checked: source.style.youtubeShowPlaylistPanel === true, onChange: (e) => handleStyleChange("youtubeShowPlaylistPanel", e.target.checked), className: "rounded border-zinc-700 bg-zinc-900 text-indigo-600 focus:ring-0 focus:ring-offset-0 cursor-pointer" } ), "Show Panel" ] }) }) ] }), /* @__PURE__ */ jsx5("p", { className: "text-[9.5px] text-zinc-400 leading-normal", children: "Create multiple video links in a custom playlist list. A sidebar display (20% of the player's width) will let users view and toggle clips." }), /* @__PURE__ */ jsxs5("div", { className: "p-2 bg-zinc-900/60 rounded-lg border border-zinc-850 space-y-2", children: [ /* @__PURE__ */ jsx5("div", { className: "text-[9.5px] font-semibold text-zinc-300", children: "Add New Playlist Video:" }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-1 gap-1.5", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", placeholder: "Video URL or ID (required)", value: playlistItemUrl, onChange: (e) => setPlaylistItemUrl(e.target.value), className: `${textInputClass} text-xs py-1` } ), /* @__PURE__ */ jsxs5("div", { className: "flex gap-1.5", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", placeholder: "Display Title / Optional description", value: playlistItemName, onChange: (e) => setPlaylistItemName(e.target.value), className: `${textInputClass} text-xs py-1 flex-1` } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { const trimmedUrl = playlistItemUrl.trim(); if (!trimmedUrl) return; let finalUrl = trimmedUrl; let videoId = ""; const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; const match = trimmedUrl.match(regExp); if (match && match[2].length === 11) { videoId = match[2]; } else if (trimmedUrl.length === 11 && !trimmedUrl.includes("http")) { videoId = trimmedUrl; } if (videoId) { finalUrl = `https://www.youtube.com/embed/${videoId}?enablejsapi=1`; } const proposedName = playlistItemName.trim() || (videoId ? `Video ${videoId}` : `Video ${(source.style.youtubePlaylist?.length || 0) + 1}`); const newItem = { id: `item-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, url: finalUrl, name: proposedName }; const currentPlaylist = source.style.youtubePlaylist || []; const updatedPlaylist = [...currentPlaylist, newItem]; handleStyleChange("youtubePlaylist", updatedPlaylist); if (currentPlaylist.length === 0) { handleStyleChange("youtubeActivePlaylistItemIndex", 0); } setPlaylistItemUrl(""); setPlaylistItemName(""); }, className: "bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-2 py-1 text-[10px] rounded transition-colors cursor-pointer", children: "\uFF0B Add" } ) ] }) ] }) ] }), Array.isArray(source.style.youtubePlaylist) && source.style.youtubePlaylist.length > 0 ? /* @__PURE__ */ jsxs5("div", { className: "space-y-1.5 max-h-[170px] overflow-y-auto pr-1", children: [ /* @__PURE__ */ jsxs5("div", { className: "text-[9.5px] font-bold text-zinc-400 uppercase tracking-wide", children: [ "Current Items (", source.style.youtubePlaylist.length, ")" ] }), source.style.youtubePlaylist.map((item, index) => { const isActive = (source.style.youtubeActivePlaylistItemIndex ?? 0) === index; return /* @__PURE__ */ jsxs5( "div", { className: `p-1.5 rounded border text-[10px] flex items-center justify-between gap-1.5 transition-all ${isActive ? "bg-indigo-650/20 border-indigo-505/40 text-white font-medium" : "bg-zinc-900/30 border-zinc-805/50 text-zinc-300"}`, children: [ /* @__PURE__ */ jsxs5( "button", { type: "button", onClick: () => handleStyleChange("youtubeActivePlaylistItemIndex", index), className: "flex-1 text-left truncate cursor-pointer hover:text-indigo-300 transition-colors", title: "Click to play on preview", children: [ /* @__PURE__ */ jsxs5("span", { className: "text-zinc-500 font-mono select-none mr-1", children: [ index + 1, "." ] }), item.name || "Untitled Video" ] } ), /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1 select-none", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", value: item.name || "", onChange: (e) => { const updatedList = (source.style.youtubePlaylist || []).map((p, i) => { if (i === index) { return { ...p, name: e.target.value }; } return p; }); handleStyleChange("youtubePlaylist", updatedList); }, className: "w-16 bg-zinc-900 border border-zinc-800 text-[9px] px-1 py-0.5 rounded text-zinc-200 focus:outline-none", placeholder: "Rename", title: "Rename playlist display item" } ), /* @__PURE__ */ jsx5( "button", { type: "button", disabled: index === 0, onClick: () => { const list = [...source.style.youtubePlaylist || []]; const target = list[index]; list[index] = list[index - 1]; list[index - 1] = target; handleStyleChange("youtubePlaylist", list); if (isActive) { handleStyleChange("youtubeActivePlaylistItemIndex", index - 1); } else if (source.style.youtubeActivePlaylistItemIndex === index - 1) { handleStyleChange("youtubeActivePlaylistItemIndex", index); } }, className: `p-0.5 rounded hover:bg-zinc-800 text-xs text-zinc-400 hover:text-white transition-all ${index === 0 ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}`, title: "Move Up", children: "\u25B2" } ), /* @__PURE__ */ jsx5( "button", { type: "button", disabled: index === source.style.youtubePlaylist.length - 1, onClick: () => { const list = [...source.style.youtubePlaylist || []]; const target = list[index]; list[index] = list[index + 1]; list[index + 1] = target; handleStyleChange("youtubePlaylist", list); if (isActive) { handleStyleChange("youtubeActivePlaylistItemIndex", index + 1); } else if (source.style.youtubeActivePlaylistItemIndex === index + 1) { handleStyleChange("youtubeActivePlaylistItemIndex", index); } }, className: `p-0.5 rounded hover:bg-zinc-800 text-xs text-zinc-400 hover:text-white transition-all ${index === source.style.youtubePlaylist.length - 1 ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}`, title: "Move Down", children: "\u25BC" } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { const updatedList = (source.style.youtubePlaylist || []).filter((_, i) => i !== index); handleStyleChange("youtubePlaylist", updatedList); let nextIndex = source.style.youtubeActivePlaylistItemIndex || 0; if (nextIndex >= updatedList.length) { nextIndex = Math.max(0, updatedList.length - 1); } handleStyleChange("youtubeActivePlaylistItemIndex", nextIndex); }, className: "p-0.5 rounded hover:bg-red-950 text-red-400 hover:text-red-300 transition-all cursor-pointer text-[10px]", title: "Delete", children: "\u2715" } ) ] }) ] }, item.id ); }) ] }) : /* @__PURE__ */ jsx5("div", { className: "text-center p-2 border border-dashed border-zinc-800 rounded text-zinc-500 text-[9.5px]", children: "No videos added to playlist yet. The main video URL will be played alone." }), Array.isArray(source.style.youtubePlaylist) && source.style.youtubePlaylist.length > 0 && /* @__PURE__ */ jsx5("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { handleStyleChange("youtubePlaylist", void 0); handleStyleChange("youtubeActivePlaylistItemIndex", 0); }, className: "text-[9.5px] font-semibold text-red-400 hover:text-red-300 cursor-pointer", children: "\u{1F5D1}\uFE0F Clear / Reset Playlist" } ) }) ] }), /* @__PURE__ */ jsxs5(SettingsInput, { label: "Video Control Panel", theme, children: [ /* @__PURE__ */ jsxs5("div", { className: "flex gap-2 mb-2", children: [ /* @__PURE__ */ jsx5( "button", { type: "button", onClick: (e) => { e.stopPropagation(); const iframe = document.getElementById(`youtube-iframe-${source.id}`); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', "*"); } }, className: `flex-1 py-1.5 px-3 text-xs font-semibold rounded bg-green-600 hover:bg-green-500 text-white transition-colors flex items-center justify-center gap-1 cursor-pointer active:scale-95`, title: "Play YouTube Video", children: "\u25B6\uFE0F Play" } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: (e) => { e.stopPropagation(); const iframe = document.getElementById(`youtube-iframe-${source.id}`); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', "*"); } }, className: `flex-1 py-1.5 px-3 text-xs font-semibold rounded bg-yellow-600 hover:bg-yellow-500 text-white transition-colors flex items-center justify-center gap-1 cursor-pointer active:scale-95`, title: "Pause YouTube Video", children: "\u23F8\uFE0F Pause" } ) ] }), /* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [ /* @__PURE__ */ jsx5( "button", { type: "button", onClick: (e) => { e.stopPropagation(); const iframe = document.getElementById(`youtube-iframe-${source.id}`); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage('{"event":"command","func":"mute","args":""}', "*"); } }, className: `flex-1 py-1 px-2 text-[11px] font-medium rounded bg-zinc-700 hover:bg-zinc-650 text-white transition-all cursor-pointer active:scale-95`, title: "Mute Audio", children: "\u{1F507} Mute" } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: (e) => { e.stopPropagation(); const iframe = document.getElementById(`youtube-iframe-${source.id}`); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage('{"event":"command","func":"unMute","args":""}', "*"); } }, className: `flex-1 py-1 px-2 text-[11px] font-medium rounded bg-zinc-700 hover:bg-zinc-650 text-white transition-all cursor-pointer active:scale-95`, title: "Unmute Audio", children: "\u{1F50A} Unmute" } ) ] }), /* @__PURE__ */ jsx5("p", { className: `text-[10px] mt-2 text-zinc-500 leading-normal`, children: "Note: Controls send play/pause and mute/unmute commands directly to the active YouTube iframe." }) ] }) ] }), source.type === "twitch" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsxs5("div", { className: `p-4 border ${themeClasses.border} rounded-xl bg-violet-950/10 border-violet-800/20 mb-4 space-y-4`, children: [ /* @__PURE__ */ jsxs5("h4", { className: "text-violet-400 font-extrabold text-xs uppercase tracking-wider flex items-center gap-1.5", children: [ /* @__PURE__ */ jsx5("span", { children: "\u{1F3AE}" }), " Twitch Interactive Stream Configurator" ] }), /* @__PURE__ */ jsx5("p", { className: "text-[10px] text-zinc-400 leading-relaxed", children: "Insert a Twitch Channel username or a full URL. The parser will automatically isolate the channel ID." }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Twitch Channel Name or URL", theme, children: /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5( "input", { type: "text", value: twitchUrlInput, onChange: (e) => { const val = e.target.value; setTwitchUrlInput(val); let parsedChannel = val.trim(); if (parsedChannel.includes("twitch.tv/")) { const parts = parsedChannel.split("twitch.tv/"); if (parts[1]) { const sub = parts[1].split(/[/?# ]/); parsedChannel = sub[0]; } } setTwitchChannel(parsedChannel); handleSourceChange("content", { channel: parsedChannel, parent: twitchParent, urlInput: val, layout: twitchLayout, autoplay: twitchAutoplay, muted: twitchMuted, theme: twitchTheme }); }, className: `${textInputClass} border-violet-500/30 focus:border-violet-500`, placeholder: "e.g. monstercat or https://twitch.tv/monstercat" } ), twitchChannel && /* @__PURE__ */ jsxs5("div", { className: "mt-1.5 flex items-center justify-between text-[10px] text-zinc-400", children: [ /* @__PURE__ */ jsx5("span", { children: "Active Channel Name:" }), /* @__PURE__ */ jsx5("span", { className: "font-mono px-1.5 py-0.5 rounded bg-violet-950 border border-violet-800 text-violet-300 font-bold", children: twitchChannel }) ] }) ] }) }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("span", { className: "block text-[11px] font-bold text-gray-400 uppercase tracking-wider mb-2", children: "Interactive Layout Component" }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-3 gap-1 bg-zinc-900/60 p-1 border border-zinc-700/40 rounded-lg", children: [ /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { setTwitchLayout("video-with-chat"); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: "video-with-chat", autoplay: twitchAutoplay, muted: twitchMuted, theme: twitchTheme }); }, className: `text-[10px] font-bold py-1.5 rounded-md cursor-pointer transition ${twitchLayout === "video-with-chat" ? "bg-violet-600 text-white shadow-sm" : "text-zinc-400 hover:text-zinc-200"}`, children: "Video + Chat" } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { setTwitchLayout("video"); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: "video", autoplay: twitchAutoplay, muted: twitchMuted, theme: twitchTheme }); }, className: `text-[10px] font-bold py-1.5 rounded-md cursor-pointer transition ${twitchLayout === "video" ? "bg-violet-600 text-white shadow-sm" : "text-zinc-400 hover:text-zinc-200"}`, children: "Video Only" } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { setTwitchLayout("chat-only"); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: "chat-only", autoplay: twitchAutoplay, muted: twitchMuted, theme: twitchTheme }); }, className: `text-[10px] font-bold py-1.5 rounded-md cursor-pointer transition ${twitchLayout === "chat-only" ? "bg-violet-600 text-white shadow-sm" : "text-zinc-400 hover:text-zinc-200"}`, children: "Chat Only" } ) ] }) ] }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Permitted Parent Domain", theme, children: /* @__PURE__ */ jsxs5("div", { className: "space-y-1.5", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", value: twitchParent, onChange: (e) => { const val = e.target.value.trim(); setTwitchParent(val); handleSourceChange("content", { channel: twitchChannel, parent: val, urlInput: twitchUrlInput, layout: twitchLayout, autoplay: twitchAutoplay, muted: twitchMuted, theme: twitchTheme }); }, className: `${textInputClass} border-violet-500/30 focus:border-violet-500`, placeholder: "e.g. localhost or customsite.com" } ), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between text-[9px] text-zinc-400 pt-0.5", children: [ /* @__PURE__ */ jsx5("span", { children: "Required for hosting previews:" }), /* @__PURE__ */ jsxs5( "button", { type: "button", onClick: () => { const currentHost = window.location.hostname || "localhost"; setTwitchParent(currentHost); handleSourceChange("content", { channel: twitchChannel, parent: currentHost, urlInput: twitchUrlInput, layout: twitchLayout, autoplay: twitchAutoplay, muted: twitchMuted, theme: twitchTheme }); }, className: "px-2 py-0.5 rounded bg-violet-900/60 hover:bg-violet-800 text-violet-200 cursor-pointer font-semibold transition active:scale-95", children: [ "Set to ", window.location.hostname || "localhost" ] } ) ] }) ] }) }), twitchLayout !== "chat-only" && /* @__PURE__ */ jsxs5("div", { className: "pt-1.5 border-t border-violet-900/10 grid grid-cols-2 gap-3", children: [ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 cursor-pointer group", children: [ /* @__PURE__ */ jsx5( "input", { type: "checkbox", checked: twitchAutoplay, onChange: (e) => { const checked = e.target.checked; setTwitchAutoplay(checked); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: twitchLayout, autoplay: checked, muted: twitchMuted, theme: twitchTheme }); }, className: "rounded border-zinc-700 text-violet-600 focus:ring-violet-500 bg-zinc-900 cursor-pointer w-3.5 h-3.5" } ), /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-bold text-gray-400 group-hover:text-gray-200 transition", children: "Autoplay Stream" }) ] }), /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 cursor-pointer group", children: [ /* @__PURE__ */ jsx5( "input", { type: "checkbox", checked: twitchMuted, onChange: (e) => { const checked = e.target.checked; setTwitchMuted(checked); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: twitchLayout, autoplay: twitchAutoplay, muted: checked, theme: twitchTheme }); }, className: "rounded border-zinc-700 text-violet-600 focus:ring-violet-500 bg-zinc-900 cursor-pointer w-3.5 h-3.5" } ), /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-bold text-gray-400 group-hover:text-gray-200 transition", children: "Muted by Default" }) ] }) ] }), /* @__PURE__ */ jsxs5("div", { className: "pt-1.5 flex items-center justify-between border-t border-violet-900/10", children: [ /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-bold text-gray-400 uppercase tracking-wider", children: "Embed Color Style" }), /* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [ /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { setTwitchTheme("dark"); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: twitchLayout, autoplay: twitchAutoplay, muted: twitchMuted, theme: "dark" }); }, className: `text-[10px] font-bold px-2 py-0.5 rounded cursor-pointer transition ${twitchTheme === "dark" ? "bg-zinc-800 text-violet-400 border border-violet-500/50" : "text-zinc-500 hover:text-zinc-300"}`, children: "Dark Theme" } ), /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => { setTwitchTheme("light"); handleSourceChange("content", { channel: twitchChannel, parent: twitchParent, urlInput: twitchUrlInput, layout: twitchLayout, autoplay: twitchAutoplay, muted: twitchMuted, theme: "light" }); }, className: `text-[10px] font-bold px-2 py-0.5 rounded cursor-pointer transition ${twitchTheme === "light" ? "bg-zinc-200 text-zinc-900 border border-zinc-300" : "text-zinc-500 hover:text-zinc-300"}`, children: "Light Theme" } ) ] }) ] }) ] }), /* @__PURE__ */ jsxs5("div", { className: "bg-zinc-950/40 border border-zinc-800 rounded-xl p-3 text-[10px] text-zinc-400 space-y-2 mb-4 leading-normal", children: [ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsx5("span", { className: "font-bold text-zinc-300 uppercase tracking-wider text-[9px]", children: "Generated Live Embed Source" }), /* @__PURE__ */ jsx5("span", { className: "text-[9px] text-violet-400 font-mono font-bold", children: twitchLayout === "chat-only" ? "CHAT COMPONENT" : "PLAYER COMPONENT" }) ] }), /* @__PURE__ */ jsx5("code", { className: "block bg-black p-2 rounded-lg text-[9.5px] break-all text-violet-300 border border-zinc-800/80 leading-normal font-mono select-all", children: twitchLayout === "chat-only" ? `https://www.twitch.tv/embed/${twitchChannel || "username"}/chat?parent=${twitchParent || "domain"}` : `https://player.twitch.tv/?channel=${twitchChannel || "username"}&parent=${twitchParent || "domain"}&autoplay=${twitchAutoplay}&muted=${twitchMuted}&layout=${twitchLayout}&theme=${twitchTheme}` }) ] }) ] }), source.type === "color" && /* @__PURE__ */ jsx5(SettingsInput, { label: "Background Color", theme, children: /* @__PURE__ */ jsx5("input", { type: "color", value: localSource.content, onChange: (e) => handleSourceChange("content", e.target.value), className: `w-full ${themeClasses.inputBg} border ${themeClasses.border} rounded h-8 p-0 cursor-pointer` }) }), source.type === "paint" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Brush Color", theme, children: /* @__PURE__ */ jsx5("input", { type: "color", value: localSource.style.brushColor || "#ffffff", onChange: (e) => handleStyleChange("brushColor", e.target.value), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Brush Size (${localSource.style.brushSize || 5}px)`, theme, children: /* @__PURE__ */ jsx5("input", { type: "range", min: "1", max: "100", step: "1", value: localSource.style.brushSize || 5, onChange: (e) => handleStyleChange("brushSize", parseInt(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }) ] }), source.type === "video" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ commonUrlInput("Video URL", "video"), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mt-3 p-1", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm font-bold", children: "Playback State" }), /* @__PURE__ */ jsx5( "button", { onClick: () => handleStyleChange("videoPlaying", !(localSource.style.videoPlaying ?? true)), className: `px-4 py-1 text-xs font-bold rounded-full text-white transition-colors cursor-pointer ${localSource.style.videoPlaying ?? true ? "bg-green-600 hover:bg-green-700" : "bg-orange-600 hover:bg-orange-700"}`, children: localSource.style.videoPlaying ?? true ? "\u23F8\uFE0F PAUSED" : "\u25B6\uFE0F PLAYING" } ) ] }), /* @__PURE__ */ jsxs5("div", { className: "mt-4 mb-4 p-2.5 border border-gray-700/50 rounded bg-gray-900/30", children: [ /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-bold block mb-2 uppercase tracking-wide text-blue-400", children: "Video Trim Timeline" }), /* @__PURE__ */ jsxs5("div", { className: "mb-3", children: [ /* @__PURE__ */ jsxs5("div", { className: "flex justify-between text-xs mb-1", children: [ /* @__PURE__ */ jsx5("span", { children: "Start Playback At:" }), /* @__PURE__ */ jsxs5("span", { className: "font-mono font-bold text-blue-400", children: [ (localSource.style.videoStart ?? 0).toFixed(1), "s" ] }) ] }), /* @__PURE__ */ jsx5( "input", { type: "range", min: "0", max: videoDuration || 100, step: "0.1", value: localSource.style.videoStart ?? 0, onChange: (e) => { const val = parseFloat(e.target.value); const endVal = localSource.style.videoEnd ?? videoDuration; handleStyleChange("videoStart", Math.min(val, endVal - 0.1)); }, className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ) ] }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsxs5("div", { className: "flex justify-between text-xs mb-1", children: [ /* @__PURE__ */ jsx5("span", { children: "Stop Playback At:" }), /* @__PURE__ */ jsxs5("span", { className: "font-mono font-bold text-red-500", children: [ (localSource.style.videoEnd ?? videoDuration).toFixed(1), "s" ] }) ] }), /* @__PURE__ */ jsx5( "input", { type: "range", min: "0", max: videoDuration || 100, step: "0.1", value: localSource.style.videoEnd ?? videoDuration, onChange: (e) => { const val = parseFloat(e.target.value); const startVal = localSource.style.videoStart ?? 0; handleStyleChange("videoEnd", Math.max(val, startVal + 0.1)); }, className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ) ] }) ] }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mt-3", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm", children: "Loop Video" }), /* @__PURE__ */ jsx5("button", { onClick: () => handleStyleChange("loop", !(localSource.style.loop ?? true)), className: `px-3 py-1 text-xs rounded-full ${localSource.style.loop ?? true ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: localSource.style.loop ?? true ? "ON" : "OFF" }) ] }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Playback Speed", theme, children: /* @__PURE__ */ jsxs5("select", { value: localSource.style.playbackRate ?? 1, onChange: (e) => handleStyleChange("playbackRate", parseFloat(e.target.value)), className: textInputClass, children: [ /* @__PURE__ */ jsx5("option", { value: "0.5", children: "0.5x" }), /* @__PURE__ */ jsx5("option", { value: "1", children: "1x (Normal)" }), /* @__PURE__ */ jsx5("option", { value: "1.5", children: "1.5x" }), /* @__PURE__ */ jsx5("option", { value: "2", children: "2x" }) ] }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Volume (${Math.round((localSource.style.volume ?? 1) * 100)}%)`, theme, children: /* @__PURE__ */ jsx5("input", { type: "range", min: "0", max: "1", step: "0.01", value: localSource.style.volume ?? 1, onChange: (e) => handleStyleChange("volume", parseFloat(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm", children: "Muted" }), /* @__PURE__ */ jsx5("button", { onClick: () => handleStyleChange("muted", !(localSource.style.muted ?? true)), className: `px-3 py-1 text-xs rounded-full ${localSource.style.muted ?? true ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: localSource.style.muted ?? true ? "ON" : "OFF" }) ] }) ] }), source.type === "text" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Text Content", theme, children: /* @__PURE__ */ jsx5("textarea", { value: localSource.content, onChange: (e) => handleSourceChange("content", e.target.value), className: `${textInputClass} h-24 resize-y` }) }), /* @__PURE__ */ jsx5("div", { className: "w-full", children: /* @__PURE__ */ jsx5(SettingsInput, { label: "Text Style", theme, children: /* @__PURE__ */ jsx5( "select", { value: localSource.style.textStyle || "none", onChange: (e) => handleStyleChange("textStyle", e.target.value), className: `${textInputClass} text-sm md:text-base font-bold py-2 px-3 h-auto min-h-10 cursor-pointer`, style: { fontSize: "14px" }, children: textStyleOptions.map((style) => /* @__PURE__ */ jsx5("option", { value: style.id, style: { fontSize: "15.5px", padding: "6px" }, children: style.name }, style.id)) } ) }) }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-x-4", children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Font Size", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: localSource.style.fontSize, onChange: (e) => handleStyleChange("fontSize", parseInt(e.target.value)), className: textInputClass }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Font Color", theme, children: /* @__PURE__ */ jsx5("input", { type: "color", value: localSource.style.textColor, onChange: (e) => handleStyleChange("textColor", e.target.value), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` }) }) ] }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-x-4", children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Font Weight", theme, children: /* @__PURE__ */ jsxs5("select", { value: localSource.style.fontWeight, onChange: (e) => handleStyleChange("fontWeight", e.target.value), className: textInputClass, children: [ /* @__PURE__ */ jsx5("option", { value: "normal", children: "Normal" }), /* @__PURE__ */ jsx5("option", { value: "bold", children: "Bold" }) ] }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Text Align", theme, children: /* @__PURE__ */ jsxs5("select", { value: localSource.style.textAlign, onChange: (e) => handleStyleChange("textAlign", e.target.value), className: textInputClass, children: [ /* @__PURE__ */ jsx5("option", { value: "left", children: "Left" }), /* @__PURE__ */ jsx5("option", { value: "center", children: "Center" }), /* @__PURE__ */ jsx5("option", { value: "right", children: "Right" }) ] }) }) ] }) ] }), source.type === "emoji" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsxs5("div", { className: `mb-3 p-2 border rounded-md ${themeClasses.border} ${themeClasses.inputBg}`, children: [ /* @__PURE__ */ jsx5("label", { className: "text-xs font-bold block mb-1 text-gray-400 uppercase tracking-wider", children: "Select Emoji" }), /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-8 gap-1.5 max-h-36 overflow-y-auto p-1 text-center select-none bg-slate-950/20 rounded", children: [ "\u{1F600}", "\u{1F602}", "\u{1F970}", "\u{1F60E}", "\u{1F914}", "\u{1F92B}", "\u{1F525}", "\u2728", "\u{1F389}", "\u{1F680}", "\u{1F4AF}", "\u2764\uFE0F", "\u{1F451}", "\u{1F440}", "\u26A1", "\u{1F4BB}", "\u{1F3AF}", "\u{1F3AE}", "\u{1F47E}", "\u{1F3A8}", "\u{1F3B5}", "\u{1F37F}", "\u{1F355}", "\u{1F354}", "\u{1F369}", "\u{1F308}", "\u{1F431}", "\u{1F436}", "\u{1F981}", "\u{1F47D}", "\u{1F480}", "\u{1F4A9}", "\u{1F4A1}", "\u{1F4E3}", "\u{1F4AC}", "\u2699\uFE0F", "\u26A0\uFE0F", "\u2705", "\u274C", "\u{1F4CC}", "\u{1F381}", "\u{1F511}", "\u{1F512}", "\u{1F514}", "\u{1F4F1}", "\u{1F4C5}", "\u{1F3C6}", "\u{1F48E}" ].map((emoji) => /* @__PURE__ */ jsx5( "button", { onClick: () => handleSourceChange("content", emoji), className: `text-xl p-1 rounded hover:scale-125 hover:bg-indigo-600 transition-all flex items-center justify-center cursor-pointer ${localSource.content === emoji ? "bg-indigo-600 border border-indigo-400 shadow-md ring-1 ring-indigo-500 scale-110" : ""}`, title: emoji, children: emoji }, emoji )) }) ] }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Or enter custom Emoji/Text", theme, children: /* @__PURE__ */ jsx5( "input", { type: "text", value: localSource.content, onChange: (e) => { handleSourceChange("content", e.target.value); }, className: textInputClass, placeholder: "Type emoji here..." } ) }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-x-4", children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Emoji Size", theme, children: /* @__PURE__ */ jsx5( "input", { type: "number", min: "10", max: "500", value: localSource.style.fontSize || 120, onChange: (e) => handleStyleChange("fontSize", parseInt(e.target.value) || 120), className: textInputClass } ) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Alignment", theme, children: /* @__PURE__ */ jsxs5( "select", { value: localSource.style.textAlign || "center", onChange: (e) => handleStyleChange("textAlign", e.target.value), className: textInputClass, children: [ /* @__PURE__ */ jsx5("option", { value: "left", children: "Left" }), /* @__PURE__ */ jsx5("option", { value: "center", children: "Center" }), /* @__PURE__ */ jsx5("option", { value: "right", children: "Right" }) ] } ) }) ] }) ] }), source.type === "gallery" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Slide Duration (sec)", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: localSource.style.slideDuration, onChange: (e) => handleStyleChange("slideDuration", parseInt(e.target.value)), min: "1", className: textInputClass }) }), /* @__PURE__ */ jsxs5("div", { className: `p-2 border ${themeClasses.border} rounded-lg mt-2`, children: [ /* @__PURE__ */ jsx5("h4", { className: "font-bold text-sm mb-2", children: "Manage Images" }), /* @__PURE__ */ jsxs5("div", { className: "flex space-x-2", children: [ /* @__PURE__ */ jsx5("input", { type: "text", placeholder: "Add Image URL", value: galleryUrl, onChange: (e) => setGalleryUrl(e.target.value), className: `flex-grow ${themeClasses.inputBg} border ${theme === "dark" ? "border-gray-600" : "border-gray-300"} rounded px-2 py-1 text-sm` }), /* @__PURE__ */ jsx5("button", { onClick: () => addGalleryImage(galleryUrl), className: "bg-blue-600 hover:bg-blue-500 text-white text-xs px-2 rounded", children: "+" }) ] }), /* @__PURE__ */ jsx5("button", { onClick: () => galleryFileInputRef.current?.click(), className: `w-full ${themeClasses.button} text-xs py-1 mt-2 rounded`, children: "Upload Image" }), /* @__PURE__ */ jsx5("input", { type: "file", ref: galleryFileInputRef, onChange: handleGalleryImageUpload, accept: "image/*", className: "hidden" }), /* @__PURE__ */ jsx5("div", { className: "max-h-40 overflow-y-auto mt-2 space-y-2", children: localSource.content.map((img, index) => /* @__PURE__ */ jsxs5("div", { className: `flex items-center p-1 rounded ${theme === "dark" ? "bg-gray-900/50" : "bg-gray-200/50"}`, children: [ /* @__PURE__ */ jsx5("img", { src: img.url, className: "w-8 h-8 object-cover rounded mr-2", alt: "" }), /* @__PURE__ */ jsx5("span", { className: "text-xs truncate flex-grow", children: img.url.startsWith("data:") ? "Local File" : img.url }), /* @__PURE__ */ jsx5("button", { onClick: () => updateGalleryImage(img.id, { visible: !img.visible }), title: img.visible ? "Hide" : "Show", className: "mx-1", children: img.visible ? "\u{1F441}\uFE0F" : "\u{1F648}" }), /* @__PURE__ */ jsx5("button", { onClick: () => moveGalleryImage(index, "up"), disabled: index === 0, className: "disabled:opacity-20", children: "\u2191" }), /* @__PURE__ */ jsx5("button", { onClick: () => moveGalleryImage(index, "down"), disabled: index === localSource.content.length - 1, className: "disabled:opacity-20", children: "\u2193" }), /* @__PURE__ */ jsx5("button", { onClick: () => removeGalleryImage(img.id), className: "text-red-500 ml-1 text-lg", children: "\xD7" }) ] }, img.id)) }) ] }) ] }), source.type === "code" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsxs5(SettingsInput, { label: "Presets", theme, children: [ /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-2 items-center mb-3", children: [ /* @__PURE__ */ jsx5( "button", { onClick: () => applyPreset("matrixRain"), className: `text-xs px-3 py-1.5 rounded font-medium transition cursor-pointer flex items-center justify-center ${activePreset === "matrixRain" ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: "Matrix" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => applyPreset("snow"), className: `text-xs px-3 py-1.5 rounded font-medium transition cursor-pointer flex items-center justify-center ${activePreset === "snow" ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: "Snow" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => applyPreset("starTunnel"), className: `text-xs px-3 py-1.5 rounded font-medium transition cursor-pointer flex items-center justify-center ${activePreset === "starTunnel" ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: "Stars" } ), customPresets.map((preset) => /* @__PURE__ */ jsxs5("div", { className: "relative group/preset flex items-center", children: [ /* @__PURE__ */ jsx5( "button", { onClick: () => { handleSourceChange("content", { html: preset.html, css: preset.css, js: preset.js }); setActivePreset(preset.id); }, className: `text-xs px-3 py-1.5 rounded font-medium transition cursor-pointer flex items-center justify-center ${activePreset === preset.id ? "bg-purple-600 text-white" : "bg-purple-900/40 text-purple-200 border border-purple-800/50 hover:bg-purple-800/30"}`, children: preset.name } ), /* @__PURE__ */ jsx5( "button", { onClick: (e) => { e.stopPropagation(); const filtered = customPresets.filter((p) => p.id !== preset.id); saveCustomPresets(filtered); if (activePreset === preset.id) { setActivePreset("custom"); } }, className: "absolute -top-1.5 -right-1.5 w-4 h-4 bg-red-600 text-white rounded-full flex items-center justify-center text-[10px] font-bold hover:bg-red-500 shadow z-10 opacity-0 group-hover/preset:opacity-100 transition-opacity cursor-pointer", title: "Delete preset", children: "\xD7" } ) ] }, preset.id)), /* @__PURE__ */ jsx5( "button", { onClick: () => setIsCreatingPreset(true), className: "text-xs px-2.5 py-1.5 rounded font-bold transition cursor-pointer flex items-center justify-center bg-emerald-600 text-white hover:bg-emerald-500", title: "New Preset from Current Code", children: "\u2795" } ), customPresets.some((p) => p.id === activePreset) && /* @__PURE__ */ jsx5( "button", { onClick: () => { const matchedIndex = customPresets.findIndex((p) => p.id === activePreset); if (matchedIndex !== -1) { const updated = [...customPresets]; const currentC = localSource.content; updated[matchedIndex] = { ...updated[matchedIndex], html: currentC.html, css: currentC.css, js: currentC.js }; saveCustomPresets(updated); } }, className: "text-xs px-2.5 py-1.5 rounded font-bold transition cursor-pointer flex items-center justify-center bg-blue-600 text-white hover:bg-blue-500", title: "Save current code to preset", children: "\u{1F4BE}" } ) ] }), isCreatingPreset && /* @__PURE__ */ jsxs5("div", { className: `p-2 border.5 ${themeClasses.border} rounded-lg ${theme === "dark" ? "bg-slate-900/60" : "bg-slate-100/60"} space-y-2 mb-3`, children: [ /* @__PURE__ */ jsx5("span", { className: "text-[10px] font-bold text-gray-400 uppercase tracking-wider block", children: "Preset Name" }), /* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [ /* @__PURE__ */ jsx5( "input", { type: "text", placeholder: "E.g., Rain Matrix...", value: newPresetName, onChange: (e) => setNewPresetName(e.target.value), className: `flex-grow ${themeClasses.inputBg} border ${theme === "dark" ? "border-gray-700" : "border-gray-300"} rounded px-2.5 py-1 text-xs`, autoFocus: true } ), /* @__PURE__ */ jsx5( "button", { onClick: () => { if (!newPresetName.trim()) return; const currentCode = localSource.content; const newPreset = { id: `preset-${Date.now()}`, name: newPresetName.trim(), html: currentCode.html, css: currentCode.css, js: currentCode.js }; const updated = [...customPresets, newPreset]; saveCustomPresets(updated); setActivePreset(newPreset.id); setNewPresetName(""); setIsCreatingPreset(false); }, className: "px-3 py-1 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-bold rounded cursor-pointer", children: "Save" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => { setIsCreatingPreset(false); setNewPresetName(""); }, className: `px-2 py-1 text-xs font-semibold rounded cursor-pointer ${themeClasses.buttonSecondary}`, children: "Cancel" } ) ] }) ] }) ] }), detectedPreset !== "custom" && /* @__PURE__ */ jsxs5("div", { className: `p-3 border ${themeClasses.border} rounded-lg mb-3 ${theme === "dark" ? "bg-slate-900/40" : "bg-slate-100/40"} space-y-3`, children: [ /* @__PURE__ */ jsx5("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsx5("span", { className: "text-[10px] font-bold text-gray-400 uppercase tracking-widest", children: detectedPreset === "matrixRain" ? "Matrix Rain Settings" : detectedPreset === "snow" ? "Snow particle Settings" : "Stars Settings" }) }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsxs5("div", { className: "flex justify-between text-[11px] mb-1", children: [ /* @__PURE__ */ jsx5("span", { className: "text-gray-400 font-semibold", children: "Size" }), /* @__PURE__ */ jsxs5("span", { className: "font-mono text-gray-300 font-bold", children: [ localSource.style.codeSize ?? getDefaultPresetSize(detectedPreset), "px" ] }) ] }), /* @__PURE__ */ jsx5( "input", { type: "range", min: "2", max: detectedPreset === "matrixRain" ? "40" : detectedPreset === "snow" ? "15" : "30", value: localSource.style.codeSize ?? getDefaultPresetSize(detectedPreset), onChange: (e) => handlePresetSliderChange("codeSize", Number(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ) ] }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsxs5("div", { className: "flex justify-between text-[11px] mb-1", children: [ /* @__PURE__ */ jsx5("span", { className: "text-gray-400 font-semibold", children: "Speed" }), /* @__PURE__ */ jsxs5("span", { className: "font-mono text-gray-300 font-bold", children: [ (localSource.style.codeSpeed ?? 1).toFixed(1), "x" ] }) ] }), /* @__PURE__ */ jsx5( "input", { type: "range", min: "0.1", max: "5.0", step: "0.1", value: localSource.style.codeSpeed ?? 1, onChange: (e) => handlePresetSliderChange("codeSpeed", Number(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ) ] }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("span", { className: "block text-[11px] text-gray-400 font-semibold mb-1", children: "Color" }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx5( "input", { type: "color", value: localSource.style.codeColor ?? getDefaultPresetColor(detectedPreset), onChange: (e) => handlePresetSliderChange("codeColor", e.target.value), className: "w-8 h-8 rounded border border-gray-600 cursor-pointer p-0 bg-transparent" } ), /* @__PURE__ */ jsx5( "input", { type: "text", value: localSource.style.codeColor ?? getDefaultPresetColor(detectedPreset), onChange: (e) => handlePresetSliderChange("codeColor", e.target.value), className: `flex-1 ${themeClasses.inputBg} border ${themeClasses.border} rounded px-2 py-1 text-xs font-mono`, placeholder: "#ffffff" } ) ] }) ] }) ] }), /* @__PURE__ */ jsx5(SettingsInput, { label: "HTML", theme, children: /* @__PURE__ */ jsx5("textarea", { value: localSource.content.html, onChange: (e) => handleCodeContentChange("html", e.target.value), className: codeTextareaClass }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "CSS", theme, children: /* @__PURE__ */ jsx5("textarea", { value: localSource.content.css, onChange: (e) => handleCodeContentChange("css", e.target.value), className: codeTextareaClass }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "JavaScript", theme, children: /* @__PURE__ */ jsx5("textarea", { value: localSource.content.js, onChange: (e) => handleCodeContentChange("js", e.target.value), className: codeTextareaClass }) }) ] }), source.type === "visualizer" && /* @__PURE__ */ jsxs5(Fragment3, { children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Visualizer Style", theme, children: /* @__PURE__ */ jsxs5( "select", { value: localSource.style.visualizerStyle || "bars", onChange: (e) => handleStyleChange("visualizerStyle", e.target.value), className: textInputClass, children: [ /* @__PURE__ */ jsx5("option", { value: "bars", children: "Bars (Equalizer)" }), /* @__PURE__ */ jsx5("option", { value: "wave", children: "Wave (Oscilloscope)" }), /* @__PURE__ */ jsx5("option", { value: "blocks", children: "Blocks (Solid Grid)" }) ] } ) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Visualizer Main Color", theme, children: /* @__PURE__ */ jsx5( "input", { type: "color", value: localSource.style.visualizerColor || "#3b82f6", onChange: (e) => handleStyleChange("visualizerColor", e.target.value), className: `w-full ${themeClasses.inputBg} border ${themeClasses.border} rounded h-8 p-0 cursor-pointer` } ) }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mt-3 mb-2", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm font-bold", children: "Inner Glow Accent" }), /* @__PURE__ */ jsx5( "button", { onClick: () => handleStyleChange("innerGlow", !localSource.style.innerGlow), className: `px-3 py-1 text-xs rounded-full ${localSource.style.innerGlow ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: localSource.style.innerGlow ? "ON" : "OFF" } ) ] }), localSource.style.innerGlow && /* @__PURE__ */ jsxs5("div", { className: `pl-3 mt-2 ml-2 border-l-2 ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Glow Color", theme, children: /* @__PURE__ */ jsx5( "input", { type: "color", value: localSource.style.innerGlowColor || "#3b82f6", onChange: (e) => handleStyleChange("innerGlowColor", e.target.value), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` } ) }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Glow Blur (${localSource.style.innerGlowBlur || 15}px)`, theme, children: /* @__PURE__ */ jsx5( "input", { type: "range", min: "0", max: "100", step: "1", value: localSource.style.innerGlowBlur || 15, onChange: (e) => handleStyleChange("innerGlowBlur", parseFloat(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ) }) ] }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mt-3 mb-2", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm font-bold", children: "Transparent Background" }), /* @__PURE__ */ jsx5( "button", { onClick: () => handleStyleChange("visualizerTransparent", localSource.style.visualizerTransparent === false), className: `px-3 py-1 text-xs rounded-full ${localSource.style.visualizerTransparent !== false ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: localSource.style.visualizerTransparent !== false ? "ON" : "OFF" } ) ] }) ] }), source.type === "youtube" && /* @__PURE__ */ jsx5("div", { className: "mb-4", children: /* @__PURE__ */ jsxs5(SettingsInput, { label: "YouTube Connection Proxy", theme, children: [ /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-2 gap-2", children: ["none", "custom"].map((pKey) => /* @__PURE__ */ jsx5( "button", { type: "button", onClick: () => handleStyleChange("youtubeProxy", pKey), className: `py-1 px-2 text-[10px] font-semibold border rounded transition duration-200 capitalize cursor-pointer active:scale-95 ${(source.style.youtubeProxy || "none") === pKey ? "bg-blue-600 border-blue-500 text-white" : themeClasses.buttonSecondary}`, children: pKey === "none" ? "None (Official YouTube)" : "Custom Proxy Host" }, pKey )) }), /* @__PURE__ */ jsx5("p", { className: `text-[10px] mt-1 ${themeClasses.label} opacity-85 leading-tight mb-2`, children: "Use an alternative, YouTube-compatible front-end proxy host (such as a private Invidious server) to bypass connection refusals." }), source.style.youtubeProxy === "custom" && /* @__PURE__ */ jsxs5("div", { className: "mt-2 space-y-2 border-t border-zinc-700/50 pt-2", children: [ /* @__PURE__ */ jsx5("label", { className: `block text-[10px] font-bold uppercase tracking-wider ${themeClasses.label}`, children: "Custom Instance Server" }), /* @__PURE__ */ jsx5( "input", { type: "text", placeholder: "e.g. invidious.example.com", value: source.style.youtubeCustomServer || "", onChange: (e) => handleStyleChange("youtubeCustomServer", e.target.value), className: textInputClass } ), /* @__PURE__ */ jsx5("p", { className: "text-[9px] text-zinc-500 mt-0.5", children: "Enter the precise instance domain name without http/https protocol prefix." }) ] }) ] }) }), /* @__PURE__ */ jsx5("hr", { className: `${themeClasses.border} my-4` }), /* @__PURE__ */ jsx5("h3", { className: "font-bold mb-2", children: "Transform & Style" }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-x-4", children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "X Position", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: Math.round(localSource.style.x), onChange: (e) => handleStyleChange("x", parseInt(e.target.value)), className: textInputClass }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Y Position", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: Math.round(localSource.style.y), onChange: (e) => handleStyleChange("y", parseInt(e.target.value)), className: textInputClass }) }) ] }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-x-4", children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Width", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: Math.round(localSource.style.width), onChange: (e) => handleStyleChange("width", parseInt(e.target.value)), className: textInputClass }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Height", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: Math.round(localSource.style.height), onChange: (e) => handleStyleChange("height", parseInt(e.target.value)), className: textInputClass }) }) ] }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Opacity (${Math.round(localSource.style.opacity * 100)}%)`, theme, children: /* @__PURE__ */ jsx5("input", { type: "range", min: "0", max: "1", step: "0.01", value: localSource.style.opacity, onChange: (e) => handleStyleChange("opacity", parseFloat(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Layer (z-index)", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", value: localSource.style.zIndex, onChange: (e) => handleStyleChange("zIndex", parseInt(e.target.value)), className: textInputClass }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Rotation (${localSource.style.rotation || 0}\xB0)`, theme, children: /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx5( "input", { type: "range", min: "0", max: "360", step: "1", value: localSource.style.rotation || 0, onChange: (e) => handleStyleChange("rotation", parseInt(e.target.value)), className: `flex-grow h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ), /* @__PURE__ */ jsx5( "input", { type: "number", min: "0", max: "360", value: localSource.style.rotation || 0, onChange: (e) => handleStyleChange("rotation", Math.min(360, Math.max(0, parseInt(e.target.value) || 0))), className: `w-14 text-xs p-1 rounded text-center border focus:outline-none focus:ring-1 focus:ring-indigo-500 ${theme === "dark" ? "bg-zinc-800 border-zinc-700 text-white" : "bg-zinc-50 border-zinc-200 text-black"}` } ) ] }) }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-2 mt-2", children: [ /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("span", { className: "text-[10px] font-bold text-gray-400 uppercase tracking-wide block mb-1", children: "Flip Horizontal" }), /* @__PURE__ */ jsxs5( "button", { type: "button", onClick: () => handleStyleChange("scaleX", (localSource.style.scaleX || 1) * -1), className: `w-full py-1.5 text-xs font-semibold rounded hover:bg-opacity-80 transition flex items-center justify-center gap-1 ${(localSource.style.scaleX || 1) === -1 ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: [ "\u2194\uFE0F ", (localSource.style.scaleX || 1) === -1 ? "Flipped" : "Normal" ] } ) ] }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("span", { className: "text-[10px] font-bold text-gray-400 uppercase tracking-wide block mb-1", children: "Flip Vertical" }), /* @__PURE__ */ jsxs5( "button", { type: "button", onClick: () => handleStyleChange("scaleY", (localSource.style.scaleY || 1) * -1), className: `w-full py-1.5 text-xs font-semibold rounded hover:bg-opacity-80 transition flex items-center justify-center gap-1 ${(localSource.style.scaleY || 1) === -1 ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: [ "\u2195\uFE0F ", (localSource.style.scaleY || 1) === -1 ? "Flipped" : "Normal" ] } ) ] }) ] }), /* @__PURE__ */ jsxs5("div", { className: "mt-4 mb-2 space-y-3", children: [ /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-bold text-gray-400 uppercase tracking-wide block mb-1", children: "Position Presets" }), /* @__PURE__ */ jsx5( "button", { onClick: centerSourceOnCanvas, type: "button", className: `w-full py-1.5 text-xs font-semibold rounded ${themeClasses.buttonSecondary} hover:bg-opacity-80 transition flex items-center justify-center gap-1`, children: "\u{1F3AF} Center on Canvas" } ) ] }), /* @__PURE__ */ jsxs5("div", { children: [ /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-bold text-gray-400 uppercase tracking-wide block mb-1", children: "Aspect Ratio & Size Presets" }), /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-5 gap-1", children: [ /* @__PURE__ */ jsx5( "button", { onClick: () => applySizePreset("full"), type: "button", className: `py-1 text-[11px] font-semibold rounded ${themeClasses.buttonSecondary} hover:bg-blue-600 hover:text-white transition`, title: "Full Screen / Fit", children: "Full" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => applySizePreset("240"), type: "button", className: `py-1 text-[11px] font-semibold rounded ${themeClasses.buttonSecondary} hover:bg-opacity-80 transition`, title: "426 x 240 (240p)", children: "240p" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => applySizePreset("480"), type: "button", className: `py-1 text-[11px] font-semibold rounded ${themeClasses.buttonSecondary} hover:bg-opacity-80 transition`, title: "854 x 480 (480p)", children: "480p" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => applySizePreset("720"), type: "button", className: `py-1 text-[11px] font-semibold rounded ${themeClasses.buttonSecondary} hover:bg-opacity-80 transition`, title: "1280 x 720 (720p HD)", children: "720p" } ), /* @__PURE__ */ jsx5( "button", { onClick: () => applySizePreset("1080"), type: "button", className: `py-1 text-[11px] font-semibold rounded ${themeClasses.buttonSecondary} hover:bg-opacity-80 transition`, title: "1920 x 1080 (1080p FHD)", children: "1080p" } ) ] }) ] }) ] }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mt-4", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm font-bold", children: "Box Shadow" }), /* @__PURE__ */ jsx5("button", { onClick: () => handleStyleChange("boxShadow", !localSource.style.boxShadow), className: `px-3 py-1 text-xs rounded-full ${localSource.style.boxShadow ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: localSource.style.boxShadow ? "ON" : "OFF" }) ] }), localSource.style.boxShadow && /* @__PURE__ */ jsxs5("div", { className: `pl-3 mt-2 ml-2 border-l-2 ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Shadow Color", theme, children: /* @__PURE__ */ jsx5("input", { type: "color", value: localSource.style.shadowColor || "#000000", onChange: (e) => handleStyleChange("shadowColor", e.target.value), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: `Shadow Blur (${localSource.style.shadowBlur || 0}px)`, theme, children: /* @__PURE__ */ jsx5("input", { type: "range", min: "0", max: "100", step: "1", value: localSource.style.shadowBlur || 0, onChange: (e) => handleStyleChange("shadowBlur", parseFloat(e.target.value)), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }) ] }), /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mt-4", children: [ /* @__PURE__ */ jsx5("label", { className: "text-sm font-bold", children: "Stroke" }), /* @__PURE__ */ jsx5("button", { onClick: () => handleStyleChange("stroke", !localSource.style.stroke), className: `px-3 py-1 text-xs rounded-full ${localSource.style.stroke ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: localSource.style.stroke ? "ON" : "OFF" }) ] }), localSource.style.stroke && /* @__PURE__ */ jsxs5("div", { className: `pl-3 mt-2 ml-2 border-l-2 ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx5(SettingsInput, { label: "Stroke Color", theme, children: /* @__PURE__ */ jsx5("input", { type: "color", value: localSource.style.strokeColor || "#ffffff", onChange: (e) => handleStyleChange("strokeColor", e.target.value), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` }) }), /* @__PURE__ */ jsx5(SettingsInput, { label: "Stroke Width", theme, children: /* @__PURE__ */ jsx5("input", { type: "number", min: "1", value: localSource.style.strokeWidth || 2, onChange: (e) => handleStyleChange("strokeWidth", parseInt(e.target.value)), className: textInputClass }) }) ] }) ] }) ] }); }; var SettingsPanel_default = SettingsPanel; // ws:scratch/cc/components/FilterPanel.tsx import { useState as useState6, useEffect as useEffect5, useRef as useRef6 } from "https://esm.sh/react@19.2.0"; import { jsx as jsx6, jsxs as jsxs6 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var SettingsInput2 = ({ label, children, className, theme }) => { const themeClasses = themes[theme]; return /* @__PURE__ */ jsxs6("div", { className: `mb-3 ${className}`, children: [ /* @__PURE__ */ jsx6("label", { className: `block text-xs font-bold mb-1 uppercase ${themeClasses.label}`, children: label }), children ] }); }; var shapes = [ { id: "circle", name: "Circle" }, { id: "oval", name: "Oval" }, { id: "triangle", name: "Triangle" }, { id: "diamond", name: "Diamond" }, { id: "star", name: "Star" }, { id: "hexagon", name: "Hexagon" } ]; var FilterPanel = ({ source, onUpdateStyle, onClose, theme }) => { const { id, style } = source; const chromaKey = style.chromaKey || { enabled: false, color: "#00ff00", sensitivity: 20 }; const colorKey = style.colorKey || { enabled: false, color: "#ff00ff", sensitivity: 20 }; const filters = style.filters || { hue: 0, blur: 0, sharpness: 0, shape: "none" }; const themeClasses = themes[theme]; const [position, setPosition] = useState6({ x: 200, y: 150 }); const [width, setWidth] = useState6(480); const [height, setHeight] = useState6(550); const panelRef = useRef6(null); const dragOffset = useRef6({ x: 0, y: 0 }); useEffect5(() => { const x = Math.max(50, (window.innerWidth - width) / 2); const y = Math.max(50, (window.innerHeight - height) / 2); setPosition({ x, y }); }, []); const handleMouseDown = (e) => { if (panelRef.current) { const rect = panelRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const handleMouseMove = (moveEvent) => { const newX = Math.max(0, Math.min(window.innerWidth - width, moveEvent.clientX - dragOffset.current.x)); const newY = Math.max(0, Math.min(window.innerHeight - height, moveEvent.clientY - dragOffset.current.y)); setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } }; const handleResizeMouseDown = (e) => { e.preventDefault(); e.stopPropagation(); const startWidth = width; const startHeight = height; const startX = e.clientX; const startY = e.clientY; const handleMouseMoveResize = (moveEvent) => { const newWidth = Math.max(340, Math.min(1e3, startWidth + (moveEvent.clientX - startX))); const newHeight = Math.max(300, Math.min(window.innerHeight - 40, startHeight + (moveEvent.clientY - startY))); setWidth(newWidth); setHeight(newHeight); }; const handleMouseUpResize = () => { window.removeEventListener("mousemove", handleMouseMoveResize); window.removeEventListener("mouseup", handleMouseUpResize); }; window.addEventListener("mousemove", handleMouseMoveResize); window.addEventListener("mouseup", handleMouseUpResize); }; const handleChromaKeyChange = (updates) => { onUpdateStyle(id, { chromaKey: { ...chromaKey, ...updates } }); }; const handleColorKeyChange = (updates) => { onUpdateStyle(id, { colorKey: { ...colorKey, ...updates } }); }; const handleFilterChange = (updates) => { onUpdateStyle(id, { filters: { ...filters, ...updates } }); }; return /* @__PURE__ */ jsxs6( "div", { ref: panelRef, style: { position: "fixed", left: `${position.x}px`, top: `${position.y}px`, width: `${width}px`, height: `${height}px`, zIndex: 11e4 }, className: `flex flex-col border rounded-xl shadow-2xl overflow-hidden backdrop-blur-md ${themeClasses.bg} bg-opacity-90 ${themeClasses.border} pointer-events-auto`, children: [ /* @__PURE__ */ jsxs6( "div", { onMouseDown: handleMouseDown, className: `flex items-center justify-between px-4 py-3 border-b cursor-move select-none ${themeClasses.border} ${themeClasses.label}`, children: [ /* @__PURE__ */ jsxs6("div", { className: "flex items-center space-x-2", children: [ /* @__PURE__ */ jsx6("span", { className: "text-xl", children: "\u{1F3A8}" }), /* @__PURE__ */ jsxs6("h2", { className: "text-sm font-bold truncate max-w-[200px]", children: [ "Filters: ", source.name ] }) ] }), /* @__PURE__ */ jsx6( "button", { onClick: onClose, title: "Close Filters Window", className: `text-2xl font-light leading-none hover:scale-110 transition-transform cursor-pointer ${theme === "dark" ? "text-gray-400 hover:text-white" : "text-gray-500 hover:text-black"}`, children: "\xD7" } ) ] } ), /* @__PURE__ */ jsxs6("div", { className: "flex-grow overflow-y-auto p-4 space-y-4 text-left", children: [ /* @__PURE__ */ jsxs6("div", { children: [ /* @__PURE__ */ jsx6("h3", { className: "font-bold text-xs uppercase tracking-wider mb-2 text-blue-500", children: "Color Key" }), /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsx6("label", { className: "text-sm", children: "Enable Filter" }), /* @__PURE__ */ jsx6("button", { onClick: () => handleColorKeyChange({ enabled: !colorKey.enabled }), className: `px-3 py-1 text-xs rounded-full cursor-pointer ${colorKey.enabled ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: colorKey.enabled ? "ON" : "OFF" }) ] }), colorKey.enabled && /* @__PURE__ */ jsxs6("div", { className: `pl-3 mt-2 ml-2 border-l-2 ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx6(SettingsInput2, { label: "Key Color", theme, children: /* @__PURE__ */ jsx6("input", { type: "color", value: colorKey.color, onChange: (e) => handleColorKeyChange({ color: e.target.value }), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` }) }), /* @__PURE__ */ jsx6(SettingsInput2, { label: `Sensitivity (${colorKey.sensitivity})`, theme, children: /* @__PURE__ */ jsx6("input", { type: "range", min: "0", max: "100", step: "1", value: colorKey.sensitivity, onChange: (e) => handleColorKeyChange({ sensitivity: parseInt(e.target.value, 10) }), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }) ] }) ] }), /* @__PURE__ */ jsx6("hr", { className: `${themeClasses.border} my-4` }), /* @__PURE__ */ jsxs6("div", { children: [ /* @__PURE__ */ jsx6("h3", { className: "font-bold text-xs uppercase tracking-wider mb-2 text-blue-500", children: "Chroma Key" }), /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsx6("label", { className: "text-sm", children: "Enable Filter" }), /* @__PURE__ */ jsx6("button", { onClick: () => handleChromaKeyChange({ enabled: !chromaKey.enabled }), className: `px-3 py-1 text-xs rounded-full cursor-pointer ${chromaKey.enabled ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: chromaKey.enabled ? "ON" : "OFF" }) ] }), chromaKey.enabled && /* @__PURE__ */ jsxs6("div", { className: `pl-3 mt-2 ml-2 border-l-2 ${themeClasses.border}`, children: [ /* @__PURE__ */ jsx6(SettingsInput2, { label: "Key Color", theme, children: /* @__PURE__ */ jsx6("input", { type: "color", value: chromaKey.color, onChange: (e) => handleChromaKeyChange({ color: e.target.value }), className: `w-full h-8 p-0 cursor-pointer ${themeClasses.inputBg} border ${themeClasses.border} rounded` }) }), /* @__PURE__ */ jsx6(SettingsInput2, { label: `Sensitivity (${chromaKey.sensitivity})`, theme, children: /* @__PURE__ */ jsx6("input", { type: "range", min: "0", max: "100", step: "1", value: chromaKey.sensitivity, onChange: (e) => handleChromaKeyChange({ sensitivity: parseInt(e.target.value, 10) }), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }) ] }) ] }), /* @__PURE__ */ jsx6("hr", { className: `${themeClasses.border} my-4` }), /* @__PURE__ */ jsxs6("div", { children: [ /* @__PURE__ */ jsx6("h3", { className: "font-bold text-xs uppercase tracking-wider mb-2 text-blue-500", children: "Color & Effects" }), /* @__PURE__ */ jsxs6("div", { className: "space-y-3", children: [ /* @__PURE__ */ jsx6(SettingsInput2, { label: `Hue Rotate (${filters.hue}\xB0)`, theme, children: /* @__PURE__ */ jsx6("input", { type: "range", min: "0", max: "360", value: filters.hue, onChange: (e) => handleFilterChange({ hue: parseInt(e.target.value) }), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }), /* @__PURE__ */ jsx6(SettingsInput2, { label: `Blur (${filters.blur}px)`, theme, children: /* @__PURE__ */ jsx6("input", { type: "range", min: "0", max: "50", value: filters.blur, onChange: (e) => handleFilterChange({ blur: parseInt(e.target.value) }), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }), /* @__PURE__ */ jsx6(SettingsInput2, { label: `Sharpness (${filters.sharpness}%)`, theme, children: /* @__PURE__ */ jsx6("input", { type: "range", min: "0", max: "100", value: filters.sharpness, onChange: (e) => handleFilterChange({ sharpness: parseInt(e.target.value) }), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` }) }) ] }) ] }), /* @__PURE__ */ jsx6("hr", { className: `${themeClasses.border} my-4` }), /* @__PURE__ */ jsxs6("div", { children: [ /* @__PURE__ */ jsx6("h3", { className: "font-bold text-xs uppercase tracking-wider mb-2 text-blue-500", children: "Audio Mic Reaction" }), /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsx6("label", { className: "text-sm", children: "Scale & Shake with Audio" }), /* @__PURE__ */ jsx6( "button", { onClick: () => handleFilterChange({ audioReactEnabled: !filters.audioReactEnabled }), className: `px-3 py-1 text-xs rounded-full cursor-pointer transition-colors ${filters.audioReactEnabled ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: filters.audioReactEnabled ? "ON" : "OFF" } ) ] }), filters.audioReactEnabled && /* @__PURE__ */ jsx6("div", { className: `pl-3 mt-2 ml-2 border-l-2 ${themeClasses.border}`, children: /* @__PURE__ */ jsxs6(SettingsInput2, { label: `Mic Sensitivity (${filters.audioSensitivity ?? 5})`, theme, children: [ /* @__PURE__ */ jsx6( "input", { type: "range", min: "1", max: "10", step: "1", value: filters.audioSensitivity ?? 5, onChange: (e) => handleFilterChange({ audioSensitivity: parseInt(e.target.value, 10) }), className: `w-full h-2 rounded-lg appearance-none cursor-pointer ${theme === "dark" ? "bg-gray-700" : "bg-gray-300"}` } ), /* @__PURE__ */ jsx6("span", { className: "text-[10px] text-gray-400 block mt-1", children: "Controls how much the source scales and shakes to live microphone volume." }) ] }) }) ] }), /* @__PURE__ */ jsx6("hr", { className: `${themeClasses.border} my-4` }), /* @__PURE__ */ jsxs6("div", { children: [ /* @__PURE__ */ jsx6("h3", { className: "font-bold text-xs uppercase tracking-wider mb-2 text-blue-500", children: "Shape Filter (Clip-Path)" }), /* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-3 gap-2 pb-6", children: [ /* @__PURE__ */ jsx6("button", { onClick: () => handleFilterChange({ shape: "none" }), className: `py-1.5 text-xs rounded cursor-pointer ${filters.shape === "none" ? "bg-blue-600 text-white" : themeClasses.buttonSecondary}`, children: "Default" }), shapes.map((shape) => /* @__PURE__ */ jsx6( "button", { onClick: () => handleFilterChange({ shape: filters.shape === shape.id ? "none" : shape.id }), className: `py-1.5 text-xs rounded cursor-pointer ${filters.shape === shape.id ? "bg-blue-600 text-white" : themeClasses.button}`, children: shape.name }, shape.id )) ] }) ] }) ] }), /* @__PURE__ */ jsx6("div", { className: `p-4 border-t ${themeClasses.border} flex justify-end space-x-2 bg-black bg-opacity-10`, children: /* @__PURE__ */ jsx6( "button", { onClick: onClose, className: "px-4 py-2 text-xs font-semibold rounded bg-blue-600 hover:bg-blue-500 text-white transition-colors cursor-pointer", children: "Done" } ) }), /* @__PURE__ */ jsx6( "div", { onMouseDown: handleResizeMouseDown, className: "absolute bottom-0 right-0 w-6 h-6 cursor-se-resize flex items-end justify-end p-0.5 z-[130000]", title: "Drag to resize panel", children: /* @__PURE__ */ jsx6("svg", { className: "w-3.5 h-3.5 text-gray-400 hover:text-blue-500 transition-colors", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3, d: "M19 19H12M19 13h-4" }) }) } ) ] } ); }; var FilterPanel_default = FilterPanel; // ws:scratch/cc/components/StatsPanel.tsx import { useState as useState7, useEffect as useEffect6, useRef as useRef7 } from "https://esm.sh/react@19.2.0"; import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var formatBytes = (bytes, decimals = 2) => { if (bytes === 0) return "0 Bytes"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; }; var getResolutionDimensions = (res) => { switch (res) { case "240": return { width: 426, height: 240 }; case "480": return { width: 854, height: 480 }; case "720": return { width: 1280, height: 720 }; case "1080": return { width: 1920, height: 1080 }; case "2k": return { width: 2560, height: 1440 }; case "4k": return { width: 3840, height: 2160 }; default: return { width: 1920, height: 1080 }; } }; var appStartTime = Date.now(); var StatsPanel = ({ actionLog, onClearLog, theme, activeScene, scenes = [], canvasResolution, performancePreset = "high", framerateLimit = "uncapped", renderScale = "1.0" }) => { const [fps, setFps] = useState7(0); const [frameTime, setFrameTime] = useState7(0); const [memory, setMemory] = useState7(null); const [importedProfile, setImportedProfile] = useState7(null); const [tickTrigger, setTickTrigger] = useState7(0); const [selectedSceneId, setSelectedSceneId] = useState7(activeScene?.id || ""); const [timeSpent, setTimeSpent] = useState7("0s"); const [isBrowserTelemetryOpen, setIsBrowserTelemetryOpen] = useState7(false); const [isSourceTelemetryOpen, setIsSourceTelemetryOpen] = useState7(false); const [isSystemLogsOpen, setIsSystemLogsOpen] = useState7(false); useEffect6(() => { const updateSec = () => { const seconds = Math.floor((Date.now() - appStartTime) / 1e3); const h = Math.floor(seconds / 3600); const m = Math.floor(seconds % 3600 / 60); const s = seconds % 60; let durationStr = ""; if (h > 0) durationStr += `${h}h `; if (m > 0 || h > 0) durationStr += `${m}m `; durationStr += `${s}s`; setTimeSpent(durationStr); }; updateSec(); const interval = setInterval(updateSec, 1e3); return () => clearInterval(interval); }, []); useEffect6(() => { if (activeScene) { setSelectedSceneId(activeScene.id); } }, [activeScene?.id]); const targetTelemetryScene = scenes && scenes.length > 0 ? scenes.find((s) => s.id === selectedSceneId) || activeScene : activeScene; const frameTimes = useRef7([]); const lastFrameTime = useRef7(performance.now()); const animationFrameRef = useRef7(null); const themeClasses = themes[theme]; const baseRes = getResolutionDimensions(canvasResolution); useEffect6(() => { const animate = (now) => { const limit = framerateLimit === "uncapped" ? 0 : 1e3 / parseInt(framerateLimit, 10); const delta = now - lastFrameTime.current; if (limit > 0 && delta < limit) { animationFrameRef.current = requestAnimationFrame(animate); return; } lastFrameTime.current = now - delta % (limit || 16); frameTimes.current.push(delta); if (frameTimes.current.length > 60) { frameTimes.current.shift(); } const totalFrameTime = frameTimes.current.reduce((acc, time) => acc + time, 0); const averageFrameTime = totalFrameTime / frameTimes.current.length; setFrameTime(averageFrameTime); setFps(1e3 / averageFrameTime); setTickTrigger((prev) => prev + 1); animationFrameRef.current = requestAnimationFrame(animate); }; animationFrameRef.current = requestAnimationFrame(animate); const memoryInterval = setInterval(() => { if ("memory" in performance) { setMemory(performance.memory); } }, 1e3); return () => { if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); clearInterval(memoryInterval); }; }, []); const handleExportLog = () => { const logText = actionLog.map((entry) => `[${new Date(entry.timestamp).toISOString()}] ${entry.message}`).reverse().join("\n"); const blob = new Blob([logText], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `creative-canvas-log-${(/* @__PURE__ */ new Date()).toISOString()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleExportPerformanceProfile = () => { const profile = { type: "creative_canvas_performance_profile", timestamp: (/* @__PURE__ */ new Date()).toISOString(), fps: parseFloat(fps.toFixed(1)), frameTime: parseFloat(frameTime.toFixed(1)), memory: memory ? formatBytes(memory.usedJSHeapSize) : "9.3 MB", activePreset: performancePreset, framerateLimit, renderScale, canvasResolution, totalSources: activeScene?.sources.length || 0 }; const blob = new Blob([JSON.stringify(profile, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `performance-benchmark-${(/* @__PURE__ */ new Date()).getTime()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleImportPerformanceProfile = () => { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.onchange = (e) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { try { const parsed = JSON.parse(event.target?.result); if (parsed.type === "creative_canvas_performance_profile") { setImportedProfile(parsed); alert(`Performance baseline imported! Baseline was ${parsed.fps} FPS @ ${parsed.frameTime}ms Frame Time.`); } else { alert("Invalid file format. Must be a Creative Canvas Performance Profile backup JSON."); } } catch { alert("Corrupt or invalid JSON file."); } }; reader.readAsText(file); } }; input.click(); }; return /* @__PURE__ */ jsxs7("div", { children: [ /* @__PURE__ */ jsx7("div", { className: `p-3 border-b ${themeClasses.border}`, children: /* @__PURE__ */ jsx7("h2", { className: "text-lg font-bold", children: "Stats & Console" }) }), /* @__PURE__ */ jsxs7("div", { className: "p-3 space-y-4", children: [ /* @__PURE__ */ jsxs7("div", { className: `p-3 rounded-lg border ${themeClasses.border} ${themeClasses.inputBg} space-y-3`, children: [ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsx7("h3", { className: "font-bold text-xs uppercase tracking-wide text-zinc-400", children: "UI Preview Benchmarks" }), importedProfile && /* @__PURE__ */ jsx7( "button", { onClick: () => setImportedProfile(null), className: "text-[9px] text-red-500 hover:text-red-400 font-bold bg-transparent border-none cursor-pointer", children: "Clear Comparison \u2715" } ) ] }), /* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-3 gap-2 text-center text-sm", children: [ /* @__PURE__ */ jsxs7("div", { children: [ /* @__PURE__ */ jsx7("div", { className: "font-bold text-green-400 text-base", children: fps.toFixed(1) }), /* @__PURE__ */ jsx7("div", { className: `text-[10px] ${themeClasses.label}`, children: "FPS" }), importedProfile && /* @__PURE__ */ jsxs7("div", { className: `text-[9.5px] font-bold font-mono leading-none mt-1 ${fps >= importedProfile.fps ? "text-green-400" : "text-red-400"}`, children: [ fps >= importedProfile.fps ? "\u25B2" : "\u25BC", " ", (fps - importedProfile.fps).toFixed(1) ] }) ] }), /* @__PURE__ */ jsxs7("div", { children: [ /* @__PURE__ */ jsxs7("div", { className: "font-bold text-yellow-400 text-base", children: [ frameTime.toFixed(1), "ms" ] }), /* @__PURE__ */ jsx7("div", { className: `text-[10px] ${themeClasses.label}`, children: "Frame Time" }), importedProfile && /* @__PURE__ */ jsxs7("div", { className: `text-[9.5px] font-bold font-mono leading-none mt-1 ${frameTime <= importedProfile.frameTime ? "text-green-400" : "text-red-400"}`, children: [ frameTime <= importedProfile.frameTime ? "\u25B2" : "\u25BC", " ", (frameTime - importedProfile.frameTime).toFixed(1), "ms" ] }) ] }), /* @__PURE__ */ jsxs7("div", { children: [ /* @__PURE__ */ jsx7("div", { className: "font-bold text-blue-400 text-base", children: memory ? formatBytes(memory.usedJSHeapSize, 1) : "9.3 MB" }), /* @__PURE__ */ jsx7("div", { className: `text-[10px] ${themeClasses.label}`, children: "Memory" }), importedProfile && /* @__PURE__ */ jsxs7("div", { className: "text-[9.5px] text-zinc-500 font-mono leading-none mt-1 truncate", title: `Base: ${importedProfile.memory}`, children: [ "Base: ", importedProfile.memory ] }) ] }) ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: `p-3 rounded-lg border ${themeClasses.border} ${themeClasses.inputBg} space-y-2.5`, children: [ /* @__PURE__ */ jsxs7( "button", { type: "button", onClick: () => setIsBrowserTelemetryOpen(!isBrowserTelemetryOpen), className: "w-full flex items-center justify-between text-left font-bold text-xs uppercase tracking-wide text-zinc-400 bg-transparent p-0 cursor-pointer focus:outline-none", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F310} Browser Telemetry" }), /* @__PURE__ */ jsx7("span", { className: "text-zinc-500 font-mono text-[10px]", children: isBrowserTelemetryOpen ? "\u25BC" : "\u25B6" }) ] } ), isBrowserTelemetryOpen && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col space-y-2 text-xs md:text-[12.5px] font-mono leading-relaxed text-zinc-350 pt-2 border-t border-zinc-800/30", children: [ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F5A5}\uFE0F Screen:" }), /* @__PURE__ */ jsxs7("span", { className: "text-white font-semibold", children: [ window.screen?.width || 0, "x", window.screen?.height || 0 ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F50C} CPU Cores:" }), /* @__PURE__ */ jsxs7("span", { className: "text-white font-semibold", children: [ navigator.hardwareConcurrency || "N/A", " Threads" ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F4BE} Device Mem:" }), /* @__PURE__ */ jsx7("span", { className: "text-white font-semibold", children: navigator.deviceMemory ? `${navigator.deviceMemory} GB` : "N/A" }) ] }), /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F310} Connection:" }), /* @__PURE__ */ jsx7("span", { className: "text-white font-semibold", children: (() => { const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; const typeStr = conn ? conn.type || conn.effectiveType || "" : ""; const status = typeStr ? `${typeStr.toUpperCase()}` : navigator.onLine ? "WiFi/Ethernet" : "Offline"; return navigator.onLine ? `${status} (Online)` : "Offline"; })() }) ] }), /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F310} Language:" }), /* @__PURE__ */ jsx7("span", { className: "text-white font-semibold", children: navigator.language || "en-US" }) ] }), /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F552} Time Zone:" }), /* @__PURE__ */ jsx7("span", { className: "text-white font-semibold truncate max-w-[150px]", title: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", children: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }) ] }), /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-zinc-805/30 pb-1", children: [ /* @__PURE__ */ jsx7("span", { children: "\u23F1\uFE0F Spent Time:" }), /* @__PURE__ */ jsx7("span", { className: "text-emerald-400 font-semibold", children: timeSpent }) ] }), /* @__PURE__ */ jsxs7("div", { className: "border-t border-zinc-800/65 pt-2 mt-1", children: [ /* @__PURE__ */ jsx7("div", { className: "text-[11px] uppercase text-zinc-500 font-semibold mb-1", children: "\u{1F525} Most Used Features:" }), /* @__PURE__ */ jsx7("ul", { className: "list-disc list-inside space-y-1 text-[11.5px] text-zinc-400", children: (() => { try { const saved = localStorage.getItem("creative_canvas_feature_usages"); if (saved) { const parsedObj = JSON.parse(saved); const sorted = Object.entries(parsedObj).filter(([_, count]) => typeof count === "number" && count > 0).sort((a, b) => b[1] - a[1]); if (sorted.length > 0) { return sorted.slice(0, 3).map(([feature, count]) => /* @__PURE__ */ jsxs7("li", { className: "truncate", children: [ /* @__PURE__ */ jsx7("span", { className: "text-zinc-300", children: feature }), " ", /* @__PURE__ */ jsxs7("span", { className: "text-indigo-400", children: [ "(", count, " counts)" ] }) ] }, feature)); } } } catch { } return /* @__PURE__ */ jsxs7(Fragment4, { children: [ /* @__PURE__ */ jsxs7("li", { children: [ /* @__PURE__ */ jsx7("span", { className: "text-zinc-300", children: "Source Customization" }), " ", /* @__PURE__ */ jsx7("span", { className: "text-indigo-400", children: "(Initial baseline)" }) ] }), /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7("span", { className: "text-zinc-300", children: "Scene Management" }) }) ] }); })() }) ] }), /* @__PURE__ */ jsxs7("div", { className: "truncate border-t border-zinc-800/40 pt-1 text-[11px] text-zinc-500 flex flex-col gap-0.5", title: navigator.userAgent, children: [ /* @__PURE__ */ jsx7("span", { className: "uppercase tracking-wider text-[9px] text-zinc-500 font-semibold", children: "\u{1F916} User Agent:" }), /* @__PURE__ */ jsx7("span", { className: "truncate text-zinc-400 text-[10.5px]", children: navigator.userAgent }) ] }) ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: `p-3 rounded-lg border ${themeClasses.border} ${themeClasses.inputBg} space-y-3`, children: [ /* @__PURE__ */ jsxs7( "button", { type: "button", onClick: () => setIsSourceTelemetryOpen(!isSourceTelemetryOpen), className: "w-full flex items-center justify-between text-left font-bold text-xs uppercase tracking-wide text-indigo-400 bg-transparent p-0 cursor-pointer focus:outline-none", children: [ /* @__PURE__ */ jsx7("span", { className: "flex items-center gap-1.5", children: "\u{1F4CA} Scene Source Telemetry" }), /* @__PURE__ */ jsx7("span", { className: "text-zinc-500 font-mono text-[10px]", children: isSourceTelemetryOpen ? "\u25BC" : "\u25B6" }) ] } ), isSourceTelemetryOpen && /* @__PURE__ */ jsxs7("div", { className: "pt-2 border-t border-zinc-801/20 space-y-3", children: [ scenes && scenes.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1.5 pb-1.5 border-b border-zinc-800/65", children: [ /* @__PURE__ */ jsx7("div", { className: "flex items-center gap-1.5 pb-1 overflow-x-auto overflow-y-hidden max-h-12 scrollbar-none w-full", children: scenes.map((s) => { const isActive = s.id === selectedSceneId; const isCurrentCanvas = activeScene?.id === s.id; return /* @__PURE__ */ jsxs7( "button", { onClick: () => setSelectedSceneId(s.id), title: s.name, className: `px-3 py-1 rounded text-sm transition-all duration-150 flex items-center justify-center gap-1 focus:outline-none whitespace-nowrap cursor-pointer h-7 ${isActive ? "bg-indigo-600 text-white font-bold scale-[1.02]" : "bg-zinc-800 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/80"}`, children: [ /* @__PURE__ */ jsx7("span", { children: s.icon || "\u{1F3AC}" }), isCurrentCanvas && /* @__PURE__ */ jsx7("span", { className: "w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse border border-green-600", title: "Active on Canvas" }) ] }, s.id ); }) }), (() => { const selScene = scenes.find((s) => s.id === selectedSceneId); if (!selScene) return null; return /* @__PURE__ */ jsxs7("div", { className: "text-[11px] text-zinc-400 bg-zinc-950/40 px-2 py-1 rounded flex items-center gap-1 font-mono", children: [ /* @__PURE__ */ jsx7("span", { children: "Selected:" }), /* @__PURE__ */ jsxs7("span", { className: "text-white font-semibold font-sans", children: [ selScene.icon, " ", selScene.name ] }), activeScene?.id === selScene.id && /* @__PURE__ */ jsx7("span", { className: "text-[9px] bg-green-950 text-green-300 border border-green-800 px-1 rounded ml-auto", children: "Active" }) ] }); })() ] }), targetTelemetryScene && targetTelemetryScene.sources && targetTelemetryScene.sources.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "space-y-2 max-h-56 overflow-y-auto pr-1", children: targetTelemetryScene.sources.map((src) => { const chromaOn = src.style.chromaKey?.enabled; const colorKeyOn = src.style.colorKey?.enabled; return /* @__PURE__ */ jsxs7("div", { className: "bg-zinc-950/45 border border-zinc-800 rounded p-2 text-[11px] leading-normal font-mono space-y-1 hover:border-zinc-700 transition-colors", children: [ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsxs7("span", { className: "font-bold text-white flex items-center gap-1", children: [ src.type === "image" && "\u{1F5BC}\uFE0F", src.type === "video" && "\u{1F3A5}", src.type === "text" && "\u270D\uFE0F", src.type === "iframe" && "\u{1F310}", src.type === "color" && "\u{1F3A8}", src.type === "gallery" && "\u{1F39E}\uFE0F", src.type === "code" && "\u{1F4BB}", src.type === "paint" && "\u{1F58C}\uFE0F", src.type === "camera" && "\u{1F4F7}", src.type === "capture" && "\u{1F5A5}\uFE0F", src.type === "emoji" && "\u{1F600}", src.type === "visualizer" && "\u{1F4CA}", src.name ] }), /* @__PURE__ */ jsxs7("div", { className: "flex gap-1.5 flex-shrink-0", children: [ /* @__PURE__ */ jsx7("span", { className: `px-1 rounded text-[9px] ${src.visible ? "bg-green-950 text-green-300 border border-green-800" : "bg-red-950 text-red-300 border border-red-800"}`, children: src.visible ? "Visible" : "Hidden" }), /* @__PURE__ */ jsx7("span", { className: `px-1 rounded text-[9px] ${src.locked ? "bg-yellow-950 text-yellow-300 border border-yellow-800" : "bg-blue-950 text-blue-300 border border-blue-800"}`, children: src.locked ? "Locked" : "Unlocked" }) ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-2 gap-x-2 text-zinc-400 text-[10px]", children: [ /* @__PURE__ */ jsxs7("div", { children: [ "Pos: X:", Math.round(src.style.x), ", Y:", Math.round(src.style.y) ] }), /* @__PURE__ */ jsxs7("div", { children: [ "Size: ", Math.round(src.style.width), "x", Math.round(src.style.height), "px" ] }), /* @__PURE__ */ jsxs7("div", { children: [ "Z-Layer: ", src.style.zIndex ] }), /* @__PURE__ */ jsxs7("div", { children: [ "Type: ", /* @__PURE__ */ jsx7("span", { className: "text-zinc-300", children: src.type.toUpperCase() }) ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: "text-[9.5px] text-zinc-500 border-t border-zinc-900 pt-1 mt-1 flex flex-wrap gap-x-2 gap-y-0.5", children: [ src.type === "text" && /* @__PURE__ */ jsxs7("span", { children: [ "Size: ", src.style.fontSize, "px | Font: ", src.style.fontFamily || "Default" ] }), src.type === "emoji" && /* @__PURE__ */ jsxs7("span", { children: [ "Size: ", src.style.fontSize || 120, "px | Align: ", src.style.textAlign || "center" ] }), src.type === "video" && /* @__PURE__ */ jsxs7("span", { children: [ "Vol: ", Math.round((src.style.volume ?? 1) * 100), "% | Speed: ", src.style.playbackRate ?? 1, "x | Muted: ", src.style.muted ? "Yes" : "No" ] }), src.type === "gallery" && /* @__PURE__ */ jsxs7("span", { children: [ "Slide: ", src.style.slideDuration, "s | Items: ", Array.isArray(src.content) ? src.content.length : 0 ] }), src.type === "iframe" && /* @__PURE__ */ jsxs7("span", { className: "truncate max-w-[200px]", title: src.content, children: [ "Url: ", src.content ] }), ["video", "camera", "capture"].includes(src.type) && window.mediaFpsStats?.[src.id] && /* @__PURE__ */ jsxs7("span", { className: "text-yellow-400 font-bold px-1 rounded bg-yellow-950/40", children: [ "\u2605 Live FPS: ", (window.mediaFpsStats[src.id].fps || 0).toFixed(1) ] }), chromaOn && /* @__PURE__ */ jsxs7("span", { className: "text-green-400 font-bold", children: [ "\u25CF ChromaKey (", src.style.chromaKey?.color, ")" ] }) ] }) ] }, src.id); }) }) : /* @__PURE__ */ jsx7("p", { className: "text-gray-500 text-[10px] italic", children: "No sources in selected scene." }) ] }) ] }), /* @__PURE__ */ jsxs7("div", { className: `p-3 rounded-lg border ${themeClasses.border} ${themeClasses.inputBg} space-y-3`, children: [ /* @__PURE__ */ jsxs7( "button", { type: "button", onClick: () => setIsSystemLogsOpen(!isSystemLogsOpen), className: "w-full flex items-center justify-between text-left font-bold text-xs uppercase tracking-wide text-zinc-400 bg-transparent p-0 cursor-pointer focus:outline-none", children: [ /* @__PURE__ */ jsx7("span", { children: "\u{1F4DD} System Logs" }), /* @__PURE__ */ jsx7("span", { className: "text-zinc-500 font-mono text-[10px]", children: isSystemLogsOpen ? "\u25BC" : "\u25B6" }) ] } ), isSystemLogsOpen && /* @__PURE__ */ jsxs7("div", { className: "pt-2 border-t border-zinc-800/20 space-y-3", children: [ /* @__PURE__ */ jsx7("div", { className: "h-28 border border-zinc-800/50 rounded p-2 overflow-y-auto text-[11px] font-mono mb-2", children: actionLog.length > 0 ? actionLog.map((entry, idx) => /* @__PURE__ */ jsxs7("div", { className: "flex leading-relaxed", children: [ /* @__PURE__ */ jsx7("span", { className: "text-gray-500 mr-2", children: new Date(entry.timestamp).toLocaleTimeString() }), /* @__PURE__ */ jsx7("span", { className: "flex-1 text-zinc-300", children: entry.message }) ] }, idx)) : /* @__PURE__ */ jsx7("p", { className: "text-gray-500 text-[10px]", children: "No actions logged yet." }) }), /* @__PURE__ */ jsxs7("div", { className: "flex justify-end space-x-2", children: [ /* @__PURE__ */ jsx7("button", { onClick: onClearLog, className: `text-xs px-2.5 py-1.5 rounded font-medium ${themeClasses.buttonSecondary}`, children: "Clear Logs" }), /* @__PURE__ */ jsx7("button", { onClick: handleExportLog, className: `text-xs px-2.5 py-1.5 rounded font-medium ${themeClasses.button}`, children: "Export Full Log" }) ] }) ] }) ] }) ] }) ] }); }; var StatsPanel_default = StatsPanel; // ws:scratch/cc/components/UI.tsx import { Fragment as Fragment5, jsx as jsx8, jsxs as jsxs8 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var themes = { light: { bg: "bg-white", text: "text-gray-800", border: "border-gray-200", inputBg: "bg-gray-100", label: "text-gray-500", button: "bg-gray-200 hover:bg-gray-300", itemHover: "hover:bg-gray-100", icon: "text-gray-500 hover:text-black", buttonSecondary: "bg-gray-300" }, dark: { bg: "bg-gray-800", text: "text-white", border: "border-gray-700", inputBg: "bg-gray-900", label: "text-gray-400", button: "bg-gray-700 hover:bg-gray-600", itemHover: "hover:bg-gray-700/70", icon: "text-gray-400 hover:text-white", buttonSecondary: "bg-gray-600" }, crimson: { bg: "bg-red-900", text: "text-red-100", border: "border-red-700", inputBg: "bg-red-950", label: "text-red-300", button: "bg-red-800 hover:bg-red-700", itemHover: "hover:bg-red-800/70", icon: "text-red-300 hover:text-white", buttonSecondary: "bg-red-800" }, metallic: { bg: "bg-slate-800", text: "text-slate-200", border: "border-slate-700", inputBg: "bg-slate-900", label: "text-slate-400", button: "bg-slate-700 hover:bg-slate-600", itemHover: "hover:bg-slate-700/70", icon: "text-slate-400 hover:text-white", buttonSecondary: "bg-slate-700" }, lime: { bg: "bg-lime-950", text: "text-lime-200", border: "border-lime-800", inputBg: "bg-lime-900/50", label: "text-lime-400", button: "bg-lime-800 hover:bg-lime-700", itemHover: "hover:bg-lime-800/70", icon: "text-lime-300 hover:text-white", buttonSecondary: "bg-lime-800" }, purple: { bg: "bg-purple-900", text: "text-purple-200", border: "border-purple-700", inputBg: "bg-purple-950", label: "text-purple-400", button: "bg-purple-800 hover:bg-purple-700", itemHover: "hover:bg-purple-800/70", icon: "text-purple-300 hover:text-white", buttonSecondary: "bg-purple-800" }, lemon: { bg: "bg-yellow-950", text: "text-yellow-200", border: "border-yellow-800", inputBg: "bg-yellow-900/50", label: "text-yellow-400", button: "bg-yellow-800 hover:bg-yellow-700", itemHover: "hover:bg-yellow-800/70", icon: "text-yellow-300 hover:text-white", buttonSecondary: "bg-yellow-800" }, silverado: { bg: "bg-zinc-800", text: "text-zinc-200", border: "border-zinc-700", inputBg: "bg-zinc-900", label: "text-zinc-400", button: "bg-zinc-700 hover:bg-zinc-600", itemHover: "hover:bg-zinc-700/70", icon: "text-zinc-400 hover:text-white", buttonSecondary: "bg-zinc-700" }, orange: { bg: "bg-orange-950", text: "text-orange-200", border: "border-orange-800", inputBg: "bg-orange-900/50", label: "text-orange-400", button: "bg-orange-800 hover:bg-orange-700", itemHover: "hover:bg-orange-800/70", icon: "text-orange-300 hover:text-white", buttonSecondary: "bg-orange-800" } }; var WelcomeOverlay = ({ onClose, theme }) => { const themeClasses = themes[theme]; const [position, setPosition] = useState8({ x: 0, y: 0 }); const modalRef = useRef8(null); const dragOffset = useRef8({ x: 0, y: 0 }); useEffect7(() => { const x = Math.max(20, (window.innerWidth - 672) / 2); const y = Math.max(20, (window.innerHeight - 560) / 2); setPosition({ x, y }); }, []); const handleMouseDown = (e) => { if (e.target.closest("button, details, summary, a")) return; if (modalRef.current) { const rect = modalRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const handleMouseMove = (moveEvent) => { const newX = Math.max(0, Math.min(window.innerWidth - 200, moveEvent.clientX - dragOffset.current.x)); const newY = Math.max(0, Math.min(window.innerHeight - 150, moveEvent.clientY - dragOffset.current.y)); setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } }; return /* @__PURE__ */ jsxs8(Fragment5, { children: [ /* @__PURE__ */ jsx8("div", { className: "fixed inset-0 bg-black/80 z-[190000] pointer-events-auto transition-opacity duration-300", onClick: onClose }), /* @__PURE__ */ jsxs8( "div", { ref: modalRef, style: { position: "fixed", left: `${position.x}px`, top: `${position.y}px`, zIndex: 2e5 }, className: `rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] md:max-h-[85vh] p-6 border text-left flex flex-col overflow-hidden pointer-events-auto ${themeClasses.bg} ${themeClasses.border}`, children: [ /* @__PURE__ */ jsxs8( "div", { onMouseDown: handleMouseDown, className: "cursor-move select-none border-b border-gray-700/30 pb-3 mb-4 flex items-center justify-between", children: [ /* @__PURE__ */ jsx8("h1", { className: "text-xl font-bold text-blue-500 pr-8", children: "Welcome to your Creative Canvas!" }), /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-zinc-500 font-mono", children: "\u2725 Drag Header" }), /* @__PURE__ */ jsx8("button", { onClick: onClose, className: `text-2xl font-light hover:scale-110 transition-transform cursor-pointer ${theme === "dark" ? "text-gray-400 hover:text-white" : "text-gray-500 hover:text-black"}`, children: "\xD7" }) ] }) ] } ), /* @__PURE__ */ jsxs8("div", { className: "flex-grow overflow-y-auto pr-2 space-y-4", children: [ /* @__PURE__ */ jsx8("p", { className: `text-sm md:text-base leading-relaxed ${themeClasses.text}`, children: "This is your personal digital studio, a versatile platform for creating dynamic visual scenes. Whether you're a streamer, presenter, educator, or just a creative soul, this canvas is your playground." }), /* @__PURE__ */ jsxs8("div", { className: "space-y-2 mt-4", children: [ /* @__PURE__ */ jsxs8("details", { className: "group border-b border-gray-700/40 pb-2 cursor-pointer", children: [ /* @__PURE__ */ jsxs8("summary", { className: "font-semibold text-blue-500 hover:text-blue-400 select-none flex justify-between items-center outline-none", children: [ /* @__PURE__ */ jsx8("span", { children: "\u{1F3AC} Compose Scenes" }), /* @__PURE__ */ jsx8("span", { className: "text-xs transition-transform group-open:rotate-180", children: "\u25BC" }) ] }), /* @__PURE__ */ jsx8("p", { className: `mt-2 text-sm pl-4 leading-relaxed ${theme === "dark" ? "text-gray-300" : "text-gray-600"}`, children: "Build multiple scenes, each with its own unique layout of sources. Seamlessly switch between setups." }) ] }), /* @__PURE__ */ jsxs8("details", { className: "group border-b border-gray-700/40 pb-2 cursor-pointer", children: [ /* @__PURE__ */ jsxs8("summary", { className: "font-semibold text-blue-500 hover:text-blue-400 select-none flex justify-between items-center outline-none", children: [ /* @__PURE__ */ jsx8("span", { children: "\u{1F4F8} Add Rich Media" }), /* @__PURE__ */ jsx8("span", { className: "text-xs transition-transform group-open:rotate-180", children: "\u25BC" }) ] }), /* @__PURE__ */ jsx8("p", { className: `mt-2 text-sm pl-4 leading-relaxed ${theme === "dark" ? "text-gray-300" : "text-gray-600"}`, children: "Incorporate images, videos, live camera feeds, screen captures, and even live websites using URL links." }) ] }), /* @__PURE__ */ jsxs8("details", { className: "group border-b border-gray-700/40 pb-2 cursor-pointer", children: [ /* @__PURE__ */ jsxs8("summary", { className: "font-semibold text-blue-500 hover:text-blue-400 select-none flex justify-between items-center outline-none", children: [ /* @__PURE__ */ jsx8("span", { children: "\u2728 Layer & Animate" }), /* @__PURE__ */ jsx8("span", { className: "text-xs transition-transform group-open:rotate-180", children: "\u25BC" }) ] }), /* @__PURE__ */ jsx8("p", { className: `mt-2 text-sm pl-4 leading-relaxed ${theme === "dark" ? "text-gray-300" : "text-gray-600"}`, children: "Arrange sources in layers, apply scale/rotation transitions, and use green-screen chroma key effects." }) ] }), /* @__PURE__ */ jsxs8("details", { className: "group border-b border-gray-700/40 pb-2 cursor-pointer", children: [ /* @__PURE__ */ jsxs8("summary", { className: "font-semibold text-blue-500 hover:text-blue-400 select-none flex justify-between items-center outline-none", children: [ /* @__PURE__ */ jsx8("span", { children: "\u{1F3A5} Live Production" }), /* @__PURE__ */ jsx8("span", { className: "text-xs transition-transform group-open:rotate-180", children: "\u25BC" }) ] }), /* @__PURE__ */ jsx8("p", { className: `mt-2 text-sm pl-4 leading-relaxed ${theme === "dark" ? "text-gray-300" : "text-gray-600"}`, children: "Seamlessly switch between scenes with smooth transitions, perfect for streaming, presentations, or local recording." }) ] }), /* @__PURE__ */ jsxs8("details", { className: "group border-b border-gray-700/40 pb-2 cursor-pointer", children: [ /* @__PURE__ */ jsxs8("summary", { className: "font-semibold text-blue-500 hover:text-blue-400 select-none flex justify-between items-center outline-none", children: [ /* @__PURE__ */ jsx8("span", { children: "\u{1F4BB} Interactive Design" }), /* @__PURE__ */ jsx8("span", { className: "text-xs transition-transform group-open:rotate-180", children: "\u25BC" }) ] }), /* @__PURE__ */ jsx8("p", { className: `mt-2 text-sm pl-4 leading-relaxed ${theme === "dark" ? "text-gray-300" : "text-gray-600"}`, children: "Add custom code (HTML/CSS/JS) to create fully custom widgets, particles, visualizers, or tools right on the canvas." }) ] }), /* @__PURE__ */ jsxs8("details", { className: "group border-b border-gray-700/40 pb-2 cursor-pointer", children: [ /* @__PURE__ */ jsxs8("summary", { className: "font-semibold text-blue-500 hover:text-blue-400 select-none flex justify-between items-center outline-none", children: [ /* @__PURE__ */ jsx8("span", { children: "\u{1F4C2} Save & Share" }), /* @__PURE__ */ jsx8("span", { className: "text-xs transition-transform group-open:rotate-180", children: "\u25BC" }) ] }), /* @__PURE__ */ jsx8("p", { className: `mt-2 text-sm pl-4 leading-relaxed ${theme === "dark" ? "text-gray-300" : "text-gray-600"}`, children: "Export your entire project setup or individual scene sources to a JSON file, and load them back anytime." }) ] }) ] }), /* @__PURE__ */ jsx8("p", { className: `text-xs md:text-sm mt-4 leading-relaxed ${themeClasses.text}`, children: 'Dive in and start creating! Hide this UI at any time using the toggle in the bottom right to enter "performance mode" and see your canvas in full brilliance.' }) ] }), /* @__PURE__ */ jsxs8("div", { className: "text-center mt-6 pt-3 border-t border-gray-700/40 flex flex-col sm:flex-row items-center justify-center gap-4 shrink-0", children: [ /* @__PURE__ */ jsx8("button", { onClick: onClose, className: "bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-lg transition-colors w-full sm:w-auto cursor-pointer", children: "Got it, let's create!" }), /* @__PURE__ */ jsxs8( "a", { href: "https://www.paypal.com/paypalme/jordanalbiar", target: "_blank", rel: "noopener noreferrer", className: "bg-yellow-500 hover:bg-yellow-400 text-black font-bold py-2 px-6 rounded-lg transition-colors flex items-center justify-center gap-2 w-full sm:w-auto", children: [ /* @__PURE__ */ jsx8("span", { children: "\u2615" }), /* @__PURE__ */ jsx8("span", { children: "Buy me a coffee" }) ] } ) ] }) ] } ) ] }); }; var AddSceneModal = ({ onConfirm, onClose, theme }) => { const [name, setName] = useState8(""); const [icon, setIcon] = useState8("\u{1F3AC}"); const [selectedCategory, setSelectedCategory] = useState8("Popular"); const themeClasses = themes[theme]; const textInputClass = `w-full ${themeClasses.inputBg} border ${themeClasses.border} rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`; const [position, setPosition] = useState8({ x: 0, y: 0 }); const modalRef = useRef8(null); const dragOffset = useRef8({ x: 0, y: 0 }); useEffect7(() => { const x = Math.max(20, (window.innerWidth - 320) / 2); const y = Math.max(20, (window.innerHeight - 440) / 2); setPosition({ x, y }); }, []); const handleMouseDown = (e) => { if (e.target.closest("button, input")) return; if (modalRef.current) { const rect = modalRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const handleMouseMove = (moveEvent) => { const newX = Math.max(0, Math.min(window.innerWidth - 180, moveEvent.clientX - dragOffset.current.x)); const newY = Math.max(0, Math.min(window.innerHeight - 120, moveEvent.clientY - dragOffset.current.y)); setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } }; const handleSubmit = (e) => { e.preventDefault(); if (name.trim()) { onConfirm(name.trim(), icon); } }; return /* @__PURE__ */ jsxs8(Fragment5, { children: [ /* @__PURE__ */ jsx8("div", { className: "fixed inset-0 bg-black/70 z-[190000] pointer-events-auto", onClick: onClose }), /* @__PURE__ */ jsxs8( "div", { ref: modalRef, style: { position: "fixed", left: `${position.x}px`, top: `${position.y}px`, zIndex: 2e5 }, className: `rounded-lg shadow-2xl max-w-xs w-full p-5 border pointer-events-auto ${themeClasses.bg} ${themeClasses.border}`, children: [ /* @__PURE__ */ jsxs8( "div", { onMouseDown: handleMouseDown, className: "cursor-move select-none border-b border-gray-700/30 pb-2 mb-3 flex items-center justify-between", children: [ /* @__PURE__ */ jsxs8("h2", { className: "text-sm font-bold uppercase tracking-wider text-indigo-400 flex items-center gap-1.5", children: [ /* @__PURE__ */ jsx8("span", { children: "\u2728" }), " Create New Scene" ] }), /* @__PURE__ */ jsx8("span", { className: "text-[9px] text-zinc-500 font-mono", children: "\u2725 Drag" }) ] } ), /* @__PURE__ */ jsxs8("form", { onSubmit: handleSubmit, children: [ /* @__PURE__ */ jsxs8("div", { className: "mb-3", children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1 uppercase ${themeClasses.label}`, children: "Scene Name" }), /* @__PURE__ */ jsx8( "input", { type: "text", value: name, onChange: (e) => setName(e.target.value), className: textInputClass, placeholder: "My Awesome Scene", autoFocus: true } ) ] }), /* @__PURE__ */ jsxs8("div", { className: "mb-4", children: [ /* @__PURE__ */ jsxs8("div", { className: "flex justify-between items-center mb-1", children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold uppercase ${themeClasses.label}`, children: "Icon Emoji Selector" }), /* @__PURE__ */ jsx8("span", { className: "text-sm font-bold px-1.5 py-0.5 bg-zinc-800/80 text-white rounded border border-zinc-700 font-mono min-w-[28px] text-center select-none", children: icon }) ] }), /* @__PURE__ */ jsx8("div", { className: "flex gap-1 mb-2 overflow-x-auto pb-1 text-[9.5px]", children: Object.keys(EMOJI_CATEGORIES).map((cat) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => setSelectedCategory(cat), className: `px-1.5 py-0.5 rounded shrink-0 transition-colors ${selectedCategory === cat ? "bg-indigo-650 text-white font-bold" : "bg-zinc-800/45 text-zinc-400 hover:text-zinc-200"}`, children: cat }, cat )) }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-6 gap-1 max-h-[100px] overflow-y-auto p-1.5 bg-zinc-900/60 rounded border border-zinc-800 mb-2", children: EMOJI_CATEGORIES[selectedCategory].map((emoji) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => setIcon(emoji), className: `text-lg p-1 rounded hover:bg-zinc-800 transition-all ${icon === emoji ? "bg-blue-600 scale-110 text-white shadow-sm" : ""}`, children: emoji }, emoji )) }), /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx8("label", { className: "block text-[10px] text-zinc-400", children: "Custom User Browser Emoji:" }), /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx8( "input", { type: "text", maxLength: 8, value: icon, onChange: (e) => setIcon(e.target.value), className: `${textInputClass} text-center font-bold text-base py-0.5 w-14`, placeholder: "\u{1F3AC}" } ), /* @__PURE__ */ jsx8("span", { className: "text-[9px] text-zinc-500 leading-tight", children: "Paste or type any emoji supported by your OS / browser." }) ] }) ] }) ] }), /* @__PURE__ */ jsxs8("div", { className: "flex justify-end space-x-2 border-t border-zinc-700/30 pt-3 mt-1", children: [ /* @__PURE__ */ jsx8("button", { type: "button", onClick: onClose, className: `px-4 py-2 text-sm rounded ${themeClasses.buttonSecondary}`, children: "Cancel" }), /* @__PURE__ */ jsx8("button", { type: "submit", disabled: !name.trim(), className: "px-4 py-2 text-sm rounded bg-blue-600 hover:bg-blue-500 text-white disabled:bg-blue-800 disabled:cursor-not-allowed", children: "Create" }) ] }) ] }) ] } ) ] }); }; var EMOJI_CATEGORIES = { "Popular": ["\u{1F3AC}", "\u{1F3AE}", "\u{1F4AC}", "\u{1F4F0}", "\u{1F399}\uFE0F", "\u{1F5BC}\uFE0F", "\u2728", "\u{1F3A5}", "\u{1F4A1}", "\u{1F3C6}", "\u2B50", "\u2764\uFE0F", "\u{1F525}", "\u{1F4BB}", "\u{1F680}", "\u{1F534}", "\u{1F7E2}", "\u{1F535}"], "Faces": ["\u{1F600}", "\u{1F605}", "\u{1F602}", "\u{1F642}", "\u{1F609}", "\u{1F60A}", "\u{1F607}", "\u{1F60D}", "\u{1F60E}", "\u{1F973}", "\u{1F914}", "\u{1F92B}", "\u{1F4A9}", "\u{1F921}", "\u{1F47D}", "\u{1F916}", "\u{1F47E}"], "Activities": ["\u26BD", "\u{1F3C0}", "\u{1F3AE}", "\u{1F3B3}", "\u{1F3AF}", "\u{1F3B0}", "\u{1F3A8}", "\u{1F3AD}", "\u{1F3A4}", "\u{1F3A7}", "\u{1F3B8}", "\u{1F3B9}", "\u{1F37F}", "\u{1F39F}\uFE0F", "\u{1F3C5}"], "Objects": ["\u{1F4F1}", "\u{1F4BB}", "\u{1F4FA}", "\u{1F4FB}", "\u{1F4F7}", "\u{1F4A1}", "\u{1F526}", "\u{1F511}", "\u{1F4E6}", "\u{1F381}", "\u{1F388}", "\u2709\uFE0F", "\u270F\uFE0F", "\u{1F6E1}\uFE0F", "\u{1F6E0}\uFE0F"], "Symbols": ["\u2753", "\u2757", "\u26A0\uFE0F", "\u2B50", "\u2764\uFE0F", "\u{1F494}", "\u26A1", "\u{1F300}", "\u{1F4A4}", "\u{1F3B5}", "\u{1F4AF}", "\u2705", "\u274C", "\u26AA", "\u26AB"] }; var EditSceneModal = ({ scene, onConfirm, onClose, theme }) => { const [name, setName] = useState8(scene.name); const [icon, setIcon] = useState8(scene.icon || "\u{1F3AC}"); const [selectedCategory, setSelectedCategory] = useState8("Popular"); const themeClasses = themes[theme]; const textInputClass = `w-full ${themeClasses.inputBg} border ${themeClasses.border} rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`; const [position, setPosition] = useState8({ x: 0, y: 0 }); const modalRef = useRef8(null); const dragOffset = useRef8({ x: 0, y: 0 }); useEffect7(() => { const x = Math.max(20, (window.innerWidth - 320) / 2); const y = Math.max(20, (window.innerHeight - 440) / 2); setPosition({ x, y }); }, []); const handleMouseDown = (e) => { if (e.target.closest("button, input")) return; if (modalRef.current) { const rect = modalRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const handleMouseMove = (moveEvent) => { const newX = Math.max(0, Math.min(window.innerWidth - 180, moveEvent.clientX - dragOffset.current.x)); const newY = Math.max(0, Math.min(window.innerHeight - 120, moveEvent.clientY - dragOffset.current.y)); setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } }; const handleSubmit = (e) => { e.preventDefault(); if (name.trim()) { onConfirm(scene.id, name.trim(), icon); } }; return /* @__PURE__ */ jsxs8(Fragment5, { children: [ /* @__PURE__ */ jsx8("div", { className: "fixed inset-0 bg-black/70 z-[190000] pointer-events-auto", onClick: onClose }), /* @__PURE__ */ jsxs8( "div", { ref: modalRef, style: { position: "fixed", left: `${position.x}px`, top: `${position.y}px`, zIndex: 2e5 }, className: `rounded-lg shadow-2xl max-w-xs w-full p-5 border pointer-events-auto ${themeClasses.bg} ${themeClasses.border}`, children: [ /* @__PURE__ */ jsxs8( "div", { onMouseDown: handleMouseDown, className: "cursor-move select-none border-b border-gray-700/30 pb-2 mb-3 flex items-center justify-between", children: [ /* @__PURE__ */ jsxs8("h2", { className: "text-sm font-bold uppercase tracking-wider text-indigo-400 flex items-center gap-1.5", children: [ /* @__PURE__ */ jsx8("span", { children: "\u270F\uFE0F" }), " Edit Scene" ] }), /* @__PURE__ */ jsx8("span", { className: "text-[9px] text-zinc-500 font-mono", children: "\u2725 Drag" }) ] } ), /* @__PURE__ */ jsxs8("form", { onSubmit: handleSubmit, children: [ /* @__PURE__ */ jsxs8("div", { className: "mb-3", children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1 uppercase ${themeClasses.label}`, children: "Scene Name" }), /* @__PURE__ */ jsx8( "input", { type: "text", value: name, onChange: (e) => setName(e.target.value), className: textInputClass, placeholder: "My Awesome Scene", autoFocus: true } ) ] }), /* @__PURE__ */ jsxs8("div", { className: "mb-4", children: [ /* @__PURE__ */ jsxs8("div", { className: "flex justify-between items-center mb-1", children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold uppercase ${themeClasses.label}`, children: "Icon Emoji Selector" }), /* @__PURE__ */ jsx8("span", { className: "text-sm font-bold px-1.5 py-0.5 bg-zinc-800/80 text-white rounded border border-zinc-700 font-mono min-w-[28px] text-center select-none", children: icon }) ] }), /* @__PURE__ */ jsx8("div", { className: "flex gap-1 mb-2 overflow-x-auto pb-1 text-[9.5px]", children: Object.keys(EMOJI_CATEGORIES).map((cat) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => setSelectedCategory(cat), className: `px-1.5 py-0.5 rounded shrink-0 transition-colors ${selectedCategory === cat ? "bg-indigo-650 text-white font-bold" : "bg-zinc-800/45 text-zinc-400 hover:text-zinc-200"}`, children: cat }, cat )) }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-6 gap-1 max-h-[100px] overflow-y-auto p-1.5 bg-zinc-900/60 rounded border border-zinc-800 mb-2", children: EMOJI_CATEGORIES[selectedCategory].map((emoji) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => setIcon(emoji), className: `text-lg p-1 rounded hover:bg-zinc-800 transition-all ${icon === emoji ? "bg-blue-600 scale-110 text-white shadow-sm" : ""}`, children: emoji }, emoji )) }), /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx8("label", { className: "block text-[10px] text-zinc-400", children: "Custom User Browser Emoji:" }), /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx8( "input", { type: "text", maxLength: 8, value: icon, onChange: (e) => setIcon(e.target.value), className: `${textInputClass} text-center font-bold text-base py-0.5 w-14`, placeholder: "\u{1F3AC}" } ), /* @__PURE__ */ jsx8("span", { className: "text-[9px] text-zinc-500 leading-tight", children: "Paste or type any emoji supported by your OS / browser." }) ] }) ] }) ] }), /* @__PURE__ */ jsxs8("div", { className: "flex justify-end space-x-2 border-t border-zinc-700/30 pt-3 mt-1", children: [ /* @__PURE__ */ jsx8("button", { type: "button", onClick: onClose, className: `px-4 py-2 text-sm rounded ${themeClasses.buttonSecondary}`, children: "Cancel" }), /* @__PURE__ */ jsx8("button", { type: "submit", disabled: !name.trim(), className: "px-4 py-2 text-sm rounded bg-blue-600 hover:bg-blue-500 text-white disabled:bg-blue-800 disabled:cursor-not-allowed", children: "Update" }) ] }) ] }) ] } ) ] }); }; var DeleteConfirmationModal = ({ onConfirm, onClose, theme }) => { const themeClasses = themes[theme]; const [position, setPosition] = useState8({ x: 0, y: 0 }); const modalRef = useRef8(null); const dragOffset = useRef8({ x: 0, y: 0 }); useEffect7(() => { const x = Math.max(20, (window.innerWidth - 384) / 2); const y = Math.max(20, (window.innerHeight - 200) / 2); setPosition({ x, y }); }, []); const handleMouseDown = (e) => { if (e.target.closest("button")) return; if (modalRef.current) { const rect = modalRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const handleMouseMove = (moveEvent) => { const newX = Math.max(0, Math.min(window.innerWidth - 180, moveEvent.clientX - dragOffset.current.x)); const newY = Math.max(0, Math.min(window.innerHeight - 100, moveEvent.clientY - dragOffset.current.y)); setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } }; useEffect7(() => { const handleKeyDown = (e) => { if (e.key === "Enter") { onConfirm(); } else if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [onConfirm, onClose]); return /* @__PURE__ */ jsxs8(Fragment5, { children: [ /* @__PURE__ */ jsx8("div", { className: "fixed inset-0 bg-black/70 z-[190000] pointer-events-auto", onClick: onClose }), /* @__PURE__ */ jsxs8( "div", { ref: modalRef, style: { position: "fixed", left: `${position.x}px`, top: `${position.y}px`, zIndex: 2e5 }, className: `rounded-lg shadow-2xl max-w-sm w-full p-6 border text-center pointer-events-auto ${themeClasses.bg} ${themeClasses.border}`, children: [ /* @__PURE__ */ jsxs8( "div", { onMouseDown: handleMouseDown, className: "cursor-move select-none border-b border-gray-700/30 pb-2 mb-3 flex items-center justify-between", children: [ /* @__PURE__ */ jsx8("h2", { className: "text-sm font-bold uppercase tracking-wider text-red-500", children: "Confirm Deletion" }), /* @__PURE__ */ jsx8("span", { className: "text-[9px] text-zinc-500 font-mono", children: "\u2725 Drag" }) ] } ), /* @__PURE__ */ jsx8("h3", { className: "text-base font-bold mb-2", children: "Are you sure?" }), /* @__PURE__ */ jsx8("p", { className: `text-sm mb-6 ${themeClasses.label}`, children: "This action cannot be undone." }), /* @__PURE__ */ jsxs8("div", { className: "flex justify-center space-x-4", children: [ /* @__PURE__ */ jsx8("button", { onClick: onClose, className: `px-6 py-2 text-sm rounded-md font-semibold cursor-pointer ${themeClasses.buttonSecondary}`, children: "No, Cancel" }), /* @__PURE__ */ jsx8("button", { onClick: onConfirm, className: "px-6 py-2 text-sm rounded-md font-semibold bg-red-600 hover:bg-red-500 text-white cursor-pointer", children: "Yes, Delete" }) ] }) ] } ) ] }); }; var CanvasSettings = ({ activeScene, onUpdateScene, theme, onSetTheme, centerHotkey, onSetCenterHotkey, fullscreenHotkey, onSetFullscreenHotkey, lockHotkey, onSetLockHotkey, visibilityHotkey, onSetVisibilityHotkey, canvasResolution, onSetCanvasResolution, isMobileMode, onToggleMobileMode, scrollbarColor, onSetScrollbarColor, performancePreset, onSetPerformancePreset, framerateLimit, onSetFramerateLimit, renderScale, onSetRenderScale, interfaceAnimations, onSetInterfaceAnimations, onExportMasterBackup, onImportMasterBackup, contextMenuTheme = "match", onSetContextMenuTheme = () => { }, sceneHotkeys = {}, onSetSceneHotkey = () => { }, scenes = [] }) => { if (!activeScene) return null; const themeClasses = themes[theme]; const [isGraphicOpen, setIsGraphicOpen] = useState8(false); const [isAppearanceOpen, setIsAppearanceOpen] = useState8(false); const [isHotkeysOpen, setIsHotkeysOpen] = useState8(false); const themeOptions = [ { id: "light", name: "Light", colors: "bg-gray-200" }, { id: "dark", name: "Dark", colors: "bg-gray-700" }, { id: "crimson", name: "Crimson", colors: "bg-red-800" }, { id: "metallic", name: "Metallic", colors: "bg-slate-700" }, { id: "lime", name: "Lime", colors: "bg-lime-800" }, { id: "purple", name: "Purple", colors: "bg-purple-800" }, { id: "lemon", name: "Lemon", colors: "bg-yellow-800" }, { id: "silverado", name: "Silverado", colors: "bg-zinc-700" }, { id: "orange", name: "Orange", colors: "bg-orange-800" } ]; return /* @__PURE__ */ jsxs8("div", { className: "p-3 space-y-4", children: [ /* @__PURE__ */ jsxs8("div", { className: `border-b ${themeClasses.border} pb-3`, children: [ /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => setIsGraphicOpen(!isGraphicOpen), className: "w-full flex items-center justify-between py-2 text-xs font-bold uppercase tracking-wide text-zinc-400 hover:text-white transition", children: [ /* @__PURE__ */ jsx8("span", { className: "flex items-center gap-1.5", children: "\u{1F3AC} Graphic Settings" }), /* @__PURE__ */ jsx8("span", { className: "text-[10px] opacity-60", children: isGraphicOpen ? "\u25B2" : "\u25BC" }) ] } ), isGraphicOpen && /* @__PURE__ */ jsxs8("div", { className: "mt-3 space-y-4 pl-1", children: [ /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1 ${themeClasses.label} uppercase`, children: "Background Color" }), /* @__PURE__ */ jsx8( "input", { type: "color", value: activeScene.backgroundColor || "#000000", onChange: (e) => onUpdateScene(activeScene.id, { backgroundColor: e.target.value }), className: `w-full border-none rounded h-8 p-0 cursor-pointer ${themeClasses.inputBg}`, style: { "--tw-ring-offset-shadow": "none", "--tw-ring-shadow": "none", "boxShadow": "none" } } ) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1.5 ${themeClasses.label} uppercase`, children: "Canvas Resolution" }), /* @__PURE__ */ jsxs8( "select", { value: canvasResolution, onChange: (e) => onSetCanvasResolution(e.target.value), className: `w-full py-1.5 px-3 text-xs font-semibold rounded border transition-colors ${themeClasses.button} ${themeClasses.text} bg-transparent border-gray-700`, children: [ /* @__PURE__ */ jsx8("option", { value: "240", className: "bg-gray-800 text-white", children: "240p (426 x 240) - 16:9 Mobile" }), /* @__PURE__ */ jsx8("option", { value: "480", className: "bg-gray-800 text-white", children: "480p (854 x 480) - SD" }), /* @__PURE__ */ jsx8("option", { value: "720", className: "bg-gray-800 text-white", children: "720p (1280 x 720) - HD" }), /* @__PURE__ */ jsx8("option", { value: "1080", className: "bg-gray-800 text-white", children: "1080p (1920 x 1080) - Full HD" }), /* @__PURE__ */ jsx8("option", { value: "2k", className: "bg-gray-800 text-white", children: "2K (2560 x 1440) - Quad HD" }), /* @__PURE__ */ jsx8("option", { value: "4k", className: "bg-gray-800 text-white", children: "4K (3840 x 2160) - Ultra HD" }) ] } ), /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-gray-500 block mt-1 leading-normal", children: "Virtual target canvas resolution." }) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1.5 ${themeClasses.label} uppercase`, children: "Performance Preset" }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-3 gap-1", children: [ { id: "low", label: "Max Perf", desc: "Disables heavy effects" }, { id: "medium", label: "Balanced", desc: "Medium optimization" }, { id: "high", label: "High Qual", desc: "No optimizations" } ].map((p) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => onSetPerformancePreset(p.id), className: `py-1 px-1 rounded text-[10px] font-bold border transition-colors cursor-pointer ${performancePreset === p.id ? "bg-blue-600 border-blue-500 text-white shadow" : `${themeClasses.buttonSecondary} border-transparent text-gray-400 hover:text-white`}`, title: p.desc, children: p.label }, p.id )) }) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1.5 ${themeClasses.label} uppercase`, children: "FPS Refresh Limit" }), /* @__PURE__ */ jsxs8( "select", { value: framerateLimit, onChange: (e) => onSetFramerateLimit(e.target.value), className: `w-full py-1.5 px-3 text-xs font-semibold rounded border transition-colors ${themeClasses.button} ${themeClasses.text} bg-transparent border-gray-700`, children: [ /* @__PURE__ */ jsx8("option", { value: "15", className: "bg-gray-800 text-white", children: "15 FPS (Lowest CPU usage)" }), /* @__PURE__ */ jsx8("option", { value: "30", className: "bg-gray-800 text-white", children: "30 FPS (Optimized power saver)" }), /* @__PURE__ */ jsx8("option", { value: "60", className: "bg-gray-800 text-white", children: "60 FPS (Fluid presentation)" }), /* @__PURE__ */ jsx8("option", { value: "uncapped", className: "bg-gray-800 text-white", children: "Uncapped (Host native refresh)" }) ] } ) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1.5 ${themeClasses.label} uppercase`, children: "Render Scaling Ratio" }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-3 gap-1", children: [ { id: "0.5", label: "50% (Fast)", title: "Reduces viewport render overhead by 75%" }, { id: "0.75", label: "75% (Opt)", title: "Balanced look and CPU usage" }, { id: "1.0", label: "100% (Crisp)", title: "Pixel-perfect native resolution" } ].map((s) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => onSetRenderScale(s.id), className: `py-1 px-1 rounded text-[10px] font-bold border transition-colors cursor-pointer ${renderScale === s.id ? "bg-blue-600 border-blue-500 text-white shadow" : `${themeClasses.buttonSecondary} border-transparent text-gray-400 hover:text-white`}`, title: s.title, children: s.label }, s.id )) }) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1.5 ${themeClasses.label} uppercase`, children: "Interface Animations" }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-2 gap-1.5", children: [ { id: "enabled", label: "\u{1F3AC} Enabled" }, { id: "disabled", label: "\u26A1 Disabled (Instant)" } ].map((a) => /* @__PURE__ */ jsx8( "button", { type: "button", onClick: () => onSetInterfaceAnimations(a.id), className: `py-1 px-2 rounded text-[10px] font-bold border transition-colors cursor-pointer ${interfaceAnimations === a.id ? "bg-blue-600 border-blue-500 text-white" : `${themeClasses.buttonSecondary} border-transparent text-gray-400 hover:text-white`}`, children: a.label }, a.id )) }) ] }) ] }) ] }), /* @__PURE__ */ jsxs8("div", { className: `border-b ${themeClasses.border} pb-3`, children: [ /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => setIsAppearanceOpen(!isAppearanceOpen), className: "w-full flex items-center justify-between py-2 text-xs font-bold uppercase tracking-wide text-zinc-400 hover:text-white transition", children: [ /* @__PURE__ */ jsx8("span", { className: "flex items-center gap-1.5", children: "\u{1F3A8} Appearance Settings" }), /* @__PURE__ */ jsx8("span", { className: "text-[10px] opacity-60", children: isAppearanceOpen ? "\u25B2" : "\u25BC" }) ] } ), isAppearanceOpen && /* @__PURE__ */ jsxs8("div", { className: "mt-3 space-y-4 pl-1", children: [ /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1 ${themeClasses.label} uppercase`, children: "UI Theme" }), /* @__PURE__ */ jsx8("p", { className: "text-[10px] text-gray-400 mb-2 leading-tight", children: "Customize the global visual appearance of interface panels." }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-2 gap-1.5", children: themeOptions.map((opt) => /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => onSetTheme(opt.id), className: `py-1 px-2 text-[10px] font-bold rounded border flex items-center justify-start gap-1.5 transition cursor-pointer ${theme === opt.id ? "bg-indigo-650 font-bold border-indigo-500 shadow text-white" : `${themeClasses.buttonSecondary} border-transparent text-gray-300 hover:text-white`}`, children: [ /* @__PURE__ */ jsx8("span", { className: `w-2.5 h-2.5 rounded-full inline-block shrink-0 ${opt.colors}` }), /* @__PURE__ */ jsx8("span", { className: "truncate", children: opt.name }) ] }, opt.id )) }) ] }), /* @__PURE__ */ jsxs8("div", { className: "border-t border-gray-700/30 pt-3", children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1 ${themeClasses.label} uppercase`, children: "Custom Scrollbar Color" }), /* @__PURE__ */ jsx8("p", { className: "text-[10px] text-gray-400 mb-2 leading-tight", children: "Toggle below to custom colorize the main window scrollbars." }), /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-2 gap-1.5", children: [ { color: "#6366f1", name: "Indigo" }, { color: "#8b5cf6", name: "Violet" }, { color: "#e11d48", name: "Rose" }, { color: "#a855f7", name: "Purple" }, { color: "#84cc16", name: "Lime" }, { color: "#f97316", name: "Orange" }, { color: "#06b6d4", name: "Cyan" }, { color: "#6b7280", name: "Slate" } ].map((col) => /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => onSetScrollbarColor(col.color), className: `py-1 px-2 text-[10px] font-bold rounded border flex items-center justify-start gap-1.5 transition ${scrollbarColor === col.color ? "bg-indigo-650 font-bold border-indigo-500 shadow text-white" : `${themeClasses.buttonSecondary} border-transparent text-gray-300 hover:text-white`}`, children: [ /* @__PURE__ */ jsx8("span", { className: "w-2.5 h-2.5 rounded-full inline-block shrink-0", style: { backgroundColor: col.color } }), /* @__PURE__ */ jsx8("span", { className: "truncate", children: col.name }) ] }, col.color )) }) ] }), /* @__PURE__ */ jsxs8("div", { className: "border-t border-gray-700/30 pt-3", children: [ /* @__PURE__ */ jsx8("label", { className: `block text-xs font-bold mb-1 ${themeClasses.label} uppercase`, children: "Right-Click Menu Theme" }), /* @__PURE__ */ jsx8("p", { className: "text-[10px] text-gray-400 mb-2 leading-tight", children: "Customize the theme styled exclusively on the right click menus." }), /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-2 gap-1.5 font-sans", children: [ /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => onSetContextMenuTheme("match"), className: `py-1 px-2 text-[10px] font-bold rounded border flex items-center justify-start gap-1.5 transition cursor-pointer ${contextMenuTheme === "match" ? "bg-indigo-650 font-bold border-indigo-500 shadow text-white" : `${themeClasses.buttonSecondary} border-transparent text-gray-300 hover:text-white`}`, children: [ /* @__PURE__ */ jsx8("span", { className: "w-2.5 h-2.5 rounded-full inline-block shrink-0 bg-transparent border border-dashed border-gray-400" }), /* @__PURE__ */ jsx8("span", { className: "truncate", children: "Match UI Theme" }) ] } ), themeOptions.map((opt) => /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => onSetContextMenuTheme(opt.id), className: `py-1 px-2 text-[10px] font-bold rounded border flex items-center justify-start gap-1.5 transition cursor-pointer ${contextMenuTheme === opt.id ? "bg-indigo-650 font-bold border-indigo-500 shadow text-white" : `${themeClasses.buttonSecondary} border-transparent text-gray-300 hover:text-white`}`, children: [ /* @__PURE__ */ jsx8("span", { className: `w-2.5 h-2.5 rounded-full inline-block shrink-0 ${opt.colors}` }), /* @__PURE__ */ jsxs8("span", { className: "truncate", children: [ opt.name, " Menu" ] }) ] }, opt.id )) ] }) ] }) ] }) ] }), /* @__PURE__ */ jsxs8("div", { className: `pb-3`, children: [ /* @__PURE__ */ jsxs8( "button", { type: "button", onClick: () => setIsHotkeysOpen(!isHotkeysOpen), className: "w-full flex items-center justify-between py-2 text-xs font-bold uppercase tracking-wide text-zinc-400 hover:text-white transition", children: [ /* @__PURE__ */ jsx8("span", { className: "flex items-center gap-1.5", children: "\u2328\uFE0F Keyboard Hotkeys" }), /* @__PURE__ */ jsx8("span", { className: "text-[10px] opacity-60", children: isHotkeysOpen ? "\u25B2" : "\u25BC" }) ] } ), isHotkeysOpen && /* @__PURE__ */ jsxs8("div", { className: "mt-3 space-y-3.5 pl-1", children: [ /* @__PURE__ */ jsxs8("p", { className: "text-[10px] text-gray-400 leading-normal", children: [ "Type keys in combination (e.g., ", /* @__PURE__ */ jsx8("strong", { children: "shift+f" }), ", ", /* @__PURE__ */ jsx8("strong", { children: "ctrl+l" }), ", or ", /* @__PURE__ */ jsx8("strong", { children: "delete" }), ")." ] }), /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [ /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-gray-400 block mb-1 font-medium uppercase", children: "Center Selected Source" }), /* @__PURE__ */ jsx8( "input", { type: "text", value: centerHotkey, onChange: (e) => onSetCenterHotkey(e.target.value), className: `w-full text-xs font-mono rounded px-2 py-1.5 border border-transparent ${themeClasses.inputBg} ${themeClasses.text} focus:outline-none focus:border-blue-500`, placeholder: "shift+f" } ) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-gray-400 block mb-1 font-medium uppercase", children: "Fullscreen Toggle" }), /* @__PURE__ */ jsx8( "input", { type: "text", value: fullscreenHotkey, onChange: (e) => onSetFullscreenHotkey(e.target.value), className: `w-full text-xs font-mono rounded px-2 py-1.5 border border-transparent ${themeClasses.inputBg} ${themeClasses.text} focus:outline-none focus:border-blue-500`, placeholder: "shift+s" } ) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-gray-400 block mb-1 font-medium uppercase", children: "Toggle Lock Target" }), /* @__PURE__ */ jsx8( "input", { type: "text", value: lockHotkey, onChange: (e) => onSetLockHotkey(e.target.value), className: `w-full text-xs font-mono rounded px-2 py-1.5 border border-transparent ${themeClasses.inputBg} ${themeClasses.text} focus:outline-none focus:border-blue-500`, placeholder: "shift+l" } ) ] }), /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-gray-400 block mb-1 font-medium uppercase", children: "Toggle Visibility Target" }), /* @__PURE__ */ jsx8( "input", { type: "text", value: visibilityHotkey, onChange: (e) => onSetVisibilityHotkey(e.target.value), className: `w-full text-xs font-mono rounded px-2 py-1.5 border border-transparent ${themeClasses.inputBg} ${themeClasses.text} focus:outline-none focus:border-blue-500`, placeholder: "shift+v" } ) ] }), scenes && scenes.length > 0 && /* @__PURE__ */ jsxs8("div", { className: "border-t border-gray-700/30 pt-3 mt-3", children: [ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-gray-400 block mb-1.5 font-bold uppercase", children: "\u{1F3AC} Scene Switch Hotkeys" }), /* @__PURE__ */ jsxs8("p", { className: "text-[9px] text-gray-500 mb-2 leading-relaxed", children: [ "Activate each scene with a custom combo (e.g. ", /* @__PURE__ */ jsx8("span", { className: "font-mono", children: "ctrl+1" }), " or ", /* @__PURE__ */ jsx8("span", { className: "font-mono", children: "shift+z" }), ")." ] }), /* @__PURE__ */ jsx8("div", { className: "space-y-2 mt-1", children: scenes.map((sc) => { const hkVal = sceneHotkeys[sc.id] || ""; return /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between gap-2 bg-black/10 p-1.5 rounded border border-gray-800/20", children: [ /* @__PURE__ */ jsxs8("span", { className: "text-[11px] font-medium truncate flex-1 flex items-center gap-1.5", children: [ /* @__PURE__ */ jsx8("span", { className: "text-sm shrink-0", children: sc.icon || "\u{1F3AC}" }), /* @__PURE__ */ jsx8("span", { className: "truncate text-zinc-300", children: sc.name }) ] }), /* @__PURE__ */ jsx8( "input", { type: "text", value: hkVal, onChange: (e) => onSetSceneHotkey(sc.id, e.target.value), className: `w-32 text-xs font-mono rounded px-2 py-1 border border-zinc-700/50 ${themeClasses.inputBg} ${themeClasses.text} focus:outline-none focus:border-blue-500 text-center`, placeholder: "e.g. ctrl+1" } ) ] }, sc.id); }) }) ] }) ] }) ] }) ] }), /* @__PURE__ */ jsx8("div", { className: `pt-4 border-t ${themeClasses.border} space-y-4`, children: /* @__PURE__ */ jsxs8("div", { className: "bg-indigo-600/10 border border-indigo-500/20 rounded-lg p-3 space-y-3", children: [ /* @__PURE__ */ jsxs8("div", { children: [ /* @__PURE__ */ jsx8("h4", { className: "text-[11px] font-bold text-indigo-400 uppercase tracking-widest flex items-center gap-1.5", children: "Backup" }), /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-zinc-400 block leading-normal mt-1", children: "Save or load your whole setup. The downloaded backup file contains all workspace scenes, sources, properties, active configurations, shortcuts, dynamic filters, and performance settings." }) ] }), /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-2 gap-2", children: [ /* @__PURE__ */ jsx8( "button", { type: "button", onClick: onImportMasterBackup, className: "py-2 px-3 text-xs font-bold rounded flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white transition-all cursor-pointer shadow-md hover:scale-[1.02] active:scale-[0.98] duration-150", title: "Import full backup JSON file", children: "\u{1F4E5} Import" } ), /* @__PURE__ */ jsx8( "button", { type: "button", onClick: onExportMasterBackup, className: "py-2 px-3 text-xs font-bold rounded flex items-center justify-center gap-1.5 bg-indigo-650 hover:bg-indigo-505 text-white transition-all cursor-pointer shadow-md hover:scale-[1.02] active:scale-[0.98] duration-150", title: "Export backup JSON file", children: "\u{1F4E4} Export" } ) ] }) ] }) }) ] }); }; var UIToggle = ({ isUIVisible, onToggleUIVisibility, theme }) => { const [isHovered, setIsHovered] = useState8(false); const themeClasses = themes[theme]; const buttonClass = `${themeClasses.button} ${themeClasses.text} hover:bg-blue-600 hover:text-white`; return /* @__PURE__ */ jsx8( "div", { className: "fixed bottom-4 right-4 flex items-center space-x-2 pointer-events-auto", onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: /* @__PURE__ */ jsx8( "button", { onClick: onToggleUIVisibility, title: isUIVisible ? "Lock Canvas & Hide UI" : "Unlock Canvas & Show UI", className: `transition-all duration-300 ease-in-out ${buttonClass} font-bold p-3 rounded-full shadow-lg transform ${isUIVisible || isHovered ? "opacity-100 scale-100" : "opacity-0 scale-90"}`, children: isUIVisible ? /* @__PURE__ */ jsx8("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" }) }) : /* @__PURE__ */ jsx8("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16m-7 6h7" }) }) } ) } ); }; var SceneSwitcher = ({ scenes, activeSceneId, onSelectScene, theme }) => { const [isHovered, setIsHovered] = useState8(false); const themeClasses = themes[theme]; return /* @__PURE__ */ jsx8( "div", { className: "fixed bottom-4 left-4 pointer-events-auto", onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: /* @__PURE__ */ jsx8("div", { className: `flex items-center space-x-2 transition-opacity duration-300 ${isHovered ? "opacity-100" : "opacity-0"}`, children: scenes.map((scene) => /* @__PURE__ */ jsxs8( "button", { onClick: () => onSelectScene(scene.id), title: scene.name, className: `px-3 py-2 text-sm rounded-lg transition-colors shadow-lg ${activeSceneId === scene.id ? "bg-blue-600 text-white" : `${themeClasses.button} ${themeClasses.text}`}`, children: [ scene.icon, " ", scene.name ] }, scene.id )) }) } ); }; var TabButton = ({ id, icon, label, activeTab, setActiveTab, theme, hidden = false, onClose }) => { if (hidden) return null; const isActive = activeTab === id; const themeClasses = themes[theme]; return /* @__PURE__ */ jsxs8( "button", { onClick: () => setActiveTab(id), title: label, className: `p-2 rounded flex-grow transition-colors relative group ${isActive ? "bg-blue-600 text-white" : `${themeClasses.button} ${themeClasses.itemHover}`}`, "aria-label": label, children: [ /* @__PURE__ */ jsx8("span", { className: "text-xl leading-none block", "aria-hidden": "true", children: icon }), onClose && /* @__PURE__ */ jsx8( "span", { onClick: (e) => { e.stopPropagation(); onClose(); }, className: "absolute top-0 right-0 text-xs px-1 rounded-full bg-black/30 opacity-0 group-hover:opacity-100 hover:bg-red-500", title: "Close Panel", children: "\xD7" } ) ] } ); }; var UI = (props) => { const { isVisible, onToggleVisibility, theme, activeTab, setActiveTab, isAddingScene, onConfirmAddScene, onCancelAddScene, isMobileMode, onToggleMobileMode, isPoppedOut, onTogglePoppedOut, panelWidth, onPanelWidthChange, isHoveredOverLeft, onHoveredOverLeftChange, scrollbarColor, onSetScrollbarColor, performancePreset, onSetPerformancePreset, framerateLimit, onSetFramerateLimit, renderScale, onSetRenderScale, interfaceAnimations, onSetInterfaceAnimations, onExportCanvasSettings, onImportCanvasSettings, onExportMasterBackup, onImportMasterBackup } = props; const [position, setPosition] = useState8({ x: 16, y: 16 }); const [isDragging, setIsDragging] = useState8(false); const dragOffset = useRef8({ x: 0, y: 0 }); const panelRef = useRef8(null); const [panelHeight, setPanelHeight] = useState8(620); const handleResizeMouseDown = (e) => { e.preventDefault(); e.stopPropagation(); const startWidth = panelWidth; const startHeight = panelHeight; const startX = e.clientX; const startY = e.clientY; const handleMouseMoveResize = (moveEvent) => { const newWidth = Math.max(285, Math.min(800, startWidth + (moveEvent.clientX - startX))); const newHeight = Math.max(300, Math.min(window.innerHeight - 40, startHeight + (moveEvent.clientY - startY))); onPanelWidthChange(newWidth); setPanelHeight(newHeight); }; const handleMouseUpResize = () => { window.removeEventListener("mousemove", handleMouseMoveResize); window.removeEventListener("mouseup", handleMouseUpResize); }; window.addEventListener("mousemove", handleMouseMoveResize); window.addEventListener("mouseup", handleMouseUpResize); }; const handleMouseDown = (e) => { if (panelRef.current) { const rect = panelRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }; setIsDragging(true); document.body.style.userSelect = "none"; } }; const handleMouseMove = useCallback2((e) => { if (!isDragging) return; e.preventDefault(); setPosition({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y }); }, [isDragging]); const handleMouseUp = useCallback2(() => { setIsDragging(false); document.body.style.userSelect = ""; }, []); useEffect7(() => { if (isDragging) { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp, { once: true }); } return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging, handleMouseMove, handleMouseUp]); const themeClasses = themes[theme]; return /* @__PURE__ */ jsxs8("div", { className: "absolute inset-0 z-[50000] pointer-events-none", children: [ isAddingScene && /* @__PURE__ */ jsx8(AddSceneModal, { onConfirm: onConfirmAddScene, onClose: onCancelAddScene, theme }), props.editingScene && /* @__PURE__ */ jsx8( EditSceneModal, { scene: props.editingScene, onConfirm: props.onConfirmEditScene, onClose: props.onCloseEditSceneModal, theme } ), props.sourceToDelete && /* @__PURE__ */ jsx8( DeleteConfirmationModal, { onConfirm: props.onConfirmDelete, onClose: props.onCancelDelete, theme } ), props.filteringSource && /* @__PURE__ */ jsx8( FilterPanel_default, { source: props.filteringSource, onUpdateStyle: props.onUpdateSourceStyle, onClose: props.onCloseFilterPanel, theme: props.theme }, `${props.filteringSource.id}-filter` ), /* @__PURE__ */ jsx8( UIToggle, { isUIVisible: isVisible, onToggleUIVisibility: onToggleVisibility, theme: props.theme } ), !isVisible && /* @__PURE__ */ jsx8( SceneSwitcher, { scenes: props.scenes, activeSceneId: props.activeScene?.id || "", onSelectScene: props.onSetActiveScene, theme: props.theme } ), (isVisible || isPoppedOut) && /* @__PURE__ */ jsxs8( "div", { ref: panelRef, onMouseEnter: () => onHoveredOverLeftChange(true), onMouseLeave: () => onHoveredOverLeftChange(false), style: isPoppedOut ? { top: `${position.y}px`, left: `${position.x}px`, width: `${panelWidth}px`, height: `${panelHeight}px`, maxHeight: "calc(100vh - 2rem)" } : { width: `${panelWidth}px` }, className: isPoppedOut ? `absolute flex flex-col pointer-events-auto transition-all duration-300 ease-out z-[50000] border rounded-lg shadow-2xl ${themeClasses.bg} bg-opacity-85 backdrop-blur-md ${themeClasses.border} ${isVisible ? props.isAutoHideEnabled && !isHoveredOverLeft ? "opacity-0 scale-90 pointer-events-none translate-y-2" : "opacity-100 scale-100 translate-y-0" : "opacity-0 scale-95 pointer-events-none"}` : `fixed top-0 bottom-0 left-0 h-screen flex flex-col pointer-events-auto transition-transform duration-300 ease-in-out z-[50000] border-r ${themeClasses.bg} bg-opacity-95 backdrop-blur-md rounded-r-lg shadow-2xl ${themeClasses.border} ${isVisible && (!props.isAutoHideEnabled || isHoveredOverLeft) ? "translate-x-0" : "-translate-x-full"}`, children: [ /* @__PURE__ */ jsxs8( "div", { onMouseDown: isPoppedOut ? handleMouseDown : void 0, className: `flex items-center justify-between px-3 py-2 border-b ${themeClasses.border} ${isPoppedOut ? "cursor-move" : ""} ${themeClasses.label}`, children: [ /* @__PURE__ */ jsx8("span", { className: "font-bold text-sm uppercase tracking-wider select-none", children: "Dashboard" }), /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-1.5", children: [ /* @__PURE__ */ jsx8( "button", { onClick: props.onToggleAutoHide, title: props.isAutoHideEnabled ? "Disable auto hide dashboard" : "Enable auto hide dashboard", className: `px-2 py-0.5 rounded text-xs transition-colors font-medium cursor-pointer ${props.isAutoHideEnabled ? "bg-indigo-600 hover:bg-indigo-700 text-white" : "bg-gray-600 hover:bg-gray-700 text-gray-200"}`, children: props.isAutoHideEnabled ? "UNHIDE" : "HIDE" } ), /* @__PURE__ */ jsx8( "button", { onClick: props.onToggleHotkeys, title: props.isHotkeysEnabled ? "Disable keyboard hotkeys" : "Enable keyboard hotkeys", className: `px-2 py-0.5 rounded text-xs transition-colors font-medium cursor-pointer ${props.isHotkeysEnabled ? "bg-green-600 hover:bg-green-700 text-white" : "bg-gray-600 hover:bg-gray-700 text-gray-200"}`, children: props.isHotkeysEnabled ? "\u2328\uFE0F ON" : "\u2328\uFE0F OFF" } ), /* @__PURE__ */ jsx8( "button", { onClick: onTogglePoppedOut, title: isPoppedOut ? "Dock Dashboard to Left (Slide-out)" : "Pop Out Panel & Customize Dimensions", className: `px-2 py-0.5 rounded text-xs transition-colors bg-blue-600 hover:bg-blue-500 text-white font-medium cursor-pointer`, children: isPoppedOut ? "\u{1F4E5} Dock" : "\u2197\uFE0F Pop Out" } ) ] }) ] } ), /* @__PURE__ */ jsxs8("div", { className: `flex items-center p-1 border-b ${themeClasses.border} space-x-1 flex-wrap`, children: [ /* @__PURE__ */ jsx8(TabButton, { id: "sources", icon: "\u{1F4DD}", label: "Sources", activeTab, setActiveTab, theme }), /* @__PURE__ */ jsx8(TabButton, { id: "scenes", icon: "\u{1F3AC}", label: "Scenes", activeTab, setActiveTab, theme }), /* @__PURE__ */ jsx8(TabButton, { id: "canvas", icon: "\u{1F5BC}\uFE0F", label: "Canvas Settings", activeTab, setActiveTab, theme }), /* @__PURE__ */ jsx8(TabButton, { id: "stats", icon: "\u{1F4CA}", label: "Stats", activeTab, setActiveTab, theme }), /* @__PURE__ */ jsx8(TabButton, { id: "settings", icon: "\u2699\uFE0F", label: "Source Settings", activeTab, setActiveTab, theme, hidden: !props.selectedSource, onClose: () => { props.onSelectSource(null); setActiveTab("sources"); } }) ] }), /* @__PURE__ */ jsxs8("div", { className: "flex-grow overflow-y-auto overflow-x-hidden", children: [ activeTab === "sources" && /* @__PURE__ */ jsx8( SourceManager_default, { sources: props.activeScene?.sources || [], onAddSource: props.onAddSource, onDeleteSource: props.onDeleteSource, onUpdateSourceLayer: props.onUpdateSourceLayer, onImportSources: props.onImportSources, onExportSources: props.onExportSources, onCenterSource: props.onCenterSource, onFullscreenSource: props.onFullscreenSource, onUndo: props.onUndo, onRedo: props.onRedo, canUndo: props.canUndo, canRedo: props.canRedo, selectedSourceId: props.selectedSource?.id || null, onSelectSource: props.onSelectSource, setActiveTab, isSnapToGridEnabled: props.isSnapToGridEnabled, onToggleSnapToGrid: props.onToggleSnapToGrid, theme: props.theme, onOpenFilterPanel: props.onOpenFilterPanel, onToggleSourceVisibility: props.onToggleSourceVisibility, onToggleSourceLock: props.onToggleSourceLock, onReorderSources: props.onReorderSources, onSourceContextMenu: props.onSourceContextMenu, copiedSource: props.copiedSource, onCopySource: props.onCopySource, onPasteSource: props.onPasteSource } ), activeTab === "scenes" && /* @__PURE__ */ jsx8( SceneManager_default, { scenes: props.scenes, activeSceneId: props.activeScene?.id || "", onSelectScene: props.onSetActiveScene, onAddScene: props.onAddScene, onDeleteScene: props.onDeleteScene, onImportProject: props.onImportProject, onExportProject: props.onExportProject, theme: props.theme, sceneTransition: props.sceneTransition, onSetSceneTransition: props.onSetSceneTransition, onOpenEditSceneModal: props.onOpenEditSceneModal, onDuplicateScene: props.onDuplicateScene } ), activeTab === "canvas" && /* @__PURE__ */ jsx8( CanvasSettings, { activeScene: props.activeScene, onUpdateScene: props.onUpdateScene, theme: props.theme, onSetTheme: props.onSetTheme, centerHotkey: props.centerHotkey, onSetCenterHotkey: props.onSetCenterHotkey, fullscreenHotkey: props.fullscreenHotkey, onSetFullscreenHotkey: props.onSetFullscreenHotkey, lockHotkey: props.lockHotkey, onSetLockHotkey: props.onSetLockHotkey, visibilityHotkey: props.visibilityHotkey, onSetVisibilityHotkey: props.onSetVisibilityHotkey, canvasResolution: props.canvasResolution, onSetCanvasResolution: props.onSetCanvasResolution, isMobileMode, onToggleMobileMode, scrollbarColor, onSetScrollbarColor, performancePreset, onSetPerformancePreset, framerateLimit, onSetFramerateLimit, renderScale, onSetRenderScale, interfaceAnimations, onSetInterfaceAnimations, onExportMasterBackup, onImportMasterBackup, contextMenuTheme: props.contextMenuTheme, onSetContextMenuTheme: props.onSetContextMenuTheme, sceneHotkeys: props.sceneHotkeys, onSetSceneHotkey: props.onSetSceneHotkey, scenes: props.scenes } ), activeTab === "stats" && /* @__PURE__ */ jsx8( StatsPanel_default, { actionLog: props.actionLog, onClearLog: props.onClearActionLog, theme: props.theme, activeScene: props.activeScene, scenes: props.scenes, canvasResolution: props.canvasResolution, performancePreset, framerateLimit, renderScale } ), activeTab === "settings" && props.selectedSource && /* @__PURE__ */ jsx8( SettingsPanel_default, { source: props.selectedSource, onUpdate: props.onUpdateSource, onClose: () => { props.onSelectSource(null); setActiveTab("sources"); }, theme: props.theme }, props.selectedSource.id ) ] }), isPoppedOut && /* @__PURE__ */ jsx8( "div", { onMouseDown: handleResizeMouseDown, className: "absolute bottom-0 right-0 w-5 h-5 cursor-se-resize flex items-end justify-end p-0.5 group z-[60000]", title: "Drag to resize panel", children: /* @__PURE__ */ jsx8("svg", { className: "w-3 h-3 text-gray-500 hover:text-blue-500 transition-colors", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3.5, d: "M19 19H12M19 13h-4" }) }) } ), props.isAutoHideEnabled && !isPoppedOut && isVisible && /* @__PURE__ */ jsx8( "div", { className: "absolute top-0 bottom-0 right-0 w-6 translate-x-full bg-transparent cursor-pointer z-[50002]", title: "Hover to show Dashboard" } ) ] } ), isPoppedOut && props.isAutoHideEnabled && !isHoveredOverLeft && isVisible && /* @__PURE__ */ jsxs8( "div", { onMouseEnter: () => onHoveredOverLeftChange(true), onClick: () => onHoveredOverLeftChange(true), style: { position: "absolute", top: `${position.y}px`, left: `${position.x}px` }, className: "w-10 h-10 z-[50001] bg-indigo-600 hover:bg-indigo-500 text-white rounded-full shadow-2xl flex items-center justify-center text-sm cursor-pointer hover:scale-110 active:scale-95 transition-all duration-200 border border-indigo-400 group animate-pulse", title: "Hover to expand Dashboard", children: [ /* @__PURE__ */ jsx8("span", { children: "\u{1F4CA}" }), /* @__PURE__ */ jsx8("span", { className: "absolute left-12 bg-zinc-950/90 text-white text-[10px] uppercase font-bold px-2 py-1 rounded shadow-md border border-zinc-700 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none", children: "Expand Dashboard" }) ] } ) ] }); }; var UI_default = UI; // ws:scratch/cc/App.tsx import { Fragment as Fragment6, jsx as jsx9, jsxs as jsxs9 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var logoWidth = 512; var logoHeight = 261; var smallLogoWidth = logoWidth * 0.6; var smallLogoHeight = logoHeight * 0.6; var defaultFilters = { hue: 0, blur: 0, sharpness: 0, shape: "none" }; var defaultSourceStyle = { opacity: 1, scale: 1, rotation: 0, scaleX: 1, scaleY: 1, resizeBehavior: "static", boxShadow: false, shadowColor: "#000000", shadowBlur: 15, stroke: false, strokeColor: "#ffffff", strokeWidth: 2, fontSize: 16, textColor: "#ffffff", fontWeight: "normal", textAlign: "left", loop: true, playbackRate: 1, volume: 1, muted: true, slideDuration: 5, chromaKey: { enabled: false, color: "#00ff00", sensitivity: 20 }, colorKey: { enabled: false, color: "#ff00ff", sensitivity: 20 }, filters: { ...defaultFilters }, brushColor: "#ffffff", brushSize: 5 }; var initialScenes = [ { "id": "scene-1", "name": "Main Scene", "icon": "\u{1F3AC}", "backgroundColor": "#000000", "sources": [ { "id": "default-background", "type": "image", "name": "Background", "visible": true, "locked": false, "content": "https://i.imgur.com/hYM563K.png", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "fill", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": false, "color": "#ff00ff", "sensitivity": 20 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 0, "y": 0, "width": 1920, "height": 1080, "zIndex": 0 } }, { "id": "default-logo", "type": "image", "name": "Logo", "visible": true, "locked": false, "content": "https://i.imgur.com/vXi6tuT.png", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "center", "boxShadow": false, "shadowColor": "rgba(0,0,0,0.5)", "shadowBlur": 20, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": true, "color": "#292929", "sensitivity": 25 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 1359.328471248247, "y": 777.4927769985975, "width": 537.6, "height": 274.05, "zIndex": 1 } }, { "id": "default-video-animation", "type": "video", "name": "Video Example", "visible": true, "locked": false, "content": "https://i.imgur.com/Vh2UVFw.mp4", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "static", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": true, "color": "#000000", "sensitivity": 30 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 1089.438990182328, "y": 845.7924263674614, "width": 308.44319775596074, "height": 197.671809256662, "zIndex": 2 } } ] }, { "id": "scene-2", "name": "Code Example", "icon": "\u{1F4BB}", "backgroundColor": "#000000", "sources": [ { "id": "scene2-background", "type": "image", "name": "Background", "visible": true, "locked": false, "content": "https://i.imgur.com/hYM563K.png", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "fill", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": false, "color": "#ff00ff", "sensitivity": 20 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 0, "y": 5.385694249649369, "width": 1920, "height": 1080, "zIndex": 0 } }, { "id": "scene2-logo", "type": "image", "name": "Small Logo", "visible": true, "locked": false, "content": "https://i.imgur.com/vXi6tuT.png", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "static", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": true, "color": "#292929", "sensitivity": 25 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 1291.6552594670404, "y": 94.72117812061711, "width": 502.692286115007, "height": 274.0782608695652, "zIndex": 1 } }, { "id": "source-1762634820820", "type": "text", "name": "New Text", "visible": true, "locked": false, "content": "A dynamic canvas application to create and manage scenes with various media sources like images, videos, text, and browser iframes. Features a toggleable UI for source and scene management, and interactive, editable sources on the canvas.\n\n", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "static", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 43, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "center", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": false, "color": "#ff00ff", "sensitivity": 20 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 1295.7194950911642, "y": 418.1921458625526, "width": 478.40673211781206, "height": 568.0631136044881, "zIndex": 2, "fontFamily": "superscript" } }, { "id": "source-1762634998835", "type": "code", "name": "Code Example", "visible": true, "locked": false, "content": { "html": '', "css": "body, html { margin: 0; padding: 0; overflow: hidden; background-color: transparent; } canvas { display: block; }", "js": "const canvas = document.getElementById('canvas');\nconst ctx = canvas.getContext('2d');\ncanvas.width = window.innerWidth;\ncanvas.height = window.innerHeight;\n\nconst letters = 'ABCDEFGHIJKLMNOPQRSTUVXYZ';\nconst fontSize = 7;\nconst columns = canvas.width / fontSize;\n\nconst drops = Array.from({ length: columns }, () => ({\n y: Math.random() * -100, // Start above the screen randomly\n speed: (Math.random() * 0.5 + 0.5) * 1.6 // Varied speeds look nicer\n}));\n\nfunction draw() {\n // 1. Clear the canvas completely so it stays transparent\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n for (let i = 0; i < drops.length; i++) {\n const drop = drops[i];\n \n // 2. Loop backwards to draw the trail behind the head of the drop\n const trailLength = 15; \n for (let j = 0; j < trailLength; j++) {\n const trailY = drop.y - j;\n if (trailY < 0) continue; // Don't draw off-screen\n\n // Calculate opacity: the head (j=0) is bright, the tail (j=14) fades out\n const opacity = 1 - (j / trailLength);\n \n const text = letters[Math.floor(Math.random() * letters.length)];\n \n // Apply the opacity to the custom color using globalAlpha\n ctx.save();\n ctx.globalAlpha = opacity;\n ctx.fillStyle = '#00ff00';\n ctx.fillText(text, i * fontSize, trailY * fontSize);\n ctx.restore();\n }\n\n // Move the drop down\n drop.y += drop.speed;\n\n // Reset drop to the top if it goes off screen\n if (drop.y * fontSize > canvas.height && Math.random() > 0.975) {\n drop.y = 0;\n }\n }\n}\n\nconst interval = setInterval(draw, 33);\n\nwindow.addEventListener('resize', () => {\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n});" }, "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "static", "boxShadow": true, "shadowColor": "#000000", "shadowBlur": 48, "stroke": true, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": false, "color": "#ff00ff", "sensitivity": 20 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 236.85834502103788, "y": 204.9929873772791, "width": 837.6998597475456, "height": 293.015427769986, "zIndex": 3, "codeSize": 7, "codeSpeed": 1.6, "codeColor": "#00ff00" } }, { "id": "source-1781286682955-205524", "type": "code", "name": "New Code", "visible": true, "locked": false, "content": { "html": '', "css": "body { background-color: transparent; margin: 0; overflow: hidden; canvas { display: block; }", "js": "const canvas = document.getElementById('canvas');\nconst ctx = canvas.getContext('2d');\nlet particles = [];\nfunction resize() {\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n particles = [];\n const particleCount = 200;\n for (let i = 0; i < particleCount; i++) {\n particles.push({\n x: Math.random() * canvas.width,\n y: Math.random() * canvas.height,\n radius: Math.random() * 4 + 1,\n speed: (Math.random() * 2 + 1) * 1,\n });\n }\n}\nfunction animate() {\n requestAnimationFrame(animate);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n particles.forEach(p => {\n p.y += p.speed;\n if (p.y > canvas.height) {\n p.y = -p.radius;\n p.x = Math.random() * canvas.width;\n }\n ctx.beginPath();\n ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);\n ctx.fillStyle = '#ffffff';\n ctx.fill();\n });\n}\nwindow.addEventListener('resize', resize);\nresize();\nanimate();" }, "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "static", "boxShadow": true, "shadowColor": "#000000", "shadowBlur": 53, "stroke": true, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": false, "color": "#ff00ff", "sensitivity": 20 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 243.02945301542775, "y": 606.6058906030855, "width": 846.9004207573632, "height": 297.6437587657784, "zIndex": 4, "codeSize": 4, "codeSpeed": 1, "codeColor": "#ffffff" } }, { "id": "default-video-animation", "type": "video", "name": "Video Example", "visible": true, "locked": false, "content": "https://i.imgur.com/Vh2UVFw.mp4", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "static", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": true, "color": "#000000", "sensitivity": 30 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 244.27068723702666, "y": 606.9004207573632, "width": 400, "height": 300, "zIndex": 5 } } ] }, { "id": "scene-3", "name": "Background Scene", "icon": "\u{1F5BC}\uFE0F", "backgroundColor": "#000000", "sources": [ { "id": "scene3-background", "type": "image", "name": "Background", "visible": true, "locked": false, "content": "https://i.imgur.com/hYM563K.png", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "fill", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": false, "color": "#ff00ff", "sensitivity": 20 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 0, "y": 0, "width": 1920, "height": 1080, "zIndex": 0 } }, { "id": "scene3-greenscreen-video", "type": "video", "name": "Video Example", "visible": true, "locked": false, "content": "https://i.imgur.com/Vh2UVFw.mp4", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": -1, "scaleY": 1, "resizeBehavior": "center", "boxShadow": false, "shadowColor": "#000000", "shadowBlur": 15, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": true, "color": "#000000", "sensitivity": 30 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "width": 938.569424964937, "height": 663.5343618513324, "x": 929.649368863955, "y": 381.92145862552604, "zIndex": 1 } }, { "id": "default-logo", "type": "image", "name": "Logo", "visible": true, "locked": false, "content": "https://i.imgur.com/vXi6tuT.png", "style": { ...defaultSourceStyle, "opacity": 1, "scale": 1, "rotation": 0, "scaleX": 1, "scaleY": 1, "resizeBehavior": "center", "boxShadow": false, "shadowColor": "rgba(0,0,0,0.5)", "shadowBlur": 20, "stroke": false, "strokeColor": "#ffffff", "strokeWidth": 2, "fontSize": 16, "textColor": "#ffffff", "fontWeight": "normal", "textAlign": "left", "loop": true, "playbackRate": 1, "volume": 1, "muted": true, "slideDuration": 5, "chromaKey": { "enabled": false, "color": "#00ff00", "sensitivity": 20 }, "colorKey": { "enabled": true, "color": "#292929", "sensitivity": 25 }, "filters": { "hue": 0, "blur": 0, "sharpness": 0, "shape": "none" }, "brushColor": "#ffffff", "brushSize": 5, "x": 45.21907433380102, "y": 50.42405329593271, "width": 860.7416549789622, "height": 511.02054698457226, "zIndex": 2 } } ] }, { "id": "scene-blank", "name": "Blank", "icon": "\u2B1B", "backgroundColor": "#000000", "sources": [] } ]; var App = () => { const [scenes, setScenes] = useState9(initialScenes); const [history, setHistory] = useState9([initialScenes]); const [historyIndex, setHistoryIndex] = useState9(0); const [activeSceneId, setActiveSceneId] = useState9("scene-1"); const [selectedSourceId, setSelectedSourceId] = useState9(null); const [filteringSourceId, setFilteringSourceId] = useState9(null); const [isUIVisible, setUIVisible] = useState9(true); const [isSnapToGridEnabled, setSnapToGridEnabled] = useState9(false); const [isWelcomeVisible, setWelcomeVisible] = useState9(true); const [theme, setTheme] = useState9(() => { return localStorage.getItem("theme") || "dark"; }); const [scrollbarColor, setScrollbarColor] = useState9(() => { return localStorage.getItem("scrollbarColor") || "#6366f1"; }); const [performancePreset, setPerformancePreset] = useState9(() => { return localStorage.getItem("performancePreset") || "high"; }); const [framerateLimit, setFramerateLimit] = useState9(() => { return localStorage.getItem("framerateLimit") || "uncapped"; }); const [renderScale, setRenderScale] = useState9(() => { return localStorage.getItem("renderScale") || "1.0"; }); const [interfaceAnimations, setInterfaceAnimations] = useState9(() => { return localStorage.getItem("interfaceAnimations") || "enabled"; }); useEffect8(() => { localStorage.setItem("theme", theme); }, [theme]); useEffect8(() => { document.documentElement.style.setProperty("--scrollbar-thumb", scrollbarColor); localStorage.setItem("scrollbarColor", scrollbarColor); }, [scrollbarColor]); useEffect8(() => { localStorage.setItem("performancePreset", performancePreset); }, [performancePreset]); useEffect8(() => { localStorage.setItem("framerateLimit", framerateLimit); }, [framerateLimit]); useEffect8(() => { localStorage.setItem("renderScale", renderScale); }, [renderScale]); useEffect8(() => { localStorage.setItem("interfaceAnimations", interfaceAnimations); }, [interfaceAnimations]); const [activeTab, setActiveTab] = useState9("sources"); const [canvasResolution, setCanvasResolution] = useState9(() => { return localStorage.getItem("canvasResolution") || "1080"; }); useEffect8(() => { localStorage.setItem("canvasResolution", canvasResolution); }, [canvasResolution]); const [isAutoHideEnabled, setAutoHideEnabled] = useState9(false); const [contextMenu, setContextMenu] = useState9({ visible: false, x: 0, y: 0, sourceId: "" }); const [isAddingScene, setIsAddingScene] = useState9(false); const [editingSceneId, setEditingSceneId] = useState9(null); const [sourceToDelete, setSourceToDelete] = useState9(null); const [copiedSource, setCopiedSource] = useState9(null); const [confirmModal, setConfirmModal] = useState9(null); const [isHotkeysEnabled, setHotkeysEnabled] = useState9(true); const [centerHotkey, setCenterHotkey] = useState9("shift+f"); const [fullscreenHotkey, setFullscreenHotkey] = useState9("shift+s"); const [lockHotkey, setLockHotkey] = useState9("shift+l"); const [visibilityHotkey, setVisibilityHotkey] = useState9("shift+v"); const [contextMenuTheme, setContextMenuTheme] = useState9(() => { const saved = localStorage.getItem("contextMenuTheme"); return saved || "match"; }); const [sceneHotkeys, setSceneHotkeys] = useState9(() => { try { const saved = localStorage.getItem("sceneHotkeys"); return saved ? JSON.parse(saved) : {}; } catch { return {}; } }); useEffect8(() => { localStorage.setItem("contextMenuTheme", contextMenuTheme); }, [contextMenuTheme]); useEffect8(() => { localStorage.setItem("sceneHotkeys", JSON.stringify(sceneHotkeys)); }, [sceneHotkeys]); const handleSetSceneHotkey = (sceneId, hotkey) => { setSceneHotkeys((prev) => ({ ...prev, [sceneId]: hotkey })); logAction(`Bound hotkey "${hotkey}" to scene ID "${sceneId}"`); }; const [sceneTransition, setSceneTransition] = useState9(() => { try { const saved = localStorage.getItem("sceneTransition"); return saved ? JSON.parse(saved) : { type: "move", speed: 5e3, style: "ease-in-out" }; } catch { return { type: "move", speed: 5e3, style: "ease-in-out" }; } }); useEffect8(() => { localStorage.setItem("sceneTransition", JSON.stringify(sceneTransition)); }, [sceneTransition]); const [isTransitioning, setIsTransitioning] = useState9(false); const [currentPlayingTransition, setCurrentPlayingTransition] = useState9(null); const stingerVideoRef = useRef9(null); const transitionTimer1Ref = useRef9(null); const transitionTimer2Ref = useRef9(null); const stingerCleanupRef = useRef9(null); const [isPoppedOut, setIsPoppedOut] = useState9(true); const [panelWidth, setPanelWidth] = useState9(400); const [isHoveredOverLeft, setIsHoveredOverLeft] = useState9(false); const [isMobileMode, setMobileMode] = useState9(() => { return localStorage.getItem("isMobileMode") === "true"; }); const [actionLog, setActionLog] = useState9([]); const [previewScalingMode, setPreviewScalingMode] = useState9("fit"); const [windowSize, setWindowSize] = useState9({ width: window.innerWidth, height: window.innerHeight }); const prevWindowSize = useRef9({ width: window.innerWidth, height: window.innerHeight }); const getResolutionDimensions2 = (res) => { switch (res) { case "240": return { width: 426, height: 240 }; case "480": return { width: 854, height: 480 }; case "720": return { width: 1280, height: 720 }; case "1080": return { width: 1920, height: 1080 }; case "2k": return { width: 2560, height: 1440 }; case "4k": return { width: 3840, height: 2160 }; default: return { width: 1920, height: 1080 }; } }; const isDashboardOccupyingSpace = isUIVisible && !isPoppedOut && (!isAutoHideEnabled || isHoveredOverLeft); const remainingWidth = isDashboardOccupyingSpace ? windowSize.width - panelWidth : windowSize.width; const { width: canvasWidth, height: canvasHeight } = getResolutionDimensions2(canvasResolution); const margin = 45; const maxW = Math.max(200, remainingWidth - margin); const maxH = Math.max(200, windowSize.height - margin); const canvasScaleFit = Math.min(maxW / canvasWidth, maxH / canvasHeight); let canvasScale = canvasScaleFit; if (previewScalingMode === "100") canvasScale = 1; else if (previewScalingMode === "75") canvasScale = 0.75; else if (previewScalingMode === "50") canvasScale = 0.5; else if (previewScalingMode === "25") canvasScale = 0.25; const canvasLeft = isDashboardOccupyingSpace ? panelWidth + remainingWidth / 2 : windowSize.width / 2; const canvasTop = windowSize.height / 2; const activeScene = scenes.find((scene) => scene.id === activeSceneId); const selectedSource = activeScene?.sources.find((s) => s.id === selectedSourceId); const filteringSource = activeScene?.sources.find((s) => s.id === filteringSourceId); const editingScene = scenes.find((s) => s.id === editingSceneId); useEffect8(() => { if (filteringSourceId && activeTab !== "filters") { } else if (!filteringSourceId && activeTab === "filters") { setActiveTab("sources"); } }, [filteringSourceId, activeTab]); useEffect8(() => { if (!selectedSourceId && activeTab === "settings") { setActiveTab("sources"); } }, [selectedSourceId, activeTab]); const logAction = useCallback3((message) => { setActionLog((prevLog) => [ { timestamp: Date.now(), message }, ...prevLog.slice(0, 199) // Keep last 200 actions ]); try { const saved = localStorage.getItem("creative_canvas_feature_usages"); const featureCounters = saved ? JSON.parse(saved) : {}; const msg = message.toLowerCase(); let key = "Source Customization"; if (msg.includes("scene") || msg.includes("layout") || msg.includes("transition")) { key = "Scene Management & Transitions"; } else if (msg.includes("position") || msg.includes("center") || msg.includes("fullscreen") || msg.includes("interaction")) { key = "Source Positioning & Canvas Layout"; } else if (msg.includes("copy") || msg.includes("paste")) { key = "Clipboard Operations (Copy/Paste)"; } else if (msg.includes("filter") || msg.includes("chroma") || msg.includes("colorkey")) { key = "Applying Chroma/Color Filters"; } else if (msg.includes("export") || msg.includes("import") || msg.includes("backup")) { key = "Workspace Exports/Imports"; } else if (msg.includes("theme") || msg.includes("hotkey") || msg.includes("mobile") || msg.includes("grid")) { key = "Interface & Theme Configurations"; } featureCounters[key] = (featureCounters[key] || 0) + 1; localStorage.setItem("creative_canvas_feature_usages", JSON.stringify(featureCounters)); } catch { } }, []); const clearActionLog = () => { setActionLog([]); logAction("Log cleared."); }; const setScenesWithHistory = useCallback3((newScenes, actionMessage) => { setScenes((prevScenes) => { const resolvedScenes = typeof newScenes === "function" ? newScenes(prevScenes) : newScenes; const targetCompare = history[historyIndex] || prevScenes; if (JSON.stringify(resolvedScenes) === JSON.stringify(targetCompare)) return prevScenes; const newHistory = history.slice(0, historyIndex + 1); newHistory.push(resolvedScenes); setTimeout(() => { setHistory(newHistory); setHistoryIndex(newHistory.length - 1); if (actionMessage) logAction(actionMessage); }, 0); return resolvedScenes; }); }, [history, historyIndex, logAction]); const undo = () => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); setScenes(history[newIndex]); logAction("Undo action performed."); } }; const redo = () => { if (historyIndex < history.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); setScenes(history[newIndex]); logAction("Redo action performed."); } }; const canUndo = historyIndex > 0; const canRedo = historyIndex < history.length - 1; const updateSource = useCallback3((sourceId, updatedSource) => { let actionMessage = `Updated source ${sourceId}.`; const resolvedUpdate = typeof updatedSource === "function" ? updatedSource(scenes.find((s) => s.id === activeSceneId)?.sources.find((src) => src.id === sourceId)) : updatedSource; if ("name" in resolvedUpdate) actionMessage = `Renamed source ${sourceId} to "${resolvedUpdate.name}".`; if ("visible" in resolvedUpdate) actionMessage = `${resolvedUpdate.visible ? "Shown" : "Hid"} source ${sourceId}.`; if ("locked" in resolvedUpdate) actionMessage = `${resolvedUpdate.locked ? "Locked" : "Unlocked"} source ${sourceId}.`; setScenesWithHistory( (prevScenes) => prevScenes.map( (scene) => scene.id === activeSceneId ? { ...scene, sources: scene.sources.map( (source) => source.id === sourceId ? { ...source, ...resolvedUpdate } : source ) } : scene ), actionMessage ); }, [activeSceneId, setScenesWithHistory, scenes]); const updateSourceStyle = useCallback3((sourceId, updatedStyle) => { setScenes( (prevScenes) => prevScenes.map( (scene) => scene.id === activeSceneId ? { ...scene, sources: scene.sources.map( (source) => source.id === sourceId ? { ...source, style: { ...source.style, ...updatedStyle } } : source ) } : scene ) ); }, [activeSceneId]); const handleInteractionEnd = useCallback3(() => { logAction("Finished canvas interaction (move/resize)."); setScenesWithHistory(scenes); }, [scenes, setScenesWithHistory, logAction]); const addSource = async (type) => { const activeSceneSources = scenes.find((s) => s.id === activeSceneId)?.sources || []; let newSource; let sourceTypeNameForLog = type; let targetWidth = canvasWidth * 0.5; let targetHeight = canvasHeight * 0.5; let targetX = (canvasWidth - targetWidth) / 2; let targetY = (canvasHeight - targetHeight) / 2; if (type === "paint" || type === "visualizer") { targetWidth = canvasWidth; targetHeight = canvasHeight; targetX = 0; targetY = 0; } const fitToBoundingBox = (origW, origH, maxW2, maxH2) => { const ratio = origW / origH; let fitW = maxW2; let fitH = maxW2 / ratio; if (fitH > maxH2) { fitH = maxH2; fitW = maxH2 * ratio; } return { width: fitW, height: fitH }; }; if (type === "iframe") { sourceTypeNameForLog = "Web"; const webUrl = ""; newSource = { id: `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}`, type: "iframe", name: "Web Source", visible: true, locked: false, content: webUrl, style: { ...defaultSourceStyle, x: targetX, y: targetY, width: targetWidth, height: targetHeight, zIndex: Math.max(0, ...activeSceneSources.map((src) => src.style.zIndex)) + 1 } }; } else if (type === "youtube") { sourceTypeNameForLog = "YouTube"; const ytUrl = ""; newSource = { id: `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}`, type: "youtube", name: "YouTube Player", visible: true, locked: false, content: ytUrl, style: { ...defaultSourceStyle, x: targetX, y: targetY, width: targetWidth, height: targetHeight, zIndex: Math.max(0, ...activeSceneSources.map((src) => src.style.zIndex)) + 1 } }; } else if (type === "twitch") { sourceTypeNameForLog = "Twitch"; newSource = { id: `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}`, type: "twitch", name: "Twitch Player", visible: true, locked: false, content: { channel: "albiar", parent: window.location.hostname || "localhost", urlInput: "https://www.twitch.tv/albiar", layout: "video-with-chat", autoplay: true, muted: false, theme: "dark" }, style: { ...defaultSourceStyle, x: targetX, y: targetY, width: targetWidth, height: targetHeight, zIndex: Math.max(0, ...activeSceneSources.map((src) => src.style.zIndex)) + 1 } }; } else if (type === "image") { const imageUrl = ""; const getAspectForImage = (url) => { return new Promise((resolve) => { if (!url) { resolve({ width: 400, height: 300 }); return; } const img = new Image(); img.src = url; img.onload = () => resolve({ width: img.naturalWidth || 400, height: img.naturalHeight || 300 }); img.onerror = () => resolve({ width: 400, height: 300 }); }); }; const dims = await getAspectForImage(imageUrl); const fitDims = fitToBoundingBox(dims.width, dims.height, targetWidth, targetHeight); targetWidth = fitDims.width; targetHeight = fitDims.height; targetX = (canvasWidth - targetWidth) / 2; targetY = (canvasHeight - targetHeight) / 2; newSource = { id: `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}`, type: "image", name: "Image Source", visible: true, locked: false, content: imageUrl, style: { ...defaultSourceStyle, x: targetX, y: targetY, width: targetWidth, height: targetHeight, zIndex: Math.max(0, ...activeSceneSources.map((src) => src.style.zIndex)) + 1, colorKey: { enabled: true, color: "#000000", sensitivity: 20 } } }; } else if (type === "video") { const videoUrl = "https://i.imgur.com/Vh2UVFw.mp4"; const getAspectForVideo = (url) => { return new Promise((resolve) => { if (!url) { resolve({ width: 640, height: 360 }); return; } const video = document.createElement("video"); video.src = url; video.onloadedmetadata = () => resolve({ width: video.videoWidth || 640, height: video.videoHeight || 360 }); video.onerror = () => resolve({ width: 640, height: 360 }); }); }; const dims = await getAspectForVideo(videoUrl); const fitDims = fitToBoundingBox(dims.width, dims.height, targetWidth, targetHeight); targetWidth = fitDims.width; targetHeight = fitDims.height; targetX = (canvasWidth - targetWidth) / 2; targetY = (canvasHeight - targetHeight) / 2; newSource = { id: `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}`, type: "video", name: "Video Source", visible: true, locked: false, content: videoUrl, style: { ...defaultSourceStyle, x: targetX, y: targetY, width: targetWidth, height: targetHeight, zIndex: Math.max(0, ...activeSceneSources.map((src) => src.style.zIndex)) + 1, loop: true, playbackRate: 1, volume: 1, muted: true } }; } else { let content = ""; if (type === "text") content = "New Text"; if (type === "color") content = "#4a5568"; if (type === "gallery") content = []; if (type === "paint") content = ""; if (type === "camera") content = ""; if (type === "capture") content = ""; if (type === "visualizer") content = "visualizer"; if (type === "emoji") { content = "\u{1F680}"; } if (type === "code") { content = { html: '', css: `body { margin: 0; background-color: transparent; overflow: hidden; } canvas { display: block; }`, js: `// JS code here. Access canvas with: document.getElementById('canvas');` }; } const isVisualizer = type === "visualizer"; newSource = { id: `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}`, type, name: type === "emoji" ? "Emoji" : isVisualizer ? "Audio Visualizer" : `New ${type.charAt(0).toUpperCase() + type.slice(1)}`, visible: true, locked: false, content, style: { ...defaultSourceStyle, x: targetX, y: targetY, width: targetWidth, height: targetHeight, zIndex: Math.max(0, ...activeSceneSources.map((src) => src.style.zIndex)) + 1, ...isVisualizer && { visualizerStyle: "bars", visualizerColor: "#3b82f6", innerGlow: true, innerGlowColor: "#3b82f6", innerGlowBlur: 15, visualizerTransparent: true }, ...type === "emoji" && { fontSize: 120, textAlign: "center" } } }; } setScenesWithHistory((prevScenes) => prevScenes.map( (scene) => scene.id === activeSceneId ? { ...scene, sources: [...scene.sources, newSource] } : scene ), `Added new ${sourceTypeNameForLog} source.`); setSelectedSourceId(newSource.id); setActiveTab("settings"); }; const deleteSource = (sourceId) => { const sourceName = scenes.find((s) => s.id === activeSceneId)?.sources.find((src) => src.id === sourceId)?.name; setScenesWithHistory((prevScenes) => prevScenes.map( (scene) => scene.id === activeSceneId ? { ...scene, sources: scene.sources.filter((s) => s.id !== sourceId) } : scene ), `Deleted source: ${sourceName || sourceId}`); if (selectedSourceId === sourceId) { setSelectedSourceId(null); } }; const reorderSources = (orderedSourceIds) => { setScenesWithHistory( (prevScenes) => prevScenes.map((scene) => { if (scene.id !== activeSceneId) return scene; const maxZIndex = scene.sources.length; const newSources = scene.sources.map((source) => { const newIndex = orderedSourceIds.findIndex((id) => id === source.id); const newZIndex = maxZIndex - newIndex; return { ...source, style: { ...source.style, zIndex: newZIndex } }; }); return { ...scene, sources: newSources }; }), "Reordered sources." ); }; const updateSourceLayer = (sourceId, direction) => { setScenesWithHistory((prevScenes) => { const newScenes = [...prevScenes]; const sceneIndex = newScenes.findIndex((s) => s.id === activeSceneId); if (sceneIndex === -1) return prevScenes; const scene = newScenes[sceneIndex]; const sourcesSorted = [...scene.sources].sort((a, b) => a.style.zIndex - b.style.zIndex); const currentIndex = sourcesSorted.findIndex((s) => s.id === sourceId); if (currentIndex === -1) return prevScenes; let otherIndex = -1; if (direction === "up" && currentIndex < sourcesSorted.length - 1) { otherIndex = currentIndex + 1; } else if (direction === "down" && currentIndex > 0) { otherIndex = currentIndex - 1; } if (otherIndex === -1) return prevScenes; const sourceToMove = sourcesSorted[currentIndex]; const otherSource = sourcesSorted[otherIndex]; const newSources = scene.sources.map((s) => { if (s.id === sourceToMove.id) { return { ...s, style: { ...s.style, zIndex: otherSource.style.zIndex } }; } if (s.id === otherSource.id) { return { ...s, style: { ...s.style, zIndex: sourceToMove.style.zIndex } }; } return s; }); newScenes[sceneIndex] = { ...scene, sources: newSources }; return newScenes; }, `Moved source ${sourceId} ${direction}.`); }; const toggleSourceVisibility = (sourceId) => { updateSource(sourceId, (s) => ({ visible: !s.visible })); }; const toggleSourceLock = (sourceId) => { updateSource(sourceId, (s) => ({ locked: !s.locked })); }; const centerSource = (sourceId) => { const source = scenes.flatMap((s) => s.sources).find((s) => s.id === sourceId); if (!source) return; const newX = canvasWidth / 2 - source.style.width / 2; const newY = canvasHeight / 2 - source.style.height / 2; updateSource(sourceId, (s) => ({ style: { ...s.style, x: newX, y: newY } })); logAction(`Centered source ${sourceId}.`); }; const fullscreenSource = (sourceId) => { updateSource(sourceId, (s) => { const isCurrentlyFullscreen = s.style.x === 0 && s.style.y === 0 && s.style.width === canvasWidth && s.style.height === canvasHeight; if (isCurrentlyFullscreen) { logAction(`Restored source ${sourceId} from fullscreen.`); if (s.preFullscreenStyle) { const { x, y, width, height } = s.preFullscreenStyle; return { style: { ...s.style, x, y, width, height } }; } return {}; } else { logAction(`Made source ${sourceId} fullscreen.`); const preFullscreenStyle = { x: s.style.x, y: s.style.y, width: s.style.width, height: s.style.height }; return { preFullscreenStyle, style: { ...s.style, x: 0, y: 0, width: canvasWidth, height: canvasHeight } }; } }); }; const handleConfirmAddScene = (name, icon) => { const newSceneId = `scene-${Date.now()}`; const newScene = { id: newSceneId, name, icon, sources: [], backgroundColor: "#000000" }; setScenesWithHistory([...scenes, newScene], `Added new scene: ${name}`); setActiveSceneId(newSceneId); setIsAddingScene(false); }; const deleteScene = (sceneId) => { if (scenes.length <= 1) { alert("You cannot delete the last scene."); return; } const sceneName = scenes.find((s) => s.id === sceneId)?.name; const newScenes = scenes.filter((s) => s.id !== sceneId); setScenesWithHistory(newScenes, `Deleted scene: ${sceneName || sceneId}`); if (activeSceneId === sceneId) { handleSetActiveScene(newScenes[0].id); } }; const updateScene = (sceneId, updates) => { if (updates.backgroundColor) { logAction(`Changed background color for scene ${sceneId}.`); } if (updates.name || updates.icon) { logAction(`Updated scene: ${updates.name || scenes.find((s) => s.id === sceneId)?.name}`); } setScenesWithHistory((prevScenes) => prevScenes.map((s) => s.id === sceneId ? { ...s, ...updates } : s)); }; const handleDuplicateScene = (sceneId) => { const sceneToDuplicate = scenes.find((s) => s.id === sceneId); if (!sceneToDuplicate) return; const newSceneId = `scene-${Date.now()}`; const duplicatedSources = sceneToDuplicate.sources.map((src) => { return { ...src }; }); const duplicatedScene = { ...sceneToDuplicate, id: newSceneId, name: `${sceneToDuplicate.name} (Copy)`, sources: duplicatedSources }; setScenesWithHistory([...scenes, duplicatedScene], `Duplicated scene: ${sceneToDuplicate.name}`); setActiveSceneId(newSceneId); }; const handleCopySource = useCallback3((sourceId) => { const src = scenes.find((s) => s.id === activeSceneId)?.sources.find((x) => x.id === sourceId); if (src) { setCopiedSource(src); logAction(`Copied source: ${src.name}`); } }, [activeSceneId, scenes, logAction]); const handlePasteSource = useCallback3(() => { if (!copiedSource) return; const activeSc = scenes.find((s) => s.id === activeSceneId); if (!activeSc) return; const idExists = activeSc.sources.some((s) => s.id === copiedSource.id); const newId = idExists ? `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}` : copiedSource.id; const pastedSource = { ...copiedSource, id: newId, name: idExists ? `${copiedSource.name} (Copy)` : copiedSource.name, style: idExists ? { ...copiedSource.style, x: copiedSource.style.x + 30, y: copiedSource.style.y + 30, zIndex: Math.max(0, ...activeSc.sources.map((src) => src.style.zIndex)) + 1 } : { ...copiedSource.style, zIndex: Math.max(0, ...activeSc.sources.map((src) => src.style.zIndex)) + 1 } }; setScenesWithHistory((prevScenes) => prevScenes.map((sc) => { if (sc.id === activeSceneId) { return { ...sc, sources: [...sc.sources, pastedSource] }; } return sc; }), `Pasted source: ${pastedSource.name}`); setSelectedSourceId(newId); }, [copiedSource, activeSceneId, scenes, setScenesWithHistory, logAction, setSelectedSourceId]); const handleSetActiveScene = (sceneId) => { if (sceneId === activeSceneId) return; if (transitionTimer1Ref.current) { clearTimeout(transitionTimer1Ref.current); transitionTimer1Ref.current = null; } if (transitionTimer2Ref.current) { clearTimeout(transitionTimer2Ref.current); transitionTimer2Ref.current = null; } if (stingerCleanupRef.current) { try { stingerCleanupRef.current(); } catch (e) { } stingerCleanupRef.current = null; } setIsTransitioning(false); setCurrentPlayingTransition(null); const targetScene = scenes.find((s) => s.id === sceneId); if (!targetScene) return; const activeTransition = targetScene.transition || sceneTransition; logAction(`Switched layout view to scene: ${targetScene.name || sceneId} using ${activeTransition.type || "cut"} transition.`); const transitionDuration = activeTransition.speed ?? 500; setCurrentPlayingTransition(activeTransition); switch (activeTransition.type) { case "fade": setIsTransitioning(true); transitionTimer1Ref.current = setTimeout(() => { setActiveSceneId(sceneId); transitionTimer2Ref.current = setTimeout(() => { setIsTransitioning(false); setCurrentPlayingTransition(null); }, 50); }, transitionDuration); break; case "move": setIsTransitioning(true); setActiveSceneId(sceneId); transitionTimer1Ref.current = setTimeout(() => { setIsTransitioning(false); setCurrentPlayingTransition(null); }, transitionDuration); break; case "stinger": if (activeTransition.stingerFile && stingerVideoRef.current) { setIsTransitioning(true); const video = stingerVideoRef.current; video.playbackRate = activeTransition.stingerSpeed || 1; const startSec = activeTransition.stingerStart || 0; const stopSec = activeTransition.stingerStop; const onTimeUpdate = () => { const durationToUse = stopSec ? stopSec - startSec : video.duration; const midwayPt = startSec + durationToUse / 2; if (video.currentTime >= midwayPt) { setActiveSceneId(sceneId); } if (stopSec && video.currentTime >= stopSec) { video.pause(); onEnded(); } }; const onEnded = () => { setIsTransitioning(false); setCurrentPlayingTransition(null); video.removeEventListener("ended", onEnded); video.removeEventListener("timeupdate", onTimeUpdate); stingerCleanupRef.current = null; }; const cleanup = () => { video.pause(); video.removeEventListener("ended", onEnded); video.removeEventListener("timeupdate", onTimeUpdate); }; stingerCleanupRef.current = cleanup; video.addEventListener("timeupdate", onTimeUpdate); video.addEventListener("ended", onEnded); video.currentTime = startSec; video.play().catch((e) => { setActiveSceneId(sceneId); setIsTransitioning(false); setCurrentPlayingTransition(null); if (stingerCleanupRef.current === cleanup) { stingerCleanupRef.current = null; } }); } else { setActiveSceneId(sceneId); setCurrentPlayingTransition(null); } break; case "cut": default: setActiveSceneId(sceneId); setCurrentPlayingTransition(null); break; } }; const exportToFile = (content, fileName) => { const jsonString = JSON.stringify(content, null, 2); const blob = new Blob([jsonString], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleImportFromFile = (onFileRead) => { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.style.display = "none"; document.body.appendChild(input); input.onchange = (event) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { onFileRead(e.target?.result); }; reader.readAsText(file); } document.body.removeChild(input); }; input.click(); setTimeout(() => { if (document.body.contains(input)) { document.body.removeChild(input); } }, 6e4); }; const exportSources = () => { const scene = scenes.find((s) => s.id === activeSceneId); if (scene) { exportToFile(scene.sources, "sources.json"); logAction(`Exported sources for scene ${scene.name}.`); } }; const importSources = () => { handleImportFromFile((content) => { try { const importedSources = JSON.parse(content); if (!Array.isArray(importedSources)) throw new Error("Invalid format"); setScenesWithHistory((prev) => prev.map((scene) => { if (scene.id === activeSceneId) { const existingIds = new Set(scene.sources.map((s) => s.id)); const processedSources = importedSources.map((src) => { const idCollision = existingIds.has(src.id); const uniqueId = idCollision ? `source-${Date.now()}-${Math.floor(Math.random() * 1e6)}` : src.id; existingIds.add(uniqueId); return { ...src, id: uniqueId, name: idCollision ? `${src.name} (Copy)` : src.name }; }); return { ...scene, sources: [...scene.sources, ...processedSources] }; } return scene; }), `Imported ${importedSources.length} sources.`); } catch (e) { alert("Failed to import sources. Invalid file format."); logAction("Failed to import sources."); } }); }; const exportProject = () => { const projectData = { type: "creative_canvas_project", scenes, sceneTransition, canvasResolution, theme, scrollbarColor, isMobileMode, framerateLimit, renderScale, interfaceAnimations, contextMenuTheme }; exportToFile(projectData, "project.json"); logAction("Exported entire project with transition and canvas settings."); }; const importProject = () => { setConfirmModal({ isOpen: true, title: "Replace Entire Project", message: "This will replace your entire project. All current scenes will be overwritten. Proceed with file selection?", onConfirm: () => { setConfirmModal(null); handleImportFromFile((content) => { try { const parsed = JSON.parse(content); let importedScenes = []; if (Array.isArray(parsed)) { importedScenes = parsed; } else if (parsed && parsed.type === "creative_canvas_project") { importedScenes = parsed.scenes; if (parsed.sceneTransition) setSceneTransition(parsed.sceneTransition); if (parsed.theme) setTheme(parsed.theme); if (parsed.scrollbarColor) setScrollbarColor(parsed.scrollbarColor); if (parsed.canvasResolution) setCanvasResolution(parsed.canvasResolution); if (parsed.isMobileMode !== void 0) setMobileMode(parsed.isMobileMode); if (parsed.framerateLimit) setFramerateLimit(parsed.framerateLimit); if (parsed.renderScale) setRenderScale(parsed.renderScale); if (parsed.interfaceAnimations) setInterfaceAnimations(parsed.interfaceAnimations); if (parsed.contextMenuTheme) setContextMenuTheme(parsed.contextMenuTheme); } else if (parsed && (parsed.scenes || parsed.sources)) { importedScenes = parsed.scenes || []; } else { throw new Error("Invalid format"); } if (!importedScenes || importedScenes.length === 0) throw new Error("No scenes found"); setScenesWithHistory(importedScenes, "Imported project from file."); setActiveSceneId(importedScenes[0].id); setSelectedSourceId(null); logAction("Imported complete project including matching scene transitions and settings."); } catch (e) { alert("Failed to import project. Invalid file format."); logAction("Failed to import project."); } }); } }); }; const exportCanvasSettings = () => { const settings = { type: "canvas_settings_only", theme, scrollbarColor, canvasResolution, isMobileMode, centerHotkey, fullscreenHotkey, lockHotkey, visibilityHotkey, performancePreset, framerateLimit, renderScale, interfaceAnimations, contextMenuTheme, sceneHotkeys, sceneTransition }; exportToFile(settings, "canvas-settings.json"); logAction("Exported Canvas individual settings and global transitions."); }; const importCanvasSettings = () => { handleImportFromFile((content) => { try { const parsed = JSON.parse(content); if (parsed.theme) setTheme(parsed.theme); if (parsed.scrollbarColor) setScrollbarColor(parsed.scrollbarColor); if (parsed.canvasResolution) setCanvasResolution(parsed.canvasResolution); if (parsed.isMobileMode !== void 0) setMobileMode(parsed.isMobileMode); if (parsed.centerHotkey) setCenterHotkey(parsed.centerHotkey); if (parsed.fullscreenHotkey) setFullscreenHotkey(parsed.fullscreenHotkey); if (parsed.lockHotkey) setLockHotkey(parsed.lockHotkey); if (parsed.visibilityHotkey) setVisibilityHotkey(parsed.visibilityHotkey); if (parsed.performancePreset) setPerformancePreset(parsed.performancePreset); if (parsed.framerateLimit) setFramerateLimit(parsed.framerateLimit); if (parsed.renderScale) setRenderScale(parsed.renderScale); if (parsed.interfaceAnimations) setInterfaceAnimations(parsed.interfaceAnimations); if (parsed.contextMenuTheme) setContextMenuTheme(parsed.contextMenuTheme); if (parsed.sceneHotkeys) setSceneHotkeys(parsed.sceneHotkeys); if (parsed.sceneTransition) setSceneTransition(parsed.sceneTransition); logAction("Imported Canvas individual settings and transitions successfully."); alert("Canvas Settings & Transitions Imported successfully!"); } catch (e) { alert("Failed to import canvas settings. Invalid format."); logAction("Failed to import canvas settings."); } }); }; const exportMasterBackup = () => { const backup = { type: "creative_canvas_master_backup", version: "1.0", scenes, activeSceneId, theme, scrollbarColor, canvasResolution, isMobileMode, centerHotkey, fullscreenHotkey, lockHotkey, visibilityHotkey, performancePreset, framerateLimit, renderScale, interfaceAnimations, contextMenuTheme, sceneHotkeys, sceneTransition }; exportToFile(backup, "creative-canvas-full-backup.json"); logAction("Exported ENTIRE Creative Canvas master workspace, setup, and transitions."); }; const importMasterBackup = () => { setConfirmModal({ isOpen: true, title: "Restore Master Backup", message: "This will overwrite all active scenes, sources, layers, hotkeys, and performance parameters. Proceed with file selection?", onConfirm: () => { setConfirmModal(null); handleImportFromFile((content) => { try { const parsed = JSON.parse(content); if (parsed.type !== "creative_canvas_master_backup") { throw new Error("Invalid backup signature"); } if (Array.isArray(parsed.scenes)) { setScenesWithHistory(parsed.scenes, "Imported full master workspace."); if (parsed.activeSceneId) setActiveSceneId(parsed.activeSceneId); } if (parsed.theme) setTheme(parsed.theme); if (parsed.scrollbarColor) setScrollbarColor(parsed.scrollbarColor); if (parsed.canvasResolution) setCanvasResolution(parsed.canvasResolution); if (parsed.isMobileMode !== void 0) setMobileMode(parsed.isMobileMode); if (parsed.centerHotkey) setCenterHotkey(parsed.centerHotkey); if (parsed.fullscreenHotkey) setFullscreenHotkey(parsed.fullscreenHotkey); if (parsed.lockHotkey) setLockHotkey(parsed.lockHotkey); if (parsed.visibilityHotkey) setVisibilityHotkey(parsed.visibilityHotkey); if (parsed.performancePreset) setPerformancePreset(parsed.performancePreset); if (parsed.framerateLimit) setFramerateLimit(parsed.framerateLimit); if (parsed.renderScale) setRenderScale(parsed.renderScale); if (parsed.interfaceAnimations) setInterfaceAnimations(parsed.interfaceAnimations); if (parsed.contextMenuTheme) setContextMenuTheme(parsed.contextMenuTheme); if (parsed.sceneHotkeys) setSceneHotkeys(parsed.sceneHotkeys); if (parsed.sceneTransition) setSceneTransition(parsed.sceneTransition); logAction("Imported ENTIRE master Creative Canvas workspace setup and transitions."); alert("Creative Canvas Workspace Fully Restored!"); } catch (e) { alert("Failed to restore master backup. Invalid or corrupt file format."); logAction("Failed to restore master backup."); } }); } }); }; const updateSourceStyleWithHistory = (sourceId, style) => { updateSource(sourceId, (s) => ({ style: { ...s.style, ...style } })); logAction(`Updated style for source ${sourceId}.`); }; const handleOpenEditSceneModal = (sceneId) => { setEditingSceneId(sceneId); }; const handleCloseEditSceneModal = () => { setEditingSceneId(null); }; const handleConfirmEditScene = (sceneId, name, icon) => { updateScene(sceneId, { name, icon }); setEditingSceneId(null); }; const handleSourceContextMenu = useCallback3((e, sourceId) => { e.preventDefault(); e.stopPropagation(); const menuWidth = 256; const menuHeight = 420; let x = e.clientX; let y = e.clientY; if (x + menuWidth > window.innerWidth) { x = Math.max(10, window.innerWidth - menuWidth - 10); } if (y + menuHeight > window.innerHeight) { y = Math.max(10, window.innerHeight - menuHeight - 10); } setContextMenu({ visible: true, x, y, sourceId }); }, []); const handleCanvasContextMenu = useCallback3((e) => { e.preventDefault(); e.stopPropagation(); const menuWidth = 256; const menuHeight = 180; let x = e.clientX; let y = e.clientY; if (x + menuWidth > window.innerWidth) { x = Math.max(10, window.innerWidth - menuWidth - 10); } if (y + menuHeight > window.innerHeight) { y = Math.max(10, window.innerHeight - menuHeight - 10); } setContextMenu({ visible: true, x, y, sourceId: "" // Empty means background context menu }); }, []); useEffect8(() => { const handleCloseMenu = () => { setContextMenu((prev) => prev.visible ? { ...prev, visible: false } : prev); }; window.addEventListener("click", handleCloseMenu); window.addEventListener("contextmenu", handleCloseMenu); return () => { window.removeEventListener("click", handleCloseMenu); window.removeEventListener("contextmenu", handleCloseMenu); }; }, []); useEffect8(() => { const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); useEffect8(() => { logAction("Application loaded."); }, [logAction]); useEffect8(() => { const handleKeyDown = (e) => { const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.getAttribute("contenteditable") === "true")) { return; } if (e.key === "Delete" && selectedSourceId) { setSourceToDelete(selectedSourceId); return; } if (!isHotkeysEnabled) return; if (e.shiftKey) { const isDigit = /^Digit[1-9]$/.test(e.code) || /^[1-9]$/.test(e.key); if (isDigit) { const match = e.code.match(/\d/) || e.key.match(/\d/); if (match) { e.preventDefault(); const sceneIndex = parseInt(match[0], 10) - 1; if (scenes[sceneIndex]) { handleSetActiveScene(scenes[sceneIndex].id); } return; } } if (e.code === "Digit0" || e.key === "0") { e.preventDefault(); const newSceneId = `scene-${Date.now()}`; const newSceneName = `Scene ${scenes.length + 1}`; const newScene = { id: newSceneId, name: newSceneName, icon: "\u{1F3AC}", sources: [], backgroundColor: "#000000" }; setScenesWithHistory([...scenes, newScene], `Added new scene: ${newSceneName}`); handleSetActiveScene(newSceneId); return; } } const keysPressed = []; if (e.ctrlKey) keysPressed.push("ctrl"); if (e.metaKey) keysPressed.push("meta"); if (e.altKey) keysPressed.push("alt"); if (e.shiftKey) keysPressed.push("shift"); const keyName = e.key.toLowerCase(); if (keyName !== "control" && keyName !== "shift" && keyName !== "alt" && keyName !== "meta") { keysPressed.push(keyName); } const pressedString = keysPressed.join("+"); const normalize = (val) => val.toLowerCase().replace(/\s+/g, ""); let matchedSceneId = null; Object.entries(sceneHotkeys).forEach(([sceneId, hotkey]) => { if (hotkey && normalize(hotkey) === normalize(pressedString)) { matchedSceneId = sceneId; } }); if (matchedSceneId) { e.preventDefault(); handleSetActiveScene(matchedSceneId); return; } if (!selectedSourceId) return; if (["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(keyName)) { e.preventDefault(); const activeScene2 = scenes.find((s) => s.id === activeSceneId); const selectedSource2 = activeScene2?.sources.find((s) => s.id === selectedSourceId); if (selectedSource2 && !selectedSource2.locked) { const step = e.shiftKey ? 25 : 5; const currentX = selectedSource2.style.x ?? 0; const currentY = selectedSource2.style.y ?? 0; let newX = currentX; let newY = currentY; if (keyName === "arrowleft") newX -= step; if (keyName === "arrowright") newX += step; if (keyName === "arrowup") newY -= step; if (keyName === "arrowdown") newY += step; updateSourceStyle(selectedSourceId, { x: newX, y: newY }); } return; } if (normalize(centerHotkey) === normalize(pressedString)) { e.preventDefault(); centerSource(selectedSourceId); } else if (normalize(fullscreenHotkey) === normalize(pressedString)) { e.preventDefault(); fullscreenSource(selectedSourceId); } else if (normalize(lockHotkey) === normalize(pressedString)) { e.preventDefault(); toggleSourceLock(selectedSourceId); } else if (normalize(visibilityHotkey) === normalize(pressedString)) { e.preventDefault(); toggleSourceVisibility(selectedSourceId); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [ isHotkeysEnabled, selectedSourceId, centerHotkey, fullscreenHotkey, lockHotkey, visibilityHotkey, scenes, activeSceneId, updateSourceStyle, sceneHotkeys, handleSetActiveScene, setScenesWithHistory ]); return /* @__PURE__ */ jsxs9("div", { className: `w-screen h-screen overflow-hidden text-white font-sans antialiased relative ${theme === "dark" ? "bg-gray-800" : "bg-gray-200"}`, children: [ /* @__PURE__ */ jsx9( Canvas_default, { scene: activeScene, isLocked: !isUIVisible, selectedSourceId, onSelectSource: (id) => { setSelectedSourceId(id); if (id) { logAction(`Selected source ${id}.`); } }, onUpdateSourceStyle: updateSourceStyle, onUpdateSource: updateSource, onInteractionEnd: handleInteractionEnd, isSnapToGridEnabled, canvasScale, canvasWidth, canvasHeight, onSourceContextMenu: handleSourceContextMenu, onCanvasContextMenu: handleCanvasContextMenu, isMobileMode, left: canvasLeft, top: canvasTop, interfaceAnimations, framerateLimit, isTransitioning, currentPlayingTransition } ), /* @__PURE__ */ jsx9( "div", { className: `fixed inset-0 z-[60000] pointer-events-none`, style: { transition: currentPlayingTransition?.type === "fade" ? `opacity ${currentPlayingTransition?.speed ?? 500}ms ${currentPlayingTransition?.style ?? "ease-in-out"}` : "none", backgroundColor: currentPlayingTransition?.color || "#000000", opacity: isTransitioning && currentPlayingTransition?.type === "fade" ? 1 : 0 } } ), currentPlayingTransition?.type === "stinger" && currentPlayingTransition.stingerFile && /* @__PURE__ */ jsx9( "div", { className: `fixed inset-0 z-[60000] pointer-events-none`, style: { transition: "opacity 200ms ease", opacity: isTransitioning ? 1 : 0 }, children: /* @__PURE__ */ jsx9("video", { ref: stingerVideoRef, src: currentPlayingTransition.stingerFile, className: "w-full h-full object-cover", muted: true }) } ), /* @__PURE__ */ jsx9( UI_default, { isVisible: isUIVisible, onToggleVisibility: () => { setUIVisible((v) => !v); setSelectedSourceId(null); }, isWelcomeVisible, onCloseWelcome: () => setWelcomeVisible(false), scenes, activeScene, onSetActiveScene: handleSetActiveScene, isAddingScene, onAddScene: () => setIsAddingScene(true), onConfirmAddScene: handleConfirmAddScene, onCancelAddScene: () => setIsAddingScene(false), onDeleteScene: deleteScene, onUpdateScene: updateScene, onAddSource: addSource, onDeleteSource: deleteSource, onUpdateSourceLayer: updateSourceLayer, onImportSources: importSources, onExportSources: exportSources, onImportProject: importProject, onExportProject: exportProject, onCenterSource: centerSource, onFullscreenSource: fullscreenSource, onUndo: undo, onRedo: redo, canUndo, canRedo, selectedSource, onUpdateSource: updateSource, onSelectSource: setSelectedSourceId, isSnapToGridEnabled, onToggleSnapToGrid: () => setSnapToGridEnabled((v) => !v), theme, onSetTheme: setTheme, filteringSource, onOpenFilterPanel: (id) => setFilteringSourceId(id), onCloseFilterPanel: () => setFilteringSourceId(null), onUpdateSourceStyle: updateSourceStyleWithHistory, onToggleSourceVisibility: toggleSourceVisibility, onToggleSourceLock: toggleSourceLock, onReorderSources: reorderSources, sceneTransition, onSetSceneTransition: setSceneTransition, actionLog, onClearActionLog: clearActionLog, activeTab, setActiveTab, editingScene, onOpenEditSceneModal: handleOpenEditSceneModal, onDuplicateScene: handleDuplicateScene, onCloseEditSceneModal: handleCloseEditSceneModal, onConfirmEditScene: handleConfirmEditScene, sourceToDelete, onConfirmDelete: () => { if (sourceToDelete) deleteSource(sourceToDelete); setSourceToDelete(null); }, onCancelDelete: () => setSourceToDelete(null), isHotkeysEnabled, onToggleHotkeys: () => setHotkeysEnabled((val) => !val), centerHotkey, onSetCenterHotkey: setCenterHotkey, fullscreenHotkey, onSetFullscreenHotkey: setFullscreenHotkey, lockHotkey, onSetLockHotkey: setLockHotkey, visibilityHotkey, onSetVisibilityHotkey: setVisibilityHotkey, canvasResolution, onSetCanvasResolution: setCanvasResolution, isAutoHideEnabled, onToggleAutoHide: () => setAutoHideEnabled((v) => !v), onSourceContextMenu: handleSourceContextMenu, isMobileMode, onToggleMobileMode: () => { const newVal = !isMobileMode; setMobileMode(newVal); localStorage.setItem("isMobileMode", String(newVal)); logAction(`Touch Mobile Mode toggled ${newVal ? "ON" : "OFF"}.`); }, isPoppedOut, onTogglePoppedOut: () => setIsPoppedOut(!isPoppedOut), panelWidth, onPanelWidthChange: setPanelWidth, isHoveredOverLeft, onHoveredOverLeftChange: setIsHoveredOverLeft, scrollbarColor, onSetScrollbarColor: setScrollbarColor, performancePreset, onSetPerformancePreset: setPerformancePreset, framerateLimit, onSetFramerateLimit: setFramerateLimit, renderScale, onSetRenderScale: setRenderScale, interfaceAnimations, onSetInterfaceAnimations: setInterfaceAnimations, onExportCanvasSettings: exportCanvasSettings, onImportCanvasSettings: importCanvasSettings, onExportMasterBackup: exportMasterBackup, onImportMasterBackup: importMasterBackup, copiedSource, onCopySource: handleCopySource, onPasteSource: handlePasteSource, contextMenuTheme, onSetContextMenuTheme: setContextMenuTheme, sceneHotkeys, onSetSceneHotkey: handleSetSceneHotkey } ), contextMenu.visible && (() => { const contextMenuSource = activeScene?.sources.find((s) => s.id === contextMenu.sourceId); const effectiveTheme = contextMenuTheme === "match" ? theme : contextMenuTheme; const cmThemes = themes[effectiveTheme] || themes.dark; const hoverClass = cmThemes.itemHover || "hover:bg-indigo-650 hover:text-white"; const fallbackText = effectiveTheme === "light" ? "text-zinc-800" : "text-zinc-100"; return /* @__PURE__ */ jsxs9( "div", { style: { top: `${contextMenu.y}px`, left: `${contextMenu.x}px` }, className: `fixed z-[99999] w-64 rounded-lg shadow-2xl border p-1.5 flex flex-col font-sans select-none pointer-events-auto ${cmThemes.bg || "bg-[#18181b]"} ${cmThemes.border || "border-zinc-700/80"} ${cmThemes.text || "text-zinc-100"}`, onClick: (e) => e.stopPropagation(), onContextMenu: (e) => e.preventDefault(), children: [ contextMenuSource ? /* @__PURE__ */ jsxs9(Fragment6, { children: [ /* @__PURE__ */ jsx9("div", { className: `px-2 py-1 text-[11px] font-bold text-gray-400 border-b ${cmThemes.border || "border-zinc-700/50"} mb-1 flex items-center gap-1.5 uppercase tracking-wider`, children: /* @__PURE__ */ jsxs9("span", { children: [ "\u2699\uFE0F ", contextMenuSource.name ] }) }), /* @__PURE__ */ jsx9( "button", { onClick: () => { centerSource(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: "\u{1F3AF} Center Source" } ), /* @__PURE__ */ jsx9( "button", { onClick: () => { fullscreenSource(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: "\u{1F4FA} Fit to Canvas" } ), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1 opacity-50` }), /* @__PURE__ */ jsx9("div", { className: "px-2 py-0.5 text-[10px] text-gray-400 uppercase tracking-widest font-semibold font-mono", children: "Position" }), /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-2 py-1", children: [ /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-gray-500 font-mono w-4", children: "X:" }), /* @__PURE__ */ jsx9( "input", { type: "number", value: Math.round(contextMenuSource.style.x), onChange: (e) => updateSourceStyle(contextMenuSource.id, { x: parseInt(e.target.value) || 0 }), className: `w-14 text-center text-xs p-0.5 rounded border focus:outline-none focus:ring-1 focus:ring-indigo-500 ${cmThemes.inputBg || "bg-zinc-800"} ${cmThemes.border || "border-zinc-700"} ${cmThemes.text || "text-white"}` } ), /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-gray-500 font-mono w-4", children: "Y:" }), /* @__PURE__ */ jsx9( "input", { type: "number", value: Math.round(contextMenuSource.style.y), onChange: (e) => updateSourceStyle(contextMenuSource.id, { y: parseInt(e.target.value) || 0 }), className: `w-14 text-center text-xs p-0.5 rounded border focus:outline-none focus:ring-1 focus:ring-indigo-500 ${cmThemes.inputBg || "bg-zinc-800"} ${cmThemes.border || "border-zinc-700"} ${cmThemes.text || "text-white"}` } ) ] }), /* @__PURE__ */ jsx9("div", { className: "px-2 py-0.5 text-[10px] text-gray-400 uppercase tracking-widest font-semibold font-mono", children: "Dimensions" }), /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-2 py-1", children: [ /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-gray-500 font-mono w-4", children: "W:" }), /* @__PURE__ */ jsx9( "input", { type: "number", value: Math.round(contextMenuSource.style.width), onChange: (e) => updateSourceStyle(contextMenuSource.id, { width: Math.max(10, parseInt(e.target.value) || 10) }), className: `w-14 text-center text-xs p-0.5 rounded border focus:outline-none focus:ring-1 focus:ring-indigo-500 ${cmThemes.inputBg || "bg-zinc-800"} ${cmThemes.border || "border-zinc-700"} ${cmThemes.text || "text-white"}` } ), /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-gray-500 font-mono w-4", children: "H:" }), /* @__PURE__ */ jsx9( "input", { type: "number", value: Math.round(contextMenuSource.style.height), onChange: (e) => updateSourceStyle(contextMenuSource.id, { height: Math.max(10, parseInt(e.target.value) || 10) }), className: `w-14 text-center text-xs p-0.5 rounded border focus:outline-none focus:ring-1 focus:ring-indigo-500 ${cmThemes.inputBg || "bg-zinc-800"} ${cmThemes.border || "border-zinc-700"} ${cmThemes.text || "text-white"}` } ) ] }), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1 opacity-50` }), /* @__PURE__ */ jsx9( "button", { onClick: () => { toggleSourceLock(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: contextMenuSource.locked ? "\u{1F513} Unlock Source" : "\u{1F512} Lock Source" } ), /* @__PURE__ */ jsx9( "button", { onClick: () => { toggleSourceVisibility(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: contextMenuSource.visible ? "\u{1F441}\uFE0F Hide Source" : "\u{1F441}\uFE0F Show Source" } ), /* @__PURE__ */ jsxs9( "button", { onClick: () => { const currentScaleX = contextMenuSource.style.scaleX === -1 ? 1 : -1; updateSourceStyle(contextMenuSource.id, { scaleX: currentScaleX }); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 rounded transition-colors flex items-center justify-between font-medium cursor-pointer ${contextMenuSource.style.scaleX === -1 ? "bg-indigo-650/30 text-indigo-400 font-semibold" : hoverClass}`, children: [ /* @__PURE__ */ jsx9("span", { className: "flex items-center gap-1.5", children: "\u2194\uFE0F Flip Horizontally" }), contextMenuSource.style.scaleX === -1 && /* @__PURE__ */ jsx9("span", { className: "text-[9px] bg-indigo-500/20 px-1 py-0.5 rounded font-bold", children: "Flipped" }) ] } ), /* @__PURE__ */ jsxs9( "button", { onClick: () => { const currentScaleY = contextMenuSource.style.scaleY === -1 ? 1 : -1; updateSourceStyle(contextMenuSource.id, { scaleY: currentScaleY }); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 rounded transition-colors flex items-center justify-between font-medium cursor-pointer ${contextMenuSource.style.scaleY === -1 ? "bg-indigo-650/30 text-indigo-400 font-semibold" : hoverClass}`, children: [ /* @__PURE__ */ jsx9("span", { className: "flex items-center gap-1.5", children: "\u2195\uFE0F Flip Vertically" }), contextMenuSource.style.scaleY === -1 && /* @__PURE__ */ jsx9("span", { className: "text-[9px] bg-indigo-500/20 px-1 py-0.5 rounded font-bold", children: "Flipped" }) ] } ), /* @__PURE__ */ jsx9( "button", { onClick: () => { setFilteringSourceId(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: "\u{1F3A8} Edit Filters" } ), /* @__PURE__ */ jsx9( "button", { onClick: () => { setSelectedSourceId(contextMenuSource.id); setActiveTab("settings"); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: "\u2699\uFE0F Edit Source" } ), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1 opacity-50` }), /* @__PURE__ */ jsx9( "button", { onClick: () => { setSourceToDelete(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 text-red-500 ${hoverClass === "hover:bg-gray-150" ? "hover:bg-red-50 hover:text-red-600" : "hover:bg-red-600 hover:text-white"} rounded transition-colors flex items-center gap-1.5 font-semibold cursor-pointer`, children: "\u{1F5D1}\uFE0F Delete Source" } ), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1 opacity-50` }), /* @__PURE__ */ jsx9( "button", { onClick: () => { handleCopySource(contextMenuSource.id); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: "\u{1F4CB} Copy Source" } ), copiedSource && /* @__PURE__ */ jsx9( "button", { onClick: () => { handlePasteSource(); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, title: `Paste ${copiedSource.name} inside active scene`, children: "\u{1F4CB} Paste Source" } ), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1.5 opacity-50` }) ] }) : /* @__PURE__ */ jsxs9(Fragment6, { children: [ /* @__PURE__ */ jsx9("div", { className: `px-2 py-1 text-[11px] font-bold text-indigo-400 border-b ${cmThemes.border || "border-zinc-700/50"} mb-1 flex items-center gap-1.5 uppercase tracking-wider`, children: /* @__PURE__ */ jsx9("span", { children: "\u{1F4FA} Stage Workspace" }) }), /* @__PURE__ */ jsx9( "button", { onClick: () => { setUIVisible((prev) => !prev); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: isUIVisible ? "\u{1F512} Hide Custom Panels" : "\u{1F513} Show Custom Panels" } ), /* @__PURE__ */ jsx9( "button", { onClick: () => { setSnapToGridEnabled((prev) => !prev); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 ${hoverClass} rounded transition-colors flex items-center gap-1.5 font-medium cursor-pointer`, children: isSnapToGridEnabled ? "\u{1F4F4} Disable Grid Snapping" : "\u{1F51B} Enable Grid Snapping" } ), /* @__PURE__ */ jsxs9("div", { className: "px-2 py-1.5 mt-1.5 border-t border-zinc-750/30", children: [ /* @__PURE__ */ jsxs9("div", { className: "text-[9.5px] text-zinc-400 font-bold uppercase mb-1 flex items-center justify-between", children: [ /* @__PURE__ */ jsx9("span", { children: "\u{1F3A8} Stage Color" }), /* @__PURE__ */ jsx9("span", { className: "text-[9px] text-indigo-400 font-mono", children: scenes.find((s) => s.id === activeSceneId)?.backgroundColor || "#000000" }) ] }), /* @__PURE__ */ jsx9("div", { className: "grid grid-cols-4 gap-1.5 mb-1.5 pt-0.5", children: [ { hex: "#000000", title: "Black" }, { hex: "#1e1e24", title: "Charcoal" }, { hex: "#0a0e17", title: "Deep Space" }, { hex: "#141a17", title: "Forest" }, { hex: "#240a15", title: "Wine Maroon" }, { hex: "#111322", title: "Midnight Indigo" }, { hex: "#7f8c8d", title: "Silver Gray" }, { hex: "#ffffff", title: "White" } ].map((col) => /* @__PURE__ */ jsx9( "button", { onClick: () => { updateScene(activeSceneId, { backgroundColor: col.hex }); }, title: col.title, className: `w-5.5 h-5.5 rounded-full border border-zinc-700 bg-transparent hover:scale-110 active:scale-95 transition cursor-pointer`, style: { backgroundColor: col.hex } }, col.hex )) }), /* @__PURE__ */ jsx9("div", { className: "flex items-center gap-1", children: /* @__PURE__ */ jsx9( "input", { type: "color", value: scenes.find((s) => s.id === activeSceneId)?.backgroundColor || "#000000", onChange: (e) => { updateScene(activeSceneId, { backgroundColor: e.target.value }); }, className: "w-full h-6 rounded cursor-pointer border border-zinc-700 bg-zinc-900 p-0", title: "Custom color input" } ) }) ] }), copiedSource && /* @__PURE__ */ jsxs9(Fragment6, { children: [ /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1 opacity-50` }), /* @__PURE__ */ jsxs9( "button", { onClick: () => { handlePasteSource(); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-xs px-2 py-1 text-emerald-500 hover:text-white hover:bg-emerald-600 rounded transition-colors flex items-center gap-1.5 font-semibold cursor-pointer`, title: `Paste copied source: ${copiedSource.name}`, children: [ "\u{1F4CB} Paste Source (", copiedSource.name, ")" ] } ) ] }), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1.5 opacity-50` }) ] }), /* @__PURE__ */ jsx9("hr", { className: `${cmThemes.border || "border-zinc-700/50"} my-1 opacity-50` }), /* @__PURE__ */ jsxs9("div", { className: "relative group/scale", children: [ /* @__PURE__ */ jsxs9( "button", { className: `w-full text-left text-xs px-2 py-1.5 ${hoverClass} rounded transition-colors flex items-center justify-between font-medium cursor-pointer`, children: [ /* @__PURE__ */ jsx9("span", { className: "flex items-center gap-1.5", children: "\u{1F50D} Preview Scale" }), /* @__PURE__ */ jsxs9("span", { className: "text-[10px] text-gray-400 font-mono flex items-center gap-1", children: [ previewScalingMode === "fit" ? "Fit" : `${previewScalingMode}%`, /* @__PURE__ */ jsx9("span", { children: "\u25B6" }) ] }) ] } ), /* @__PURE__ */ jsx9( "div", { className: `absolute left-full top-[-6px] ml-1 hidden group-hover/scale:flex flex-col w-48 rounded-lg shadow-2xl border p-1.5 ${cmThemes.bg || "bg-[#18181b]"} ${cmThemes.border || "border-zinc-700/80"} ${cmThemes.text || "text-zinc-100"}`, children: [ { value: "fit", label: "\u{1F50D} Scale to Fit (Window)" }, { value: "100", label: "\u{1F50D} 100% Scale" }, { value: "75", label: "\u{1F50D} 75% Scale" }, { value: "50", label: "\u{1F50D} 50% Scale" }, { value: "25", label: "\u{1F50D} 25% Scale" } ].map((opt) => /* @__PURE__ */ jsxs9( "button", { onClick: () => { setPreviewScalingMode(opt.value); setContextMenu((c) => ({ ...c, visible: false })); }, className: `w-full text-left text-[11px] px-2 py-1.5 ${hoverClass} rounded transition-colors flex items-center justify-between font-medium cursor-pointer`, children: [ /* @__PURE__ */ jsx9("span", { className: "flex-1", children: opt.label }), previewScalingMode === opt.value && /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-indigo-400 font-bold font-mono", children: "\u2713" }) ] }, opt.value )) } ) ] }) ] } ); })(), isWelcomeVisible && /* @__PURE__ */ jsx9( WelcomeOverlay, { onClose: () => setWelcomeVisible(false), theme } ), confirmModal && confirmModal.isOpen && /* @__PURE__ */ jsxs9(Fragment6, { children: [ /* @__PURE__ */ jsx9( "div", { className: "fixed inset-0 bg-black/80 z-[190000] pointer-events-auto cursor-pointer", onClick: () => setConfirmModal(null) } ), /* @__PURE__ */ jsxs9( "div", { style: { position: "fixed", left: "50%", top: "50%", transform: "translate(-50%, -50%)", zIndex: 2e5 }, className: `rounded-lg shadow-2xl max-w-sm w-full p-6 border pointer-events-auto text-left flex flex-col ${themes[theme]?.bg || "bg-gray-800"} ${themes[theme]?.border || "border-gray-700"} ${themes[theme]?.text || "text-white"}`, children: [ /* @__PURE__ */ jsx9("div", { className: "border-b border-gray-700/30 pb-2 mb-4 flex items-center justify-between", children: /* @__PURE__ */ jsxs9("h2", { className: "text-sm font-bold uppercase tracking-wider text-indigo-400", children: [ "\u26A0\uFE0F ", confirmModal.title ] }) }), /* @__PURE__ */ jsx9("p", { className: "text-xs md:text-sm leading-relaxed mb-6 font-medium", children: confirmModal.message }), /* @__PURE__ */ jsxs9("div", { className: "flex justify-end space-x-3", children: [ /* @__PURE__ */ jsx9( "button", { onClick: () => setConfirmModal(null), className: `px-4 py-2 text-xs rounded font-bold cursor-pointer transition-all ${themes[theme]?.buttonSecondary || "bg-gray-600 hover:bg-gray-500 text-white"}`, children: "No, Cancel" } ), /* @__PURE__ */ jsx9( "button", { onClick: () => { confirmModal.onConfirm(); }, className: "px-4 py-2 text-xs rounded font-bold bg-indigo-600 hover:bg-indigo-500 text-white cursor-pointer transition-all active:scale-95", children: "Yes, Proceed" } ) ] }) ] } ) ] }) ] }); }; var App_default = App; // ws:scratch/cc/index.tsx import { jsx as jsx10 } from "https://esm.sh/react@19.2.0/jsx-runtime"; var rootElement = document.getElementById("root"); if (!rootElement) { throw new Error("Could not find root element to mount to"); } var root = ReactDOM.createRoot(rootElement); root.render( /* @__PURE__ */ jsx10(React10.StrictMode, { children: /* @__PURE__ */ jsx10(App_default, {}) }) );