@toriistudio/v0-playground
Version:
1,324 lines (1,300 loc) • 92.1 kB
JavaScript
"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