UNPKG

@toriistudio/v0-playground

Version:
1,324 lines (1,300 loc) 92.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Button: () => Button, ControlsProvider: () => ControlsProvider, DEFAULT_ADVANCED_PALETTE: () => DEFAULT_ADVANCED_PALETTE, DEFAULT_HEX_PALETTE: () => DEFAULT_HEX_PALETTE, Playground: () => Playground, advancedPaletteToHexColors: () => advancedPaletteToHexColors, clonePalette: () => clonePalette, computePaletteGradient: () => computePaletteGradient, createAdvancedPalette: () => createAdvancedPalette, createPaletteSignature: () => createPaletteSignature, hexToPaletteValue: () => hexToPaletteValue, paletteValueToHex: () => paletteValueToHex, useAdvancedPaletteControls: () => useAdvancedPaletteControls, useControls: () => useControls, useDefaultAdvancedPaletteControls: () => useDefaultAdvancedPaletteControls, useUrlSyncedControls: () => useUrlSyncedControls }); module.exports = __toCommonJS(src_exports); // src/components/Playground.tsx var import_react8 = require("react"); var import_lucide_react5 = require("lucide-react"); // src/context/ResizableLayout.tsx var import_react = require("react"); var import_lucide_react = require("lucide-react"); var import_jsx_runtime = require("react/jsx-runtime"); var ResizableLayoutContext = (0, import_react.createContext)( null ); var useResizableLayout = () => { const ctx = (0, import_react.useContext)(ResizableLayoutContext); if (!ctx) throw new Error("ResizableLayoutContext not found"); return ctx; }; var ResizableLayout = ({ children, hideControls }) => { const [leftPanelWidth, setLeftPanelWidth] = (0, import_react.useState)(25); const [isDesktop, setIsDesktop] = (0, import_react.useState)(false); const [isHydrated, setIsHydrated] = (0, import_react.useState)(false); const [isDragging, setIsDragging] = (0, import_react.useState)(false); const [sidebarNarrow, setSidebarNarrow] = (0, import_react.useState)(false); const containerRef = (0, import_react.useRef)(null); (0, import_react.useEffect)(() => { setIsHydrated(true); const handleResize = () => setIsDesktop(window.innerWidth >= 768); handleResize(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); (0, import_react.useEffect)(() => { if (!isHydrated || !isDesktop) return; const checkSidebarWidth = () => { if (containerRef.current) { const containerWidth = containerRef.current.clientWidth; const sidebarWidth = leftPanelWidth / 100 * containerWidth; setSidebarNarrow(sidebarWidth < 350); } }; checkSidebarWidth(); window.addEventListener("resize", checkSidebarWidth); return () => window.removeEventListener("resize", checkSidebarWidth); }, [leftPanelWidth, isHydrated, isDesktop]); (0, import_react.useEffect)(() => { const handleMouseMove = (e) => { if (isDragging && containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); const newLeftWidth = (e.clientX - containerRect.left) / containerRect.width * 100; if (newLeftWidth >= 20 && newLeftWidth <= 80) { setLeftPanelWidth(newLeftWidth); } } }; const handleMouseUp = () => setIsDragging(false); if (isDragging) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging]); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ResizableLayoutContext.Provider, { value: { leftPanelWidth, isHydrated, isDesktop, sidebarNarrow, containerRef }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "min-h-screen w-full bg-black text-white", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "div", { ref: containerRef, className: "flex flex-col md:flex-row min-h-screen w-full overflow-hidden select-none", children: [ children, isHydrated && isDesktop && !hideControls && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: "order-3 w-2 bg-stone-800 hover:bg-stone-700 cursor-col-resize items-center justify-center z-10 transition-opacity duration-300", onMouseDown: () => setIsDragging(true), style: { position: "absolute", left: `${leftPanelWidth}%`, top: 0, bottom: 0, display: "flex" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.GripVertical, { className: "h-6 w-6 text-stone-500" }) } ) ] } ) }) } ); }; // src/context/ControlsContext.tsx var import_react2 = require("react"); // src/utils/getUrlParams.ts var getUrlParams = () => { if (typeof window === "undefined") return {}; const params = new URLSearchParams(window.location.search); const entries = {}; for (const [key, value] of params.entries()) { entries[key] = value; } return entries; }; // src/constants/urlParams.ts var NO_CONTROLS_PARAM = "nocontrols"; var PRESENTATION_PARAM = "presentation"; var CONTROLS_ONLY_PARAM = "controlsonly"; // src/utils/getControlsChannelName.ts var EXCLUDED_KEYS = /* @__PURE__ */ new Set([ NO_CONTROLS_PARAM, PRESENTATION_PARAM, CONTROLS_ONLY_PARAM ]); var getControlsChannelName = () => { if (typeof window === "undefined") return null; const params = new URLSearchParams(window.location.search); for (const key of EXCLUDED_KEYS) { params.delete(key); } const query = params.toString(); const base = window.location.pathname || "/"; return `v0-controls:${base}${query ? `?${query}` : ""}`; }; // src/lib/advancedPalette.ts var CHANNEL_KEYS = ["r", "g", "b"]; var DEFAULT_CHANNEL_LABELS = { r: "Red", g: "Green", b: "Blue" }; var DEFAULT_SECTIONS = [ { key: "A", label: "Vector A", helper: "Base offset" }, { key: "B", label: "Vector B", helper: "Amplitude" }, { key: "C", label: "Vector C", helper: "Frequency" }, { key: "D", label: "Vector D", helper: "Phase shift" } ]; var DEFAULT_RANGES = { A: { min: 0, max: 1, step: 0.01 }, B: { min: -1, max: 1, step: 0.01 }, C: { min: 0, max: 2, step: 0.01 }, D: { min: 0, max: 1, step: 0.01 } }; var DEFAULT_HIDDEN_KEY_PREFIX = "palette"; var DEFAULT_GRADIENT_STEPS = 12; var DEFAULT_HEX_PALETTE = { A: { r: 0.5, g: 0.5, b: 0.5 }, B: { r: 0.5, g: 0.5, b: 0.5 }, C: { r: 1, g: 1, b: 1 }, D: { r: 0, g: 0.1, b: 0.2 } }; var createPaletteControlKey = (prefix, section, channel) => `${prefix}${section}${channel}`; var clamp = (value, min, max) => Math.min(Math.max(value, min), max); var clamp01 = (value) => clamp(value, 0, 1); var toNumberOr = (value, fallback) => { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string") { const parsed = parseFloat(value); if (Number.isFinite(parsed)) return parsed; } return fallback; }; var paletteColorAt = (palette, t) => { const twoPi = Math.PI * 2; const computeChannel = (a, b, c, d) => { const value = a + b * Math.cos(twoPi * (c * t + d)); return clamp(value, 0, 1); }; return { r: computeChannel( palette.A?.r ?? 0, palette.B?.r ?? 0, palette.C?.r ?? 0, palette.D?.r ?? 0 ), g: computeChannel( palette.A?.g ?? 0, palette.B?.g ?? 0, palette.C?.g ?? 0, palette.D?.g ?? 0 ), b: computeChannel( palette.A?.b ?? 0, palette.B?.b ?? 0, palette.C?.b ?? 0, palette.D?.b ?? 0 ) }; }; var toRgba = ({ r, g, b }, alpha = 0.5) => `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round( b * 255 )}, ${alpha})`; var computePaletteGradient = (palette, steps = DEFAULT_GRADIENT_STEPS) => { const stops = Array.from({ length: steps }, (_, index) => { const t = index / (steps - 1); const color = paletteColorAt(palette, t); const stop = (t * 100).toFixed(1); return `${toRgba(color)} ${stop}%`; }); return `linear-gradient(to right, ${stops.join(", ")})`; }; var createPaletteSignature = (palette) => Object.entries(palette).sort(([aKey], [bKey]) => aKey.localeCompare(bKey)).flatMap( ([, channels]) => CHANNEL_KEYS.map((channel) => (channels?.[channel] ?? 0).toFixed(3)) ).join("-"); var isAdvancedPaletteValue = (value) => Boolean( value && typeof value === "object" && CHANNEL_KEYS.every((channel) => { const channelValue = value[channel]; return typeof channelValue === "number" && Number.isFinite(channelValue); }) ); var isAdvancedPalette = (value) => Boolean( value && typeof value === "object" && Object.values(value).every( (entry) => isAdvancedPaletteValue(entry) || typeof entry === "object" ) ); var normalizePaletteValue = (source) => { if (typeof source === "string") { return hexToPaletteValue(source); } const channelSource = source ?? {}; const toChannel = (channel) => clamp01( toNumberOr( channelSource[channel], 0 ) ); return { r: toChannel("r"), g: toChannel("g"), b: toChannel("b") }; }; var createPaletteFromRecord = (record) => Object.entries(record).reduce((acc, [key, value]) => { acc[key] = normalizePaletteValue(value); return acc; }, {}); var clonePalette = (palette) => Object.fromEntries( Object.entries(palette).map(([sectionKey, channels]) => [ sectionKey, { ...channels } ]) ); var hexComponentToNormalized = (component) => clamp01(parseInt(component, 16) / 255 || 0); var normalizedChannelToHex = (value) => Math.round(clamp01(value) * 255).toString(16).padStart(2, "0"); var sanitizeHex = (hex) => { let sanitized = hex.trim(); if (sanitized.startsWith("#")) { sanitized = sanitized.slice(1); } if (sanitized.length === 3) { sanitized = sanitized.split("").map((char) => char + char).join(""); } return sanitized.length === 6 ? sanitized : null; }; var hexToPaletteValue = (hex) => { const sanitized = sanitizeHex(hex); if (!sanitized) { return { r: 0, g: 0, b: 0 }; } return { r: hexComponentToNormalized(sanitized.slice(0, 2)), g: hexComponentToNormalized(sanitized.slice(2, 4)), b: hexComponentToNormalized(sanitized.slice(4, 6)) }; }; var paletteValueToHex = (value) => `#${normalizedChannelToHex(value.r)}${normalizedChannelToHex( value.g )}${normalizedChannelToHex(value.b)}`; var createAdvancedPalette = (source = DEFAULT_HEX_PALETTE, options) => { if (Array.isArray(source)) { const order = options?.sectionOrder ?? DEFAULT_SECTIONS.map((section) => section.key); const record = {}; source.forEach((value, index) => { const preferredKey = order[index]; const fallbackKey = `Color${index + 1}`; const key = preferredKey && !(preferredKey in record) ? preferredKey : fallbackKey; record[key] = value; }); return createPaletteFromRecord(record); } if (isAdvancedPalette(source)) { return clonePalette( Object.entries(source ?? {}).reduce( (acc, [key, value]) => { acc[key] = normalizePaletteValue(value); return acc; }, {} ) ); } if (source && typeof source === "object") { return createPaletteFromRecord(source); } return createPaletteFromRecord(DEFAULT_HEX_PALETTE); }; var DEFAULT_ADVANCED_PALETTE = createAdvancedPalette( DEFAULT_HEX_PALETTE ); var advancedPaletteToHexColors = (palette, options) => { const fallbackPalette = options?.fallbackPalette ?? DEFAULT_ADVANCED_PALETTE; const orderedKeys = options?.sectionOrder ?? (Object.keys(palette).length > 0 ? Object.keys(palette) : Object.keys(fallbackPalette)); const uniqueKeys = Array.from(new Set(orderedKeys)); if (uniqueKeys.length === 0) { uniqueKeys.push(...Object.keys(DEFAULT_ADVANCED_PALETTE)); } const defaultColor = options?.defaultColor ?? "#000000"; return uniqueKeys.map((key) => { const paletteValue = palette[key] ?? fallbackPalette[key]; if (!paletteValue) return defaultColor; return paletteValueToHex(paletteValue); }); }; var createDefaultSectionsFromPalette = (palette) => { const sectionKeys = Object.keys(palette); if (sectionKeys.length === 0) return DEFAULT_SECTIONS; return sectionKeys.map((key, index) => ({ key, label: `Vector ${key}`, helper: DEFAULT_SECTIONS[index]?.helper ?? "Palette parameter" })); }; var resolveAdvancedPaletteConfig = (config) => { const defaultPalette = createAdvancedPalette(config.defaultPalette); const sections = config.sections ?? createDefaultSectionsFromPalette(defaultPalette); const ranges = {}; sections.forEach((section) => { ranges[section.key] = config.ranges?.[section.key] ?? DEFAULT_RANGES[section.key] ?? { min: 0, max: 1, step: 0.01 }; }); const channelLabels = { ...DEFAULT_CHANNEL_LABELS, ...config.channelLabels ?? {} }; return { ...config, defaultPalette, sections, ranges, channelLabels, hiddenKeyPrefix: config.hiddenKeyPrefix ?? DEFAULT_HIDDEN_KEY_PREFIX, controlKey: config.controlKey ?? "advancedPaletteControl", gradientSteps: config.gradientSteps ?? DEFAULT_GRADIENT_STEPS }; }; var createAdvancedPaletteSchemaEntries = (schema, resolvedConfig) => { const { sections, hiddenKeyPrefix, defaultPalette } = resolvedConfig; const updatedSchema = { ...schema }; sections.forEach((section) => { CHANNEL_KEYS.forEach((channel) => { const key = createPaletteControlKey( hiddenKeyPrefix, section.key, channel ); if (!(key in updatedSchema)) { updatedSchema[key] = { type: "number", value: defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0, hidden: true }; } }); }); return updatedSchema; }; // src/context/ControlsContext.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var ControlsContext = (0, import_react2.createContext)(null); var useControlsContext = () => { const ctx = (0, import_react2.useContext)(ControlsContext); if (!ctx) throw new Error("useControls must be used within ControlsProvider"); return ctx; }; var ControlsProvider = ({ children }) => { const [schema, setSchema] = (0, import_react2.useState)({}); const [values, setValues] = (0, import_react2.useState)({}); const [config, setConfig] = (0, import_react2.useState)({ showCopyButton: true, showCodeSnippet: false }); const [componentName, setComponentName] = (0, import_react2.useState)(); const [channelName, setChannelName] = (0, import_react2.useState)(null); const channelRef = (0, import_react2.useRef)(null); const instanceIdRef = (0, import_react2.useRef)(null); const skipBroadcastRef = (0, import_react2.useRef)(false); const latestValuesRef = (0, import_react2.useRef)(values); (0, import_react2.useEffect)(() => { latestValuesRef.current = values; }, [values]); (0, import_react2.useEffect)(() => { if (typeof window === "undefined") return; setChannelName(getControlsChannelName()); }, []); const setValue = (key, value) => { setValues((prev) => ({ ...prev, [key]: value })); }; const registerSchema = (newSchema, opts) => { if (opts?.componentName) { setComponentName(opts.componentName); } if (opts?.config) { const { addAdvancedPaletteControl, addMediaUploadControl, ...otherConfig } = opts.config; setConfig((prev) => { const nextConfig = { ...prev, ...otherConfig }; if (Object.prototype.hasOwnProperty.call( opts.config, "addAdvancedPaletteControl" )) { nextConfig.addAdvancedPaletteControl = addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(addAdvancedPaletteControl) : void 0; } if (Object.prototype.hasOwnProperty.call( opts.config, "addMediaUploadControl" )) { nextConfig.addMediaUploadControl = addMediaUploadControl ? { ...addMediaUploadControl } : void 0; } return nextConfig; }); } setSchema((prevSchema) => ({ ...prevSchema, ...newSchema })); setValues((prevValues) => { const updated = { ...prevValues }; for (const key in newSchema) { const control = newSchema[key]; if (!(key in updated)) { if ("value" in control) { updated[key] = control.value; } } } return updated; }); }; (0, import_react2.useEffect)(() => { if (!channelName) return; if (typeof window === "undefined") return; if (typeof window.BroadcastChannel === "undefined") return; const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2); instanceIdRef.current = instanceId; const channel = new BroadcastChannel(channelName); channelRef.current = channel; const sendValues = () => { if (!instanceIdRef.current) return; channel.postMessage({ type: "controls-sync-values", source: instanceIdRef.current, values: latestValuesRef.current }); }; const handleMessage = (event) => { const data = event.data; if (!data || data.source === instanceIdRef.current) return; if (data.type === "controls-sync-request") { sendValues(); return; } if (data.type === "controls-sync-values" && data.values) { const incoming = data.values; setValues((prev) => { const prevKeys = Object.keys(prev); const incomingKeys = Object.keys(incoming); const sameLength = prevKeys.length === incomingKeys.length; const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]); if (sameValues) return prev; skipBroadcastRef.current = true; return { ...incoming }; }); } }; channel.addEventListener("message", handleMessage); channel.postMessage({ type: "controls-sync-request", source: instanceId }); return () => { channel.removeEventListener("message", handleMessage); channel.close(); channelRef.current = null; instanceIdRef.current = null; }; }, [channelName]); (0, import_react2.useEffect)(() => { if (!channelRef.current || !instanceIdRef.current) return; if (skipBroadcastRef.current) { skipBroadcastRef.current = false; return; } channelRef.current.postMessage({ type: "controls-sync-values", source: instanceIdRef.current, values }); }, [values]); const contextValue = (0, import_react2.useMemo)( () => ({ schema, values, setValue, registerSchema, componentName, config }), [schema, values, componentName, config] ); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ControlsContext.Provider, { value: contextValue, children }); }; var useControls = (schema, options) => { const ctx = (0, import_react2.useContext)(ControlsContext); if (!ctx) throw new Error("useControls must be used within ControlsProvider"); const lastAdvancedPaletteSignature = (0, import_react2.useRef)(null); const urlParams = getUrlParams(); const resolvedAdvancedConfig = options?.config?.addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(options.config.addAdvancedPaletteControl) : void 0; const schemaWithAdvanced = (0, import_react2.useMemo)(() => { const baseSchema = { ...schema }; if (!resolvedAdvancedConfig) return baseSchema; return createAdvancedPaletteSchemaEntries( baseSchema, resolvedAdvancedConfig ); }, [schema, resolvedAdvancedConfig]); const urlParamsKey = (0, import_react2.useMemo)(() => JSON.stringify(urlParams), [urlParams]); const mergedSchema = (0, import_react2.useMemo)(() => { return Object.fromEntries( Object.entries(schemaWithAdvanced).map(([key, control]) => { const urlValue = urlParams[key]; if (!urlValue || !("value" in control)) return [key, control]; const defaultValue = control.value; let parsed = urlValue; if (typeof defaultValue === "number") { parsed = parseFloat(urlValue); if (isNaN(parsed)) parsed = defaultValue; } else if (typeof defaultValue === "boolean") { parsed = urlValue === "true"; } return [ key, { ...control, value: parsed } ]; }) ); }, [schemaWithAdvanced, urlParams, urlParamsKey]); (0, import_react2.useEffect)(() => { ctx.registerSchema(mergedSchema, options); }, [JSON.stringify(mergedSchema), JSON.stringify(options)]); (0, import_react2.useEffect)(() => { for (const key in mergedSchema) { if (!(key in ctx.values) && "value" in mergedSchema[key]) { ctx.setValue(key, mergedSchema[key].value); } } }, [JSON.stringify(mergedSchema), JSON.stringify(ctx.values)]); (0, import_react2.useEffect)(() => { if (!resolvedAdvancedConfig?.onPaletteChange) return; const palette = resolvedAdvancedConfig.sections.reduce( (acc, section) => { const channels = CHANNEL_KEYS.reduce( (channelAcc, channel) => { const key = createPaletteControlKey( resolvedAdvancedConfig.hiddenKeyPrefix, section.key, channel ); const fallback = resolvedAdvancedConfig.defaultPalette?.[section.key]?.[channel] ?? 0; channelAcc[channel] = toNumberOr(ctx.values[key], fallback); return channelAcc; }, {} ); acc[section.key] = channels; return acc; }, {} ); const signature = createPaletteSignature(palette); if (lastAdvancedPaletteSignature.current === signature) return; lastAdvancedPaletteSignature.current = signature; resolvedAdvancedConfig.onPaletteChange(clonePalette(palette)); }, [ctx.values, resolvedAdvancedConfig]); const typedValues = ctx.values; const jsx15 = (0, import_react2.useCallback)(() => { if (!options?.componentName) return ""; const props = Object.entries(typedValues).map(([key, val]) => { if (typeof val === "string") return `${key}="${val}"`; if (typeof val === "boolean") return `${key}={${val}}`; return `${key}={${JSON.stringify(val)}}`; }).join(" "); return `<${options.componentName} ${props} />`; }, [options?.componentName, JSON.stringify(typedValues)]); return { ...typedValues, controls: ctx.values, schema: ctx.schema, setValue: ctx.setValue, jsx: jsx15 }; }; var useUrlSyncedControls = useControls; // src/components/ControlPanel.tsx var import_react6 = require("react"); var import_lucide_react4 = require("lucide-react"); // src/hooks/usePreviewUrl.ts var import_react3 = require("react"); var usePreviewUrl = (values, basePath = "") => { const [url, setUrl] = (0, import_react3.useState)(""); (0, import_react3.useEffect)(() => { if (typeof window === "undefined") return; const params = new URLSearchParams(); params.set(NO_CONTROLS_PARAM, "true"); for (const [key, value] of Object.entries(values)) { if (value !== void 0 && value !== null) { params.set(key, value.toString()); } } const fullUrl = `${basePath || window.location.pathname}?${params.toString()}`; setUrl(fullUrl); }, [values, basePath]); return url; }; // src/components/ui/switch.tsx var React3 = __toESM(require("react")); var SwitchPrimitives = __toESM(require("@radix-ui/react-switch")); // src/lib/utils.ts var import_clsx = require("clsx"); var import_tailwind_merge = require("tailwind-merge"); function cn(...inputs) { return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs)); } // src/components/ui/switch.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); var Switch = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( SwitchPrimitives.Root, { className: cn( "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", className ), ...props, ref, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( SwitchPrimitives.Thumb, { className: cn( "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" ) } ) } )); Switch.displayName = SwitchPrimitives.Root.displayName; // src/components/ui/label.tsx var React4 = __toESM(require("react")); var LabelPrimitive = __toESM(require("@radix-ui/react-label")); var import_class_variance_authority = require("class-variance-authority"); var import_jsx_runtime4 = require("react/jsx-runtime"); var labelVariants = (0, import_class_variance_authority.cva)( "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" ); var Label = React4.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( LabelPrimitive.Root, { ref, className: cn(labelVariants(), className), ...props } )); Label.displayName = LabelPrimitive.Root.displayName; // src/components/ui/slider.tsx var React5 = __toESM(require("react")); var SliderPrimitive = __toESM(require("@radix-ui/react-slider")); var import_jsx_runtime5 = require("react/jsx-runtime"); var Slider = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)( SliderPrimitive.Root, { ref, className: cn( "relative flex w-full touch-none select-none items-center", className ), ...props, children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }), /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SliderPrimitive.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" }) ] } )); Slider.displayName = SliderPrimitive.Root.displayName; // src/components/ui/input.tsx var React6 = __toESM(require("react")); var import_jsx_runtime6 = require("react/jsx-runtime"); var Input = React6.forwardRef( ({ className, type, ...props }, ref) => { return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)( "input", { type, className: cn( "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", className ), ref, ...props } ); } ); Input.displayName = "Input"; // src/components/ui/select.tsx var React7 = __toESM(require("react")); var SelectPrimitive = __toESM(require("@radix-ui/react-select")); var import_lucide_react2 = require("lucide-react"); var import_jsx_runtime7 = require("react/jsx-runtime"); var Select = SelectPrimitive.Root; var SelectValue = SelectPrimitive.Value; var SelectTrigger = React7.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)( SelectPrimitive.Trigger, { ref, className: cn( "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className ), ...props, children: [ children, /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.ChevronDown, { className: "h-4 w-4 opacity-50" }) }) ] } )); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; var SelectScrollUpButton = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( SelectPrimitive.ScrollUpButton, { ref, className: cn( "flex cursor-default items-center justify-center py-1", className ), ...props, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.ChevronUp, { className: "h-4 w-4" }) } )); SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; var SelectScrollDownButton = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( SelectPrimitive.ScrollDownButton, { ref, className: cn( "flex cursor-default items-center justify-center py-1", className ), ...props, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.ChevronDown, { className: "h-4 w-4" }) } )); SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; var SelectContent = React7.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectPrimitive.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)( SelectPrimitive.Content, { ref, className: cn( "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className ), position, ...props, children: [ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectScrollUpButton, {}), /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( SelectPrimitive.Viewport, { className: cn( "p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" ), children } ), /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectScrollDownButton, {}) ] } ) })); SelectContent.displayName = SelectPrimitive.Content.displayName; var SelectLabel = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( SelectPrimitive.Label, { ref, className: cn("px-2 py-1.5 text-sm font-semibold", className), ...props } )); SelectLabel.displayName = SelectPrimitive.Label.displayName; var SelectItem = React7.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)( SelectPrimitive.Item, { ref, className: cn( "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className ), ...props, children: [ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.Check, { className: "h-4 w-4" }) }) }), /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectPrimitive.ItemText, { children }) ] } )); SelectItem.displayName = SelectPrimitive.Item.displayName; var SelectSeparator = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( SelectPrimitive.Separator, { ref, className: cn("-mx-1 my-1 h-px bg-muted", className), ...props } )); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; // src/components/ui/button.tsx var React8 = __toESM(require("react")); var import_react_slot = require("@radix-ui/react-slot"); var import_class_variance_authority2 = require("class-variance-authority"); var import_jsx_runtime8 = require("react/jsx-runtime"); var buttonVariants = (0, import_class_variance_authority2.cva)( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "bg-gray-800 hover:bg-gray-900", link: "text-primary underline-offset-4 hover:underline" }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9" } }, defaultVariants: { variant: "default", size: "default" } } ); var Button = React8.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? import_react_slot.Slot : "button"; return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)( Comp, { className: cn(buttonVariants({ variant, size, className })), ref, ...props } ); } ); Button.displayName = "Button"; // src/constants/layout.ts var MOBILE_CONTROL_PANEL_PEEK = 112; // src/components/AdvancedPaletteControl.tsx var import_react4 = require("react"); var import_jsx_runtime9 = require("react/jsx-runtime"); var AdvancedPaletteControl = ({ config }) => { const { values, setValue } = useControlsContext(); const palette = (0, import_react4.useMemo)(() => { const result = {}; config.sections.forEach((section) => { result[section.key] = CHANNEL_KEYS.reduce((acc, channel) => { const key = createPaletteControlKey( config.hiddenKeyPrefix, section.key, channel ); const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0; acc[channel] = toNumberOr(values?.[key], defaultValue); return acc; }, {}); }); return result; }, [config.defaultPalette, config.hiddenKeyPrefix, config.sections, values]); const paletteGradient = (0, import_react4.useMemo)( () => computePaletteGradient(palette, config.gradientSteps), [palette, config.gradientSteps] ); const paletteSignature = (0, import_react4.useMemo)( () => createPaletteSignature(palette), [palette] ); const lastSignatureRef = (0, import_react4.useRef)(null); (0, import_react4.useEffect)(() => { if (!config.onPaletteChange) return; if (lastSignatureRef.current === paletteSignature) return; lastSignatureRef.current = paletteSignature; config.onPaletteChange(palette); }, [config, palette, paletteSignature]); const updatePaletteValue = (0, import_react4.useCallback)( (sectionKey, channel, nextValue) => { const range = config.ranges[sectionKey] ?? DEFAULT_RANGES[sectionKey] ?? { min: 0, max: 1, step: 0.01 }; const clamped = Math.min(Math.max(nextValue, range.min), range.max); config.onInteraction?.(); const controlKey = createPaletteControlKey( config.hiddenKeyPrefix, sectionKey, channel ); setValue(controlKey, clamped); }, [config, setValue] ); const handleResetPalette = (0, import_react4.useCallback)(() => { config.onInteraction?.(); config.sections.forEach((section) => { CHANNEL_KEYS.forEach((channel) => { const controlKey = createPaletteControlKey( config.hiddenKeyPrefix, section.key, channel ); const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0; setValue(controlKey, defaultValue); }); }); }, [config, setValue]); return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex w-full flex-col gap-6", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex w-full flex-col gap-4", children: [ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-xs font-semibold uppercase tracking-wide text-stone-200", children: "Palette" }), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)( "button", { type: "button", onClick: handleResetPalette, className: "rounded border border-stone-700 px-3 py-1 text-[10px] font-semibold uppercase tracking-widest text-stone-200 transition hover:border-stone-500", children: "Reset Palette" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)( "div", { className: "h-4 w-full rounded border border-stone-700", style: { background: paletteGradient } } ), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex flex-col gap-4", children: config.sections.map((section) => { const range = config.ranges[section.key]; return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-3", children: [ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-between text-[11px] uppercase tracking-widest text-stone-300", children: [ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: section.label }), section.helper && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-stone-500", children: section.helper }) ] }), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "grid grid-cols-3 gap-3", children: CHANNEL_KEYS.map((channel) => { const value = palette[section.key][channel]; const channelLabel = config.channelLabels?.[channel] ?? DEFAULT_CHANNEL_LABELS[channel]; return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-2", children: [ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-between text-[10px] uppercase tracking-widest text-stone-400", children: [ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: channelLabel }), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: value.toFixed(2) }) ] }), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)( "input", { type: "range", min: range.min, max: range.max, step: range.step, value, onPointerDown: config.onInteraction, onChange: (event) => updatePaletteValue( section.key, channel, parseFloat(event.target.value) ), className: "w-full cursor-pointer accent-stone-300" } ), /* @__PURE__ */ (0, import_jsx_runtime9.jsx)( "input", { type: "number", min: range.min, max: range.max, step: range.step, value: value.toFixed(3), onPointerDown: config.onInteraction, onFocus: config.onInteraction, onChange: (event) => { const parsed = parseFloat(event.target.value); if (Number.isNaN(parsed)) return; updatePaletteValue(section.key, channel, parsed); }, className: "w-full rounded border border-stone-700 bg-stone-900 px-2 py-1 text-xs text-stone-200 focus:border-stone-500 focus:outline-none" } ) ] }, channel); }) }) ] }, section.key); }) }) ] }) }); }; var AdvancedPaletteControl_default = AdvancedPaletteControl; // src/components/MediaUploadControl.tsx var import_react5 = require("react"); var import_lucide_react3 = require("lucide-react"); // src/state/mediaSelectionStore.ts var snapshot = { media: null, error: null }; var listeners = /* @__PURE__ */ new Set(); var emitChange = () => { for (const listener of listeners) { listener(); } }; var mediaSelectionStore = { subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }, getSnapshot: () => snapshot, setSnapshot: (next) => { snapshot = next; emitChange(); } }; // src/components/MediaUploadControl.tsx var import_jsx_runtime10 = require("react/jsx-runtime"); var DEFAULT_PRESET_MEDIA = [ { src: "/v0.png", label: "Default", type: "image" }, { src: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=240&auto=format&fit=crop", label: "Mountains", type: "image" }, { src: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=240&auto=format&fit=crop", label: "Beach", type: "image" }, { src: "https://images.unsplash.com/photo-1518770660439-4636190af475?w=240&auto=format&fit=crop", label: "City", type: "image" } ]; function MediaUploadControl({ onSelectMedia, onClear, presetMedia, maxPresetCount }) { const inputId = (0, import_react5.useId)(); const inputRef = (0, import_react5.useRef)(null); const uploadedUrlRef = (0, import_react5.useRef)(null); const { media, error } = (0, import_react5.useSyncExternalStore)( mediaSelectionStore.subscribe, mediaSelectionStore.getSnapshot, mediaSelectionStore.getSnapshot ); const VIDEO_EXTENSIONS = (0, import_react5.useMemo)( () => [".mp4", ".webm", ".ogg", ".ogv", ".mov", ".m4v"], [] ); const setSelection = (0, import_react5.useCallback)( (next) => { mediaSelectionStore.setSnapshot(next); }, [] ); const handleFileChange = (event) => { const file = event.target.files?.[0]; if (!file) { return; } if (uploadedUrlRef.current) { URL.revokeObjectURL(uploadedUrlRef.current); uploadedUrlRef.current = null; } const objectUrl = URL.createObjectURL(file); uploadedUrlRef.current = objectUrl; const lowerName = file.name?.toLowerCase() ?? ""; const hasVideoExtension = VIDEO_EXTENSIONS.some( (ext) => lowerName.endsWith(ext) ); const isVideo = file.type.startsWith("video/") || hasVideoExtension; if (isVideo) { setSelection({ media: null, error: "Videos are not supported in this effect yet." }); return; } const nextMedia = { src: objectUrl, type: "image" }; setSelection({ media: nextMedia, error: null }); onSelectMedia(nextMedia); }; const handleClearSelection = () => { if (uploadedUrlRef.current) { URL.revokeObjectURL(uploadedUrlRef.current); uploadedUrlRef.current = null; } setSelection({ media: null, error: null }); onClear(); }; const handlePresetSelect = (entry) => { if (entry.type === "video") { setSelection({ media: null, error: "Videos are not supported in this effect yet." }); return; } const nextMedia = { src: entry.src, type: entry.type }; setSelection({ media: nextMedia, error: null }); onSelectMedia(nextMedia); }; (0, import_react5.useEffect)(() => { return () => { if (uploadedUrlRef.current) { URL.revokeObjectURL(uploadedUrlRef.current); uploadedUrlRef.current = null; } }; }, []); const presets = (0, import_react5.useMemo)(() => { const source = presetMedia ?? DEFAULT_PRESET_MEDIA; if (typeof maxPresetCount === "number" && Number.isFinite(maxPresetCount)) { const safeCount = Math.max(0, Math.floor(maxPresetCount)); return source.slice(0, safeCount); } return source; }, [presetMedia, maxPresetCount]); return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)( "div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { htmlFor: inputId, style: { fontSize: "0.85rem", fontWeight: 500 }, children: "Upload media" }), /* @__PURE__ */ (0, import_jsx_runtime10.jsx)( "input", { id: inputId, type: "file", accept: "image/*", ref: inputRef, style: { display: "none" }, onChange: handleFileChange } ), /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)( "div", { style: { display: "flex", alignItems: "center", gap: "0.75rem" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)( "button", { type: "button", onClick: () => inputRef.current?.click(), style: { padding: "0.35rem 0.75rem", borderRadius: "0.4rem", border: "1px solid rgba(255, 255, 255, 0.25)", background: "rgba(255, 255, 255, 0.08)", color: "inherit", cursor: "pointer" }, children: "Choose file" } ), media ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)( "div", { style: { width: 36, height: 36, borderRadius: "0.35rem", overflow: "hidden", border: "1px solid rgba(255, 255, 255, 0.15)" }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)( "img", { src: media.src, alt: "Thumbnail", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } } ) } ) : null, media ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)( "button", { type: "button", onClick: handleClearSelection, style: { display: "flex