@toriistudio/v0-playground 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/Playground/Playground.tsx
2
- import { useEffect as useEffect4, useMemo as useMemo3, useState as useState5 } from "react";
2
+ import { useEffect as useEffect5, useMemo as useMemo4, useState as useState5 } from "react";
3
3
  import { Check as Check3, Copy as Copy2 } from "lucide-react";
4
4
 
5
5
  // src/context/ResizableLayout.tsx
@@ -116,7 +116,8 @@ import {
116
116
  useState as useState2,
117
117
  useMemo,
118
118
  useEffect as useEffect2,
119
- useCallback
119
+ useCallback,
120
+ useRef as useRef2
120
121
  } from "react";
121
122
 
122
123
  // src/utils/getUrlParams.ts
@@ -130,6 +131,274 @@ var getUrlParams = () => {
130
131
  return entries;
131
132
  };
132
133
 
134
+ // src/constants/urlParams.ts
135
+ var NO_CONTROLS_PARAM = "nocontrols";
136
+ var PRESENTATION_PARAM = "presentation";
137
+ var CONTROLS_ONLY_PARAM = "controlsonly";
138
+
139
+ // src/utils/getControlsChannelName.ts
140
+ var EXCLUDED_KEYS = /* @__PURE__ */ new Set([
141
+ NO_CONTROLS_PARAM,
142
+ PRESENTATION_PARAM,
143
+ CONTROLS_ONLY_PARAM
144
+ ]);
145
+ var getControlsChannelName = () => {
146
+ if (typeof window === "undefined") return null;
147
+ const params = new URLSearchParams(window.location.search);
148
+ for (const key of EXCLUDED_KEYS) {
149
+ params.delete(key);
150
+ }
151
+ const query = params.toString();
152
+ const base = window.location.pathname || "/";
153
+ return `v0-controls:${base}${query ? `?${query}` : ""}`;
154
+ };
155
+
156
+ // src/lib/advancedPalette.ts
157
+ var CHANNEL_KEYS = ["r", "g", "b"];
158
+ var DEFAULT_CHANNEL_LABELS = {
159
+ r: "Red",
160
+ g: "Green",
161
+ b: "Blue"
162
+ };
163
+ var DEFAULT_SECTIONS = [
164
+ { key: "A", label: "Vector A", helper: "Base offset" },
165
+ { key: "B", label: "Vector B", helper: "Amplitude" },
166
+ { key: "C", label: "Vector C", helper: "Frequency" },
167
+ { key: "D", label: "Vector D", helper: "Phase shift" }
168
+ ];
169
+ var DEFAULT_RANGES = {
170
+ A: { min: 0, max: 1, step: 0.01 },
171
+ B: { min: -1, max: 1, step: 0.01 },
172
+ C: { min: 0, max: 2, step: 0.01 },
173
+ D: { min: 0, max: 1, step: 0.01 }
174
+ };
175
+ var DEFAULT_HIDDEN_KEY_PREFIX = "palette";
176
+ var DEFAULT_GRADIENT_STEPS = 12;
177
+ var DEFAULT_HEX_PALETTE = {
178
+ A: { r: 0.5, g: 0.5, b: 0.5 },
179
+ B: { r: 0.5, g: 0.5, b: 0.5 },
180
+ C: { r: 1, g: 1, b: 1 },
181
+ D: { r: 0, g: 0.1, b: 0.2 }
182
+ };
183
+ var createPaletteControlKey = (prefix, section, channel) => `${prefix}${section}${channel}`;
184
+ var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
185
+ var clamp01 = (value) => clamp(value, 0, 1);
186
+ var toNumberOr = (value, fallback) => {
187
+ if (typeof value === "number" && Number.isFinite(value)) return value;
188
+ if (typeof value === "string") {
189
+ const parsed = parseFloat(value);
190
+ if (Number.isFinite(parsed)) return parsed;
191
+ }
192
+ return fallback;
193
+ };
194
+ var paletteColorAt = (palette, t) => {
195
+ const twoPi = Math.PI * 2;
196
+ const computeChannel = (a, b, c, d) => {
197
+ const value = a + b * Math.cos(twoPi * (c * t + d));
198
+ return clamp(value, 0, 1);
199
+ };
200
+ return {
201
+ r: computeChannel(
202
+ palette.A?.r ?? 0,
203
+ palette.B?.r ?? 0,
204
+ palette.C?.r ?? 0,
205
+ palette.D?.r ?? 0
206
+ ),
207
+ g: computeChannel(
208
+ palette.A?.g ?? 0,
209
+ palette.B?.g ?? 0,
210
+ palette.C?.g ?? 0,
211
+ palette.D?.g ?? 0
212
+ ),
213
+ b: computeChannel(
214
+ palette.A?.b ?? 0,
215
+ palette.B?.b ?? 0,
216
+ palette.C?.b ?? 0,
217
+ palette.D?.b ?? 0
218
+ )
219
+ };
220
+ };
221
+ var toRgba = ({ r, g, b }, alpha = 0.5) => `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(
222
+ b * 255
223
+ )}, ${alpha})`;
224
+ var computePaletteGradient = (palette, steps = DEFAULT_GRADIENT_STEPS) => {
225
+ const stops = Array.from({ length: steps }, (_, index) => {
226
+ const t = index / (steps - 1);
227
+ const color = paletteColorAt(palette, t);
228
+ const stop = (t * 100).toFixed(1);
229
+ return `${toRgba(color)} ${stop}%`;
230
+ });
231
+ return `linear-gradient(to right, ${stops.join(", ")})`;
232
+ };
233
+ var createPaletteSignature = (palette) => Object.entries(palette).sort(([aKey], [bKey]) => aKey.localeCompare(bKey)).flatMap(
234
+ ([, channels]) => CHANNEL_KEYS.map((channel) => (channels?.[channel] ?? 0).toFixed(3))
235
+ ).join("-");
236
+ var isAdvancedPaletteValue = (value) => Boolean(
237
+ value && typeof value === "object" && CHANNEL_KEYS.every((channel) => {
238
+ const channelValue = value[channel];
239
+ return typeof channelValue === "number" && Number.isFinite(channelValue);
240
+ })
241
+ );
242
+ var isAdvancedPalette = (value) => Boolean(
243
+ value && typeof value === "object" && Object.values(value).every(
244
+ (entry) => isAdvancedPaletteValue(entry) || typeof entry === "object"
245
+ )
246
+ );
247
+ var normalizePaletteValue = (source) => {
248
+ if (typeof source === "string") {
249
+ return hexToPaletteValue(source);
250
+ }
251
+ const channelSource = source ?? {};
252
+ const toChannel = (channel) => clamp01(
253
+ toNumberOr(
254
+ channelSource[channel],
255
+ 0
256
+ )
257
+ );
258
+ return {
259
+ r: toChannel("r"),
260
+ g: toChannel("g"),
261
+ b: toChannel("b")
262
+ };
263
+ };
264
+ var createPaletteFromRecord = (record) => Object.entries(record).reduce((acc, [key, value]) => {
265
+ acc[key] = normalizePaletteValue(value);
266
+ return acc;
267
+ }, {});
268
+ var clonePalette = (palette) => Object.fromEntries(
269
+ Object.entries(palette).map(([sectionKey, channels]) => [
270
+ sectionKey,
271
+ { ...channels }
272
+ ])
273
+ );
274
+ var hexComponentToNormalized = (component) => clamp01(parseInt(component, 16) / 255 || 0);
275
+ var normalizedChannelToHex = (value) => Math.round(clamp01(value) * 255).toString(16).padStart(2, "0");
276
+ var sanitizeHex = (hex) => {
277
+ let sanitized = hex.trim();
278
+ if (sanitized.startsWith("#")) {
279
+ sanitized = sanitized.slice(1);
280
+ }
281
+ if (sanitized.length === 3) {
282
+ sanitized = sanitized.split("").map((char) => char + char).join("");
283
+ }
284
+ return sanitized.length === 6 ? sanitized : null;
285
+ };
286
+ var hexToPaletteValue = (hex) => {
287
+ const sanitized = sanitizeHex(hex);
288
+ if (!sanitized) {
289
+ return { r: 0, g: 0, b: 0 };
290
+ }
291
+ return {
292
+ r: hexComponentToNormalized(sanitized.slice(0, 2)),
293
+ g: hexComponentToNormalized(sanitized.slice(2, 4)),
294
+ b: hexComponentToNormalized(sanitized.slice(4, 6))
295
+ };
296
+ };
297
+ var paletteValueToHex = (value) => `#${normalizedChannelToHex(value.r)}${normalizedChannelToHex(
298
+ value.g
299
+ )}${normalizedChannelToHex(value.b)}`;
300
+ var createAdvancedPalette = (source = DEFAULT_HEX_PALETTE, options) => {
301
+ if (Array.isArray(source)) {
302
+ const order = options?.sectionOrder ?? DEFAULT_SECTIONS.map((section) => section.key);
303
+ const record = {};
304
+ source.forEach((value, index) => {
305
+ const preferredKey = order[index];
306
+ const fallbackKey = `Color${index + 1}`;
307
+ const key = preferredKey && !(preferredKey in record) ? preferredKey : fallbackKey;
308
+ record[key] = value;
309
+ });
310
+ return createPaletteFromRecord(record);
311
+ }
312
+ if (isAdvancedPalette(source)) {
313
+ return clonePalette(
314
+ Object.entries(source ?? {}).reduce(
315
+ (acc, [key, value]) => {
316
+ acc[key] = normalizePaletteValue(value);
317
+ return acc;
318
+ },
319
+ {}
320
+ )
321
+ );
322
+ }
323
+ if (source && typeof source === "object") {
324
+ return createPaletteFromRecord(source);
325
+ }
326
+ return createPaletteFromRecord(DEFAULT_HEX_PALETTE);
327
+ };
328
+ var DEFAULT_ADVANCED_PALETTE = createAdvancedPalette(
329
+ DEFAULT_HEX_PALETTE
330
+ );
331
+ var advancedPaletteToHexColors = (palette, options) => {
332
+ const fallbackPalette = options?.fallbackPalette ?? DEFAULT_ADVANCED_PALETTE;
333
+ const orderedKeys = options?.sectionOrder ?? (Object.keys(palette).length > 0 ? Object.keys(palette) : Object.keys(fallbackPalette));
334
+ const uniqueKeys = Array.from(new Set(orderedKeys));
335
+ if (uniqueKeys.length === 0) {
336
+ uniqueKeys.push(...Object.keys(DEFAULT_ADVANCED_PALETTE));
337
+ }
338
+ const defaultColor = options?.defaultColor ?? "#000000";
339
+ return uniqueKeys.map((key) => {
340
+ const paletteValue = palette[key] ?? fallbackPalette[key];
341
+ if (!paletteValue) return defaultColor;
342
+ return paletteValueToHex(paletteValue);
343
+ });
344
+ };
345
+ var createDefaultSectionsFromPalette = (palette) => {
346
+ const sectionKeys = Object.keys(palette);
347
+ if (sectionKeys.length === 0) return DEFAULT_SECTIONS;
348
+ return sectionKeys.map((key, index) => ({
349
+ key,
350
+ label: `Vector ${key}`,
351
+ helper: DEFAULT_SECTIONS[index]?.helper ?? "Palette parameter"
352
+ }));
353
+ };
354
+ var resolveAdvancedPaletteConfig = (config) => {
355
+ const defaultPalette = createAdvancedPalette(config.defaultPalette);
356
+ const sections = config.sections ?? createDefaultSectionsFromPalette(defaultPalette);
357
+ const ranges = {};
358
+ sections.forEach((section) => {
359
+ ranges[section.key] = config.ranges?.[section.key] ?? DEFAULT_RANGES[section.key] ?? {
360
+ min: 0,
361
+ max: 1,
362
+ step: 0.01
363
+ };
364
+ });
365
+ const channelLabels = {
366
+ ...DEFAULT_CHANNEL_LABELS,
367
+ ...config.channelLabels ?? {}
368
+ };
369
+ return {
370
+ ...config,
371
+ defaultPalette,
372
+ sections,
373
+ ranges,
374
+ channelLabels,
375
+ hiddenKeyPrefix: config.hiddenKeyPrefix ?? DEFAULT_HIDDEN_KEY_PREFIX,
376
+ controlKey: config.controlKey ?? "advancedPaletteControl",
377
+ gradientSteps: config.gradientSteps ?? DEFAULT_GRADIENT_STEPS
378
+ };
379
+ };
380
+ var createAdvancedPaletteSchemaEntries = (schema, resolvedConfig) => {
381
+ const { sections, hiddenKeyPrefix, defaultPalette } = resolvedConfig;
382
+ const updatedSchema = { ...schema };
383
+ sections.forEach((section) => {
384
+ CHANNEL_KEYS.forEach((channel) => {
385
+ const key = createPaletteControlKey(
386
+ hiddenKeyPrefix,
387
+ section.key,
388
+ channel
389
+ );
390
+ if (!(key in updatedSchema)) {
391
+ updatedSchema[key] = {
392
+ type: "number",
393
+ value: defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0,
394
+ hidden: true
395
+ };
396
+ }
397
+ });
398
+ });
399
+ return updatedSchema;
400
+ };
401
+
133
402
  // src/context/ControlsContext.tsx
134
403
  import { jsx as jsx2 } from "react/jsx-runtime";
135
404
  var ControlsContext = createContext2(null);
@@ -145,6 +414,18 @@ var ControlsProvider = ({ children }) => {
145
414
  showCopyButton: true
146
415
  });
147
416
  const [componentName, setComponentName] = useState2();
417
+ const [channelName, setChannelName] = useState2(null);
418
+ const channelRef = useRef2(null);
419
+ const instanceIdRef = useRef2(null);
420
+ const skipBroadcastRef = useRef2(false);
421
+ const latestValuesRef = useRef2(values);
422
+ useEffect2(() => {
423
+ latestValuesRef.current = values;
424
+ }, [values]);
425
+ useEffect2(() => {
426
+ if (typeof window === "undefined") return;
427
+ setChannelName(getControlsChannelName());
428
+ }, []);
148
429
  const setValue = (key, value) => {
149
430
  setValues((prev) => ({ ...prev, [key]: value }));
150
431
  };
@@ -153,9 +434,16 @@ var ControlsProvider = ({ children }) => {
153
434
  setComponentName(opts.componentName);
154
435
  }
155
436
  if (opts?.config) {
437
+ const { addAdvancedPaletteControl, ...otherConfig } = opts.config;
156
438
  setConfig((prev) => ({
157
439
  ...prev,
158
- ...opts.config
440
+ ...otherConfig,
441
+ ...Object.prototype.hasOwnProperty.call(
442
+ opts.config,
443
+ "addAdvancedPaletteControl"
444
+ ) ? {
445
+ addAdvancedPaletteControl: addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(addAdvancedPaletteControl) : void 0
446
+ } : {}
159
447
  }));
160
448
  }
161
449
  setSchema((prevSchema) => ({ ...prevSchema, ...newSchema }));
@@ -172,6 +460,66 @@ var ControlsProvider = ({ children }) => {
172
460
  return updated;
173
461
  });
174
462
  };
463
+ useEffect2(() => {
464
+ if (!channelName) return;
465
+ if (typeof window === "undefined") return;
466
+ if (typeof window.BroadcastChannel === "undefined") return;
467
+ const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
468
+ instanceIdRef.current = instanceId;
469
+ const channel = new BroadcastChannel(channelName);
470
+ channelRef.current = channel;
471
+ const sendValues = () => {
472
+ if (!instanceIdRef.current) return;
473
+ channel.postMessage({
474
+ type: "controls-sync-values",
475
+ source: instanceIdRef.current,
476
+ values: latestValuesRef.current
477
+ });
478
+ };
479
+ const handleMessage = (event) => {
480
+ const data = event.data;
481
+ if (!data || data.source === instanceIdRef.current) return;
482
+ if (data.type === "controls-sync-request") {
483
+ sendValues();
484
+ return;
485
+ }
486
+ if (data.type === "controls-sync-values" && data.values) {
487
+ const incoming = data.values;
488
+ setValues((prev) => {
489
+ const prevKeys = Object.keys(prev);
490
+ const incomingKeys = Object.keys(incoming);
491
+ const sameLength = prevKeys.length === incomingKeys.length;
492
+ const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]);
493
+ if (sameValues) return prev;
494
+ skipBroadcastRef.current = true;
495
+ return { ...incoming };
496
+ });
497
+ }
498
+ };
499
+ channel.addEventListener("message", handleMessage);
500
+ channel.postMessage({
501
+ type: "controls-sync-request",
502
+ source: instanceId
503
+ });
504
+ return () => {
505
+ channel.removeEventListener("message", handleMessage);
506
+ channel.close();
507
+ channelRef.current = null;
508
+ instanceIdRef.current = null;
509
+ };
510
+ }, [channelName]);
511
+ useEffect2(() => {
512
+ if (!channelRef.current || !instanceIdRef.current) return;
513
+ if (skipBroadcastRef.current) {
514
+ skipBroadcastRef.current = false;
515
+ return;
516
+ }
517
+ channelRef.current.postMessage({
518
+ type: "controls-sync-values",
519
+ source: instanceIdRef.current,
520
+ values
521
+ });
522
+ }, [values]);
175
523
  const contextValue = useMemo(
176
524
  () => ({
177
525
  schema,
@@ -188,28 +536,41 @@ var ControlsProvider = ({ children }) => {
188
536
  var useControls = (schema, options) => {
189
537
  const ctx = useContext2(ControlsContext);
190
538
  if (!ctx) throw new Error("useControls must be used within ControlsProvider");
539
+ const lastAdvancedPaletteSignature = useRef2(null);
191
540
  const urlParams = getUrlParams();
192
- const mergedSchema = Object.fromEntries(
193
- Object.entries(schema).map(([key, control]) => {
194
- const urlValue = urlParams[key];
195
- if (!urlValue || !("value" in control)) return [key, control];
196
- const defaultValue = control.value;
197
- let parsed = urlValue;
198
- if (typeof defaultValue === "number") {
199
- parsed = parseFloat(urlValue);
200
- if (isNaN(parsed)) parsed = defaultValue;
201
- } else if (typeof defaultValue === "boolean") {
202
- parsed = urlValue === "true";
203
- }
204
- return [
205
- key,
206
- {
207
- ...control,
208
- value: parsed
541
+ const resolvedAdvancedConfig = options?.config?.addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(options.config.addAdvancedPaletteControl) : void 0;
542
+ const schemaWithAdvanced = useMemo(() => {
543
+ const baseSchema = { ...schema };
544
+ if (!resolvedAdvancedConfig) return baseSchema;
545
+ return createAdvancedPaletteSchemaEntries(
546
+ baseSchema,
547
+ resolvedAdvancedConfig
548
+ );
549
+ }, [schema, resolvedAdvancedConfig]);
550
+ const urlParamsKey = useMemo(() => JSON.stringify(urlParams), [urlParams]);
551
+ const mergedSchema = useMemo(() => {
552
+ return Object.fromEntries(
553
+ Object.entries(schemaWithAdvanced).map(([key, control]) => {
554
+ const urlValue = urlParams[key];
555
+ if (!urlValue || !("value" in control)) return [key, control];
556
+ const defaultValue = control.value;
557
+ let parsed = urlValue;
558
+ if (typeof defaultValue === "number") {
559
+ parsed = parseFloat(urlValue);
560
+ if (isNaN(parsed)) parsed = defaultValue;
561
+ } else if (typeof defaultValue === "boolean") {
562
+ parsed = urlValue === "true";
209
563
  }
210
- ];
211
- })
212
- );
564
+ return [
565
+ key,
566
+ {
567
+ ...control,
568
+ value: parsed
569
+ }
570
+ ];
571
+ })
572
+ );
573
+ }, [schemaWithAdvanced, urlParams, urlParamsKey]);
213
574
  useEffect2(() => {
214
575
  ctx.registerSchema(mergedSchema, options);
215
576
  }, [JSON.stringify(mergedSchema), JSON.stringify(options)]);
@@ -220,8 +581,35 @@ var useControls = (schema, options) => {
220
581
  }
221
582
  }
222
583
  }, [JSON.stringify(mergedSchema), JSON.stringify(ctx.values)]);
584
+ useEffect2(() => {
585
+ if (!resolvedAdvancedConfig?.onPaletteChange) return;
586
+ const palette = resolvedAdvancedConfig.sections.reduce(
587
+ (acc, section) => {
588
+ const channels = CHANNEL_KEYS.reduce(
589
+ (channelAcc, channel) => {
590
+ const key = createPaletteControlKey(
591
+ resolvedAdvancedConfig.hiddenKeyPrefix,
592
+ section.key,
593
+ channel
594
+ );
595
+ const fallback = resolvedAdvancedConfig.defaultPalette?.[section.key]?.[channel] ?? 0;
596
+ channelAcc[channel] = toNumberOr(ctx.values[key], fallback);
597
+ return channelAcc;
598
+ },
599
+ {}
600
+ );
601
+ acc[section.key] = channels;
602
+ return acc;
603
+ },
604
+ {}
605
+ );
606
+ const signature = createPaletteSignature(palette);
607
+ if (lastAdvancedPaletteSignature.current === signature) return;
608
+ lastAdvancedPaletteSignature.current = signature;
609
+ resolvedAdvancedConfig.onPaletteChange(clonePalette(palette));
610
+ }, [ctx.values, resolvedAdvancedConfig]);
223
611
  const typedValues = ctx.values;
224
- const jsx16 = useCallback(() => {
612
+ const jsx14 = useCallback(() => {
225
613
  if (!options?.componentName) return "";
226
614
  const props = Object.entries(typedValues).map(([key, val]) => {
227
615
  if (typeof val === "string") return `${key}="${val}"`;
@@ -235,14 +623,20 @@ var useControls = (schema, options) => {
235
623
  controls: ctx.values,
236
624
  schema: ctx.schema,
237
625
  setValue: ctx.setValue,
238
- jsx: jsx16
626
+ jsx: jsx14
239
627
  };
240
628
  };
241
629
  var useUrlSyncedControls = useControls;
242
630
 
243
631
  // src/components/ControlPanel/ControlPanel.tsx
244
- import { useState as useState4, useMemo as useMemo2 } from "react";
245
- import { Check as Check2, Copy, SquareArrowOutUpRight } from "lucide-react";
632
+ import { useState as useState4, useMemo as useMemo3, useCallback as useCallback3 } from "react";
633
+ import {
634
+ Check as Check2,
635
+ Copy,
636
+ SquareArrowOutUpRight,
637
+ ChevronDown as ChevronDown2,
638
+ Presentation
639
+ } from "lucide-react";
246
640
 
247
641
  // src/hooks/usePreviewUrl.ts
248
642
  import { useEffect as useEffect3, useState as useState3 } from "react";
@@ -251,7 +645,7 @@ var usePreviewUrl = (values, basePath = "") => {
251
645
  useEffect3(() => {
252
646
  if (typeof window === "undefined") return;
253
647
  const params = new URLSearchParams();
254
- params.set("nocontrols", "true");
648
+ params.set(NO_CONTROLS_PARAM, "true");
255
649
  for (const [key, value] of Object.entries(values)) {
256
650
  if (value !== void 0 && value !== null) {
257
651
  params.set(key, value.toString());
@@ -514,20 +908,223 @@ var Button = React8.forwardRef(
514
908
  );
515
909
  Button.displayName = "Button";
516
910
 
911
+ // src/constants/layout.ts
912
+ var MOBILE_CONTROL_PANEL_PEEK = 112;
913
+
914
+ // src/components/AdvancedPaletteControl/AdvancedPaletteControl.tsx
915
+ import {
916
+ useCallback as useCallback2,
917
+ useEffect as useEffect4,
918
+ useMemo as useMemo2,
919
+ useRef as useRef3
920
+ } from "react";
921
+ import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
922
+ var AdvancedPaletteControl = ({
923
+ config
924
+ }) => {
925
+ const { values, setValue } = useControlsContext();
926
+ const palette = useMemo2(() => {
927
+ const result = {};
928
+ config.sections.forEach((section) => {
929
+ result[section.key] = CHANNEL_KEYS.reduce((acc, channel) => {
930
+ const key = createPaletteControlKey(
931
+ config.hiddenKeyPrefix,
932
+ section.key,
933
+ channel
934
+ );
935
+ const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0;
936
+ acc[channel] = toNumberOr(values?.[key], defaultValue);
937
+ return acc;
938
+ }, {});
939
+ });
940
+ return result;
941
+ }, [config.defaultPalette, config.hiddenKeyPrefix, config.sections, values]);
942
+ const paletteGradient = useMemo2(
943
+ () => computePaletteGradient(palette, config.gradientSteps),
944
+ [palette, config.gradientSteps]
945
+ );
946
+ const paletteSignature = useMemo2(
947
+ () => createPaletteSignature(palette),
948
+ [palette]
949
+ );
950
+ const lastSignatureRef = useRef3(null);
951
+ useEffect4(() => {
952
+ if (!config.onPaletteChange) return;
953
+ if (lastSignatureRef.current === paletteSignature) return;
954
+ lastSignatureRef.current = paletteSignature;
955
+ config.onPaletteChange(palette);
956
+ }, [config, palette, paletteSignature]);
957
+ const updatePaletteValue = useCallback2(
958
+ (sectionKey, channel, nextValue) => {
959
+ const range = config.ranges[sectionKey] ?? DEFAULT_RANGES[sectionKey] ?? {
960
+ min: 0,
961
+ max: 1,
962
+ step: 0.01
963
+ };
964
+ const clamped = Math.min(Math.max(nextValue, range.min), range.max);
965
+ config.onInteraction?.();
966
+ const controlKey = createPaletteControlKey(
967
+ config.hiddenKeyPrefix,
968
+ sectionKey,
969
+ channel
970
+ );
971
+ setValue(controlKey, clamped);
972
+ },
973
+ [config, setValue]
974
+ );
975
+ const handleResetPalette = useCallback2(() => {
976
+ config.onInteraction?.();
977
+ config.sections.forEach((section) => {
978
+ CHANNEL_KEYS.forEach((channel) => {
979
+ const controlKey = createPaletteControlKey(
980
+ config.hiddenKeyPrefix,
981
+ section.key,
982
+ channel
983
+ );
984
+ const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0;
985
+ setValue(controlKey, defaultValue);
986
+ });
987
+ });
988
+ }, [config, setValue]);
989
+ return /* @__PURE__ */ jsx9("div", { className: "flex w-full flex-col gap-6", children: /* @__PURE__ */ jsxs4("div", { className: "flex w-full flex-col gap-4", children: [
990
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between", children: [
991
+ /* @__PURE__ */ jsx9("span", { className: "text-xs font-semibold uppercase tracking-wide text-stone-200", children: "Palette" }),
992
+ /* @__PURE__ */ jsx9(
993
+ "button",
994
+ {
995
+ type: "button",
996
+ onClick: handleResetPalette,
997
+ 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",
998
+ children: "Reset Palette"
999
+ }
1000
+ )
1001
+ ] }),
1002
+ /* @__PURE__ */ jsx9(
1003
+ "div",
1004
+ {
1005
+ className: "h-4 w-full rounded border border-stone-700",
1006
+ style: { background: paletteGradient }
1007
+ }
1008
+ ),
1009
+ /* @__PURE__ */ jsx9("div", { className: "flex flex-col gap-4", children: config.sections.map((section) => {
1010
+ const range = config.ranges[section.key];
1011
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
1012
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between text-[11px] uppercase tracking-widest text-stone-300", children: [
1013
+ /* @__PURE__ */ jsx9("span", { children: section.label }),
1014
+ section.helper && /* @__PURE__ */ jsx9("span", { className: "text-stone-500", children: section.helper })
1015
+ ] }),
1016
+ /* @__PURE__ */ jsx9("div", { className: "grid grid-cols-3 gap-3", children: CHANNEL_KEYS.map((channel) => {
1017
+ const value = palette[section.key][channel];
1018
+ const channelLabel = config.channelLabels?.[channel] ?? DEFAULT_CHANNEL_LABELS[channel];
1019
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
1020
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between text-[10px] uppercase tracking-widest text-stone-400", children: [
1021
+ /* @__PURE__ */ jsx9("span", { children: channelLabel }),
1022
+ /* @__PURE__ */ jsx9("span", { children: value.toFixed(2) })
1023
+ ] }),
1024
+ /* @__PURE__ */ jsx9(
1025
+ "input",
1026
+ {
1027
+ type: "range",
1028
+ min: range.min,
1029
+ max: range.max,
1030
+ step: range.step,
1031
+ value,
1032
+ onPointerDown: config.onInteraction,
1033
+ onChange: (event) => updatePaletteValue(
1034
+ section.key,
1035
+ channel,
1036
+ parseFloat(event.target.value)
1037
+ ),
1038
+ className: "w-full cursor-pointer accent-stone-300"
1039
+ }
1040
+ ),
1041
+ /* @__PURE__ */ jsx9(
1042
+ "input",
1043
+ {
1044
+ type: "number",
1045
+ min: range.min,
1046
+ max: range.max,
1047
+ step: range.step,
1048
+ value: value.toFixed(3),
1049
+ onPointerDown: config.onInteraction,
1050
+ onFocus: config.onInteraction,
1051
+ onChange: (event) => {
1052
+ const parsed = parseFloat(event.target.value);
1053
+ if (Number.isNaN(parsed)) return;
1054
+ updatePaletteValue(section.key, channel, parsed);
1055
+ },
1056
+ 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"
1057
+ }
1058
+ )
1059
+ ] }, channel);
1060
+ }) })
1061
+ ] }, section.key);
1062
+ }) })
1063
+ ] }) });
1064
+ };
1065
+ var AdvancedPaletteControl_default = AdvancedPaletteControl;
1066
+
517
1067
  // src/components/ControlPanel/ControlPanel.tsx
518
- import { Fragment, jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
1068
+ import { Fragment, jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
519
1069
  var ControlPanel = () => {
520
1070
  const [copied, setCopied] = useState4(false);
1071
+ const [folderStates, setFolderStates] = useState4({});
521
1072
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
522
1073
  const { schema, setValue, values, componentName, config } = useControlsContext();
523
1074
  const previewUrl = usePreviewUrl(values);
524
- const normalControls = Object.entries(schema).filter(
525
- ([, control]) => control.type !== "button" && !control.hidden
526
- );
527
- const buttonControls = Object.entries(schema).filter(
528
- ([, control]) => control.type === "button" && !control.hidden
1075
+ const buildUrl = useCallback3(
1076
+ (modifier) => {
1077
+ if (!previewUrl) return "";
1078
+ const [path, search = ""] = previewUrl.split("?");
1079
+ const params = new URLSearchParams(search);
1080
+ modifier(params);
1081
+ const query = params.toString();
1082
+ return query ? `${path}?${query}` : path;
1083
+ },
1084
+ [previewUrl]
529
1085
  );
530
- const jsx16 = useMemo2(() => {
1086
+ const presentationUrl = useMemo3(() => {
1087
+ if (!previewUrl) return "";
1088
+ return buildUrl((params) => {
1089
+ params.set(PRESENTATION_PARAM, "true");
1090
+ });
1091
+ }, [buildUrl, previewUrl]);
1092
+ const controlsOnlyUrl = useMemo3(() => {
1093
+ if (!previewUrl) return "";
1094
+ return buildUrl((params) => {
1095
+ params.delete(NO_CONTROLS_PARAM);
1096
+ params.delete(PRESENTATION_PARAM);
1097
+ params.set(CONTROLS_ONLY_PARAM, "true");
1098
+ });
1099
+ }, [buildUrl, previewUrl]);
1100
+ const handlePresentationClick = useCallback3(() => {
1101
+ if (typeof window === "undefined" || !presentationUrl) return;
1102
+ window.open(presentationUrl, "_blank", "noopener,noreferrer");
1103
+ if (controlsOnlyUrl) {
1104
+ const viewportWidth = window.innerWidth || 1200;
1105
+ const viewportHeight = window.innerHeight || 900;
1106
+ const controlsWidth = Math.max(
1107
+ 320,
1108
+ Math.min(
1109
+ 600,
1110
+ Math.round(viewportWidth * leftPanelWidth / 100)
1111
+ )
1112
+ );
1113
+ const controlsHeight = Math.max(600, viewportHeight);
1114
+ const controlsFeatures = [
1115
+ "noopener",
1116
+ "noreferrer",
1117
+ "toolbar=0",
1118
+ "menubar=0",
1119
+ "resizable=yes",
1120
+ "scrollbars=yes",
1121
+ `width=${controlsWidth}`,
1122
+ `height=${controlsHeight}`
1123
+ ].join(",");
1124
+ window.open(controlsOnlyUrl, "v0-controls", controlsFeatures);
1125
+ }
1126
+ }, [controlsOnlyUrl, leftPanelWidth, presentationUrl]);
1127
+ const jsx14 = useMemo3(() => {
531
1128
  if (!componentName) return "";
532
1129
  const props = Object.entries(values).map(([key, val]) => {
533
1130
  if (typeof val === "string") return `${key}="${val}"`;
@@ -536,182 +1133,367 @@ var ControlPanel = () => {
536
1133
  }).join(" ");
537
1134
  return `<${componentName} ${props} />`;
538
1135
  }, [componentName, values]);
539
- return /* @__PURE__ */ jsx9(
1136
+ const visibleEntries = Object.entries(schema).filter(
1137
+ ([, control]) => !control.hidden
1138
+ );
1139
+ const rootControls = [];
1140
+ const folderOrder = [];
1141
+ const folderControls = /* @__PURE__ */ new Map();
1142
+ const folderExtras = /* @__PURE__ */ new Map();
1143
+ const folderPlacement = /* @__PURE__ */ new Map();
1144
+ const seenFolders = /* @__PURE__ */ new Set();
1145
+ const ensureFolder = (folder) => {
1146
+ if (!seenFolders.has(folder)) {
1147
+ seenFolders.add(folder);
1148
+ folderOrder.push(folder);
1149
+ }
1150
+ };
1151
+ visibleEntries.forEach((entry) => {
1152
+ const [key, control] = entry;
1153
+ const folder = control.folder?.trim();
1154
+ if (folder) {
1155
+ const placement = control.folderPlacement ?? "bottom";
1156
+ ensureFolder(folder);
1157
+ if (!folderControls.has(folder)) {
1158
+ folderControls.set(folder, []);
1159
+ }
1160
+ folderControls.get(folder).push(entry);
1161
+ const existingPlacement = folderPlacement.get(folder);
1162
+ if (!existingPlacement || placement === "top") {
1163
+ folderPlacement.set(folder, placement);
1164
+ }
1165
+ } else {
1166
+ rootControls.push(entry);
1167
+ }
1168
+ });
1169
+ const advancedConfig = config?.addAdvancedPaletteControl;
1170
+ let advancedPaletteControlNode = null;
1171
+ if (advancedConfig) {
1172
+ const advancedNode = /* @__PURE__ */ jsx10(
1173
+ AdvancedPaletteControl_default,
1174
+ {
1175
+ config: advancedConfig
1176
+ },
1177
+ "advancedPaletteControl"
1178
+ );
1179
+ const advancedFolder = advancedConfig.folder?.trim();
1180
+ if (advancedFolder) {
1181
+ const placement = advancedConfig.folderPlacement ?? "bottom";
1182
+ ensureFolder(advancedFolder);
1183
+ if (!folderControls.has(advancedFolder)) {
1184
+ folderControls.set(advancedFolder, []);
1185
+ }
1186
+ const existingPlacement = folderPlacement.get(advancedFolder);
1187
+ if (!existingPlacement || placement === "top") {
1188
+ folderPlacement.set(advancedFolder, placement);
1189
+ }
1190
+ if (!folderExtras.has(advancedFolder)) {
1191
+ folderExtras.set(advancedFolder, []);
1192
+ }
1193
+ folderExtras.get(advancedFolder).push(advancedNode);
1194
+ } else {
1195
+ advancedPaletteControlNode = advancedNode;
1196
+ }
1197
+ }
1198
+ const rootButtonControls = rootControls.filter(
1199
+ ([, control]) => control.type === "button"
1200
+ );
1201
+ const rootNormalControls = rootControls.filter(
1202
+ ([, control]) => control.type !== "button"
1203
+ );
1204
+ const folderGroups = folderOrder.map((folder) => ({
1205
+ folder,
1206
+ entries: folderControls.get(folder) ?? [],
1207
+ extras: folderExtras.get(folder) ?? [],
1208
+ placement: folderPlacement.get(folder) ?? "bottom"
1209
+ })).filter((group) => group.entries.length > 0 || group.extras.length > 0);
1210
+ const hasRootButtonControls = rootButtonControls.length > 0;
1211
+ const hasAnyFolders = folderGroups.length > 0;
1212
+ const jsonToComponentString = useCallback3(
1213
+ ({
1214
+ componentName: componentNameOverride,
1215
+ props
1216
+ }) => {
1217
+ const resolvedComponentName = componentNameOverride ?? componentName;
1218
+ if (!resolvedComponentName) return "";
1219
+ const formatProp = (key, value) => {
1220
+ if (value === void 0) return null;
1221
+ if (value === null) return `${key}={null}`;
1222
+ if (typeof value === "string") return `${key}="${value}"`;
1223
+ if (typeof value === "number" || typeof value === "boolean") {
1224
+ return `${key}={${value}}`;
1225
+ }
1226
+ if (typeof value === "bigint") {
1227
+ return `${key}={${value.toString()}n}`;
1228
+ }
1229
+ return `${key}={${JSON.stringify(value)}}`;
1230
+ };
1231
+ const formattedProps = Object.entries(props ?? {}).map(([key, value]) => formatProp(key, value)).filter((prop) => Boolean(prop)).join(" ");
1232
+ if (!formattedProps) {
1233
+ return `<${resolvedComponentName} />`;
1234
+ }
1235
+ return `<${resolvedComponentName} ${formattedProps} />`;
1236
+ },
1237
+ [componentName]
1238
+ );
1239
+ const copyText = config?.showCopyButtonFn?.({
1240
+ componentName,
1241
+ values,
1242
+ schema,
1243
+ jsx: jsx14,
1244
+ jsonToComponentString
1245
+ }) ?? jsx14;
1246
+ const shouldShowCopyButton = config?.showCopyButton !== false && Boolean(copyText);
1247
+ const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
1248
+ const renderButtonControl = (key, control, variant) => /* @__PURE__ */ jsx10(
540
1249
  "div",
541
1250
  {
542
- className: `order-2 md:order-1 w-full md:h-auto p-2 md:p-4 bg-stone-900 font-mono text-stone-300 transition-opacity duration-300 ${!isHydrated ? "opacity-0" : "opacity-100"}`,
543
- onPointerDown: (e) => e.stopPropagation(),
544
- onTouchStart: (e) => e.stopPropagation(),
545
- style: {
546
- width: "100%",
547
- height: "auto",
548
- flex: "0 0 auto",
549
- ...isHydrated && isDesktop ? {
550
- position: "absolute",
551
- left: 0,
552
- top: 0,
553
- bottom: 0,
554
- width: `${leftPanelWidth}%`,
555
- overflowY: "auto"
556
- } : {}
557
- },
558
- children: /* @__PURE__ */ jsxs4("div", { className: "mb-10 space-y-4 p-2 md:p-4 border border-stone-700 rounded-md", children: [
559
- /* @__PURE__ */ jsx9("div", { className: "space-y-1", children: /* @__PURE__ */ jsx9("h1", { className: "text-lg text-stone-100 font-bold", children: config?.mainLabel ?? "Controls" }) }),
560
- /* @__PURE__ */ jsxs4("div", { className: "space-y-4 pt-2", children: [
561
- normalControls.map(([key, control]) => {
562
- const value = values[key];
563
- switch (control.type) {
564
- case "boolean":
565
- return /* @__PURE__ */ jsxs4(
566
- "div",
567
- {
568
- className: "flex items-center space-x-4 border-t border-stone-700 pt-4",
569
- children: [
570
- /* @__PURE__ */ jsx9(
571
- Switch,
572
- {
573
- id: key,
574
- checked: value,
575
- onCheckedChange: (v) => setValue(key, v),
576
- className: "data-[state=checked]:bg-stone-700 data-[state=unchecked]:bg-stone-700/40"
577
- }
578
- ),
579
- /* @__PURE__ */ jsx9(Label, { htmlFor: key, className: "cursor-pointer", children: key })
580
- ]
581
- },
582
- key
583
- );
584
- case "number":
585
- return /* @__PURE__ */ jsxs4("div", { className: "space-y-2 w-full", children: [
586
- /* @__PURE__ */ jsx9("div", { className: "flex items-center justify-between pb-1", children: /* @__PURE__ */ jsxs4(Label, { className: "text-stone-300", htmlFor: key, children: [
587
- key,
588
- ": ",
589
- value
590
- ] }) }),
591
- /* @__PURE__ */ jsx9(
592
- Slider,
593
- {
594
- id: key,
595
- min: control.min ?? 0,
596
- max: control.max ?? 100,
597
- step: control.step ?? 1,
598
- value: [value],
599
- onValueChange: ([v]) => setValue(key, v),
600
- className: "[&>span]:border-none [&_.bg-primary]:bg-stone-800 [&>.bg-background]:bg-stone-500/30"
601
- }
602
- )
603
- ] }, key);
604
- case "string":
605
- return /* @__PURE__ */ jsx9(
606
- Input,
607
- {
608
- id: key,
609
- value,
610
- className: "bg-stone-900",
611
- placeholder: key,
612
- onChange: (e) => setValue(key, e.target.value)
613
- },
614
- key
615
- );
616
- case "color":
617
- return /* @__PURE__ */ jsxs4("div", { className: "space-y-2 w-full", children: [
618
- /* @__PURE__ */ jsx9("div", { className: "flex items-center justify-between pb-1", children: /* @__PURE__ */ jsx9(Label, { className: "text-stone-300", htmlFor: key, children: key }) }),
619
- /* @__PURE__ */ jsx9(
620
- "input",
621
- {
622
- type: "color",
623
- id: key,
624
- value,
625
- onChange: (e) => setValue(key, e.target.value),
626
- className: "w-full h-10 rounded border border-stone-600 bg-transparent"
627
- }
628
- )
629
- ] }, key);
630
- case "select":
631
- return /* @__PURE__ */ jsxs4(
632
- "div",
633
- {
634
- className: "space-y-2 border-t border-stone-700 pt-4",
635
- children: [
636
- /* @__PURE__ */ jsx9(Label, { className: "text-stone-300", htmlFor: key, children: key }),
637
- /* @__PURE__ */ jsxs4(
638
- Select,
639
- {
640
- value,
641
- onValueChange: (val) => setValue(key, val),
642
- children: [
643
- /* @__PURE__ */ jsx9(SelectTrigger, { children: /* @__PURE__ */ jsx9(SelectValue, { placeholder: "Select option" }) }),
644
- /* @__PURE__ */ jsx9(SelectContent, { children: Object.entries(control.options).map(
645
- ([label, _val]) => /* @__PURE__ */ jsx9(SelectItem, { value: label, children: label }, label)
646
- ) })
647
- ]
648
- }
649
- )
650
- ]
651
- },
652
- key
653
- );
654
- default:
655
- return null;
1251
+ className: variant === "root" ? "flex-1 [&_[data-slot=button]]:w-full" : "[&_[data-slot=button]]:w-full",
1252
+ children: control.render ? control.render() : /* @__PURE__ */ jsx10(
1253
+ "button",
1254
+ {
1255
+ onClick: control.onClick,
1256
+ className: "w-full px-4 py-2 text-sm bg-stone-800 hover:bg-stone-700 text-white rounded-md shadow",
1257
+ children: control.label ?? key
1258
+ }
1259
+ )
1260
+ },
1261
+ `control-panel-custom-${key}`
1262
+ );
1263
+ const renderControl = (key, control, variant) => {
1264
+ if (control.type === "button") {
1265
+ return renderButtonControl(key, control, variant);
1266
+ }
1267
+ const value = values[key];
1268
+ switch (control.type) {
1269
+ case "boolean":
1270
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [
1271
+ /* @__PURE__ */ jsx10(Label, { htmlFor: key, className: "cursor-pointer", children: labelize(key) }),
1272
+ /* @__PURE__ */ jsx10(
1273
+ Switch,
1274
+ {
1275
+ id: key,
1276
+ checked: value,
1277
+ onCheckedChange: (v) => setValue(key, v),
1278
+ className: "cursor-pointer scale-90"
656
1279
  }
657
- }),
658
- (buttonControls.length > 0 || jsx16) && /* @__PURE__ */ jsxs4(
659
- "div",
1280
+ )
1281
+ ] }, key);
1282
+ case "number":
1283
+ return /* @__PURE__ */ jsxs5("div", { className: "space-y-3 w-full", children: [
1284
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [
1285
+ /* @__PURE__ */ jsx10(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
1286
+ /* @__PURE__ */ jsx10(
1287
+ Input,
1288
+ {
1289
+ type: "number",
1290
+ value,
1291
+ min: control.min ?? 0,
1292
+ max: control.max ?? 100,
1293
+ step: control.step ?? 1,
1294
+ onChange: (e) => {
1295
+ const v = parseFloat(e.target.value);
1296
+ if (Number.isNaN(v)) return;
1297
+ setValue(key, v);
1298
+ },
1299
+ className: "w-20 text-center cursor-text"
1300
+ }
1301
+ )
1302
+ ] }),
1303
+ /* @__PURE__ */ jsx10(
1304
+ Slider,
1305
+ {
1306
+ id: key,
1307
+ min: control.min ?? 0,
1308
+ max: control.max ?? 100,
1309
+ step: control.step ?? 1,
1310
+ value: [value],
1311
+ onValueChange: ([v]) => setValue(key, v),
1312
+ className: "w-full cursor-pointer"
1313
+ }
1314
+ )
1315
+ ] }, key);
1316
+ case "string":
1317
+ return /* @__PURE__ */ jsxs5("div", { className: "space-y-2 w-full", children: [
1318
+ /* @__PURE__ */ jsx10(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
1319
+ /* @__PURE__ */ jsx10(
1320
+ Input,
660
1321
  {
661
- className: `${normalControls.length > 0 ? "border-t" : ""} border-stone-700`,
1322
+ id: key,
1323
+ value,
1324
+ placeholder: key,
1325
+ onChange: (e) => setValue(key, e.target.value),
1326
+ className: "bg-stone-900"
1327
+ }
1328
+ )
1329
+ ] }, key);
1330
+ case "color":
1331
+ return /* @__PURE__ */ jsxs5("div", { className: "space-y-2 w-full", children: [
1332
+ /* @__PURE__ */ jsx10(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
1333
+ /* @__PURE__ */ jsx10(
1334
+ "input",
1335
+ {
1336
+ type: "color",
1337
+ id: key,
1338
+ value,
1339
+ onChange: (e) => setValue(key, e.target.value),
1340
+ className: "w-full h-10 rounded border border-stone-600 bg-transparent"
1341
+ }
1342
+ )
1343
+ ] }, key);
1344
+ case "select":
1345
+ return /* @__PURE__ */ jsx10("div", { className: "space-y-2", children: /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3", children: [
1346
+ /* @__PURE__ */ jsx10(Label, { className: "min-w-fit", htmlFor: key, children: labelize(key) }),
1347
+ /* @__PURE__ */ jsxs5(Select, { value, onValueChange: (val) => setValue(key, val), children: [
1348
+ /* @__PURE__ */ jsx10(SelectTrigger, { className: "flex-1 cursor-pointer", children: /* @__PURE__ */ jsx10(SelectValue, { placeholder: "Select option" }) }),
1349
+ /* @__PURE__ */ jsx10(SelectContent, { className: "cursor-pointer z-[9999]", children: Object.entries(control.options).map(([label]) => /* @__PURE__ */ jsx10(
1350
+ SelectItem,
1351
+ {
1352
+ value: label,
1353
+ className: "cursor-pointer",
1354
+ children: label
1355
+ },
1356
+ label
1357
+ )) })
1358
+ ] })
1359
+ ] }) }, key);
1360
+ default:
1361
+ return null;
1362
+ }
1363
+ };
1364
+ const renderFolder = (folder, entries, extras = []) => {
1365
+ const isOpen = folderStates[folder] ?? true;
1366
+ return /* @__PURE__ */ jsxs5(
1367
+ "div",
1368
+ {
1369
+ className: "border border-stone-700/60 rounded-lg bg-stone-900/70",
1370
+ children: [
1371
+ /* @__PURE__ */ jsxs5(
1372
+ "button",
1373
+ {
1374
+ type: "button",
1375
+ onClick: () => setFolderStates((prev) => ({
1376
+ ...prev,
1377
+ [folder]: !isOpen
1378
+ })),
1379
+ className: "w-full flex items-center justify-between px-4 py-3 text-left font-semibold text-stone-200 tracking-wide",
662
1380
  children: [
663
- jsx16 && config?.showCopyButton !== false && /* @__PURE__ */ jsx9("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx9(
664
- "button",
1381
+ /* @__PURE__ */ jsx10("span", { children: folder }),
1382
+ /* @__PURE__ */ jsx10(
1383
+ ChevronDown2,
665
1384
  {
666
- onClick: () => {
667
- navigator.clipboard.writeText(jsx16);
668
- setCopied(true);
669
- setTimeout(() => setCopied(false), 5e3);
670
- },
671
- className: "w-full px-4 py-2 text-sm bg-stone-700 hover:bg-stone-600 text-white rounded flex items-center justify-center gap-2",
672
- children: copied ? /* @__PURE__ */ jsxs4(Fragment, { children: [
673
- /* @__PURE__ */ jsx9(Check2, { className: "w-4 h-4" }),
674
- "Copied"
675
- ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
676
- /* @__PURE__ */ jsx9(Copy, { className: "w-4 h-4" }),
677
- "Copy to Clipboard"
678
- ] })
1385
+ className: `w-4 h-4 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`
679
1386
  }
680
- ) }, "control-panel-jsx"),
681
- buttonControls.length > 0 && /* @__PURE__ */ jsx9("div", { className: "flex flex-wrap gap-2 pt-4", children: buttonControls.map(
682
- ([key, control]) => control.type === "button" ? /* @__PURE__ */ jsx9(
683
- "div",
684
- {
685
- className: "flex-1",
686
- children: control.render ? control.render() : /* @__PURE__ */ jsx9(
687
- "button",
688
- {
689
- onClick: control.onClick,
690
- className: "w-full px-4 py-2 text-sm bg-stone-700 hover:bg-stone-600 text-white rounded",
691
- children: control.label ?? key
692
- }
693
- )
694
- },
695
- `control-panel-custom-${key}`
696
- ) : null
697
- ) })
1387
+ )
698
1388
  ]
699
1389
  }
700
- )
1390
+ ),
1391
+ isOpen && /* @__PURE__ */ jsxs5("div", { className: "px-4 pb-4 pt-0 space-y-5", children: [
1392
+ entries.map(
1393
+ ([key, control]) => renderControl(key, control, "folder")
1394
+ ),
1395
+ extras.map((extra) => extra)
1396
+ ] })
1397
+ ]
1398
+ },
1399
+ folder
1400
+ );
1401
+ };
1402
+ const topFolderSections = hasAnyFolders ? folderGroups.filter(({ placement }) => placement === "top").map(
1403
+ ({ folder, entries, extras }) => renderFolder(folder, entries, extras)
1404
+ ) : null;
1405
+ const bottomFolderSections = hasAnyFolders ? folderGroups.filter(({ placement }) => placement === "bottom").map(
1406
+ ({ folder, entries, extras }) => renderFolder(folder, entries, extras)
1407
+ ) : null;
1408
+ const panelStyle = {
1409
+ width: "100%",
1410
+ height: "auto",
1411
+ flex: "0 0 auto"
1412
+ };
1413
+ if (isHydrated) {
1414
+ if (isDesktop) {
1415
+ Object.assign(panelStyle, {
1416
+ position: "absolute",
1417
+ left: 0,
1418
+ top: 0,
1419
+ bottom: 0,
1420
+ width: `${leftPanelWidth}%`,
1421
+ overflowY: "auto"
1422
+ });
1423
+ } else {
1424
+ Object.assign(panelStyle, {
1425
+ marginTop: `calc(-1 * (${MOBILE_CONTROL_PANEL_PEEK}px + env(safe-area-inset-bottom, 0px)))`,
1426
+ paddingBottom: `calc(${MOBILE_CONTROL_PANEL_PEEK}px + env(safe-area-inset-bottom, 0px))`
1427
+ });
1428
+ }
1429
+ }
1430
+ return /* @__PURE__ */ jsx10(
1431
+ "div",
1432
+ {
1433
+ className: `order-2 md:order-1 w-full md:h-auto p-2 md:p-4 bg-stone-900 font-mono text-stone-300 transition-opacity duration-300 z-max ${!isHydrated ? "opacity-0" : "opacity-100"}`,
1434
+ onPointerDown: (e) => e.stopPropagation(),
1435
+ onTouchStart: (e) => e.stopPropagation(),
1436
+ style: panelStyle,
1437
+ children: /* @__PURE__ */ jsxs5("div", { className: "dark mb-10 space-y-6 p-4 md:p-6 bg-stone-900/95 backdrop-blur-md border-2 border-stone-700 rounded-xl shadow-lg", children: [
1438
+ /* @__PURE__ */ jsx10("div", { className: "space-y-1", children: /* @__PURE__ */ jsx10("h1", { className: "text-lg text-stone-100 font-semibold", children: config?.mainLabel ?? "Controls" }) }),
1439
+ /* @__PURE__ */ jsxs5("div", { className: "space-y-6", children: [
1440
+ topFolderSections,
1441
+ hasRootButtonControls && /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap gap-2", children: rootButtonControls.map(
1442
+ ([key, control]) => renderButtonControl(key, control, "root")
1443
+ ) }),
1444
+ advancedPaletteControlNode,
1445
+ rootNormalControls.map(
1446
+ ([key, control]) => renderControl(key, control, "root")
1447
+ ),
1448
+ bottomFolderSections,
1449
+ shouldShowCopyButton && /* @__PURE__ */ jsx10("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx10(
1450
+ "button",
1451
+ {
1452
+ onClick: () => {
1453
+ if (!copyText) return;
1454
+ navigator.clipboard.writeText(copyText);
1455
+ setCopied(true);
1456
+ setTimeout(() => setCopied(false), 5e3);
1457
+ },
1458
+ className: "w-full px-4 py-2 text-sm bg-stone-800 hover:bg-stone-700 text-white rounded-md flex items-center justify-center gap-2 shadow",
1459
+ children: copied ? /* @__PURE__ */ jsxs5(Fragment, { children: [
1460
+ /* @__PURE__ */ jsx10(Check2, { className: "w-4 h-4" }),
1461
+ "Copied"
1462
+ ] }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
1463
+ /* @__PURE__ */ jsx10(Copy, { className: "w-4 h-4" }),
1464
+ "Copy to Clipboard"
1465
+ ] })
1466
+ }
1467
+ ) }, "control-panel-jsx")
701
1468
  ] }),
702
- previewUrl && /* @__PURE__ */ jsx9(Button, { asChild: true, children: /* @__PURE__ */ jsxs4(
703
- "a",
704
- {
705
- href: previewUrl,
706
- target: "_blank",
707
- rel: "noopener noreferrer",
708
- className: "w-full px-4 py-2 text-sm text-center bg-stone-800 hover:bg-stone-700 text-white rounded",
709
- children: [
710
- /* @__PURE__ */ jsx9(SquareArrowOutUpRight, {}),
711
- " Open in a New Tab"
712
- ]
713
- }
714
- ) })
1469
+ previewUrl && /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1470
+ /* @__PURE__ */ jsx10(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ jsxs5(
1471
+ "a",
1472
+ {
1473
+ href: previewUrl,
1474
+ target: "_blank",
1475
+ rel: "noopener noreferrer",
1476
+ className: "w-full px-4 py-2 text-sm text-center bg-stone-900 hover:bg-stone-800 text-white rounded-md border border-stone-700",
1477
+ children: [
1478
+ /* @__PURE__ */ jsx10(SquareArrowOutUpRight, {}),
1479
+ " Open in a New Tab"
1480
+ ]
1481
+ }
1482
+ ) }),
1483
+ config?.showPresentationButton && presentationUrl && /* @__PURE__ */ jsxs5(
1484
+ Button,
1485
+ {
1486
+ type: "button",
1487
+ onClick: handlePresentationClick,
1488
+ variant: "secondary",
1489
+ className: "w-full bg-stone-800 text-white hover:bg-stone-700 border border-stone-700",
1490
+ children: [
1491
+ /* @__PURE__ */ jsx10(Presentation, {}),
1492
+ " Presentation Mode"
1493
+ ]
1494
+ }
1495
+ )
1496
+ ] })
715
1497
  ] })
716
1498
  }
717
1499
  );
@@ -719,12 +1501,12 @@ var ControlPanel = () => {
719
1501
  var ControlPanel_default = ControlPanel;
720
1502
 
721
1503
  // src/components/PreviewContainer/PreviewContainer.tsx
722
- import { useRef as useRef2 } from "react";
1504
+ import { useRef as useRef4 } from "react";
723
1505
 
724
1506
  // src/components/Grid/Grid.tsx
725
- import { jsx as jsx10 } from "react/jsx-runtime";
1507
+ import { jsx as jsx11 } from "react/jsx-runtime";
726
1508
  function Grid() {
727
- return /* @__PURE__ */ jsx10(
1509
+ return /* @__PURE__ */ jsx11(
728
1510
  "div",
729
1511
  {
730
1512
  className: "absolute inset-0 w-screen h-screen z-[0] blur-[1px]",
@@ -742,12 +1524,12 @@ function Grid() {
742
1524
  var Grid_default = Grid;
743
1525
 
744
1526
  // src/components/PreviewContainer/PreviewContainer.tsx
745
- import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1527
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
746
1528
  var PreviewContainer = ({ children, hideControls }) => {
747
1529
  const { config } = useControlsContext();
748
1530
  const { leftPanelWidth, isDesktop, isHydrated, containerRef } = useResizableLayout();
749
- const previewRef = useRef2(null);
750
- return /* @__PURE__ */ jsx11(
1531
+ const previewRef = useRef4(null);
1532
+ return /* @__PURE__ */ jsx12(
751
1533
  "div",
752
1534
  {
753
1535
  ref: previewRef,
@@ -756,9 +1538,9 @@ var PreviewContainer = ({ children, hideControls }) => {
756
1538
  width: `${100 - leftPanelWidth}%`,
757
1539
  marginLeft: `${leftPanelWidth}%`
758
1540
  } : {},
759
- children: /* @__PURE__ */ jsxs5("div", { className: "w-screen h-screen", children: [
760
- config?.showGrid && /* @__PURE__ */ jsx11(Grid_default, {}),
761
- /* @__PURE__ */ jsx11("div", { className: "w-screen h-screen flex items-center justify-center relative", children })
1541
+ children: /* @__PURE__ */ jsxs6("div", { className: "w-screen h-screen", children: [
1542
+ config?.showGrid && /* @__PURE__ */ jsx12(Grid_default, {}),
1543
+ /* @__PURE__ */ jsx12("div", { className: "w-screen h-screen flex items-center justify-center relative", children })
762
1544
  ] })
763
1545
  }
764
1546
  );
@@ -766,166 +1548,178 @@ var PreviewContainer = ({ children, hideControls }) => {
766
1548
  var PreviewContainer_default = PreviewContainer;
767
1549
 
768
1550
  // src/components/Playground/Playground.tsx
769
- import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
770
- var NO_CONTROLS_PARAM = "nocontrols";
1551
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
1552
+ var HiddenPreview = ({ children }) => /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: "hidden", children });
771
1553
  function Playground({ children }) {
772
1554
  const [isHydrated, setIsHydrated] = useState5(false);
773
1555
  const [copied, setCopied] = useState5(false);
774
- useEffect4(() => {
1556
+ useEffect5(() => {
775
1557
  setIsHydrated(true);
776
1558
  }, []);
777
- const hideControls = useMemo3(() => {
778
- if (typeof window === "undefined") return false;
779
- return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
1559
+ const { showControls, isPresentationMode, isControlsOnly } = useMemo4(() => {
1560
+ if (typeof window === "undefined") {
1561
+ return {
1562
+ showControls: true,
1563
+ isPresentationMode: false,
1564
+ isControlsOnly: false
1565
+ };
1566
+ }
1567
+ const params = new URLSearchParams(window.location.search);
1568
+ const presentation = params.get(PRESENTATION_PARAM) === "true";
1569
+ const controlsOnly = params.get(CONTROLS_ONLY_PARAM) === "true";
1570
+ const noControlsParam = params.get(NO_CONTROLS_PARAM) === "true";
1571
+ const showControlsValue = controlsOnly || !presentation && !noControlsParam;
1572
+ return {
1573
+ showControls: showControlsValue,
1574
+ isPresentationMode: presentation,
1575
+ isControlsOnly: controlsOnly
1576
+ };
780
1577
  }, []);
1578
+ const shouldShowShareButton = !showControls && !isPresentationMode;
1579
+ const layoutHideControls = !showControls || isControlsOnly;
781
1580
  const handleCopy = () => {
782
1581
  navigator.clipboard.writeText(window.location.href);
783
1582
  setCopied(true);
784
1583
  setTimeout(() => setCopied(false), 2e3);
785
1584
  };
786
1585
  if (!isHydrated) return null;
787
- return /* @__PURE__ */ jsx12(ResizableLayout, { hideControls, children: /* @__PURE__ */ jsxs6(ControlsProvider, { children: [
788
- hideControls && /* @__PURE__ */ jsxs6(
1586
+ return /* @__PURE__ */ jsx13(ResizableLayout, { hideControls: layoutHideControls, children: /* @__PURE__ */ jsxs7(ControlsProvider, { children: [
1587
+ shouldShowShareButton && /* @__PURE__ */ jsxs7(
789
1588
  "button",
790
1589
  {
791
1590
  onClick: handleCopy,
792
1591
  className: "absolute top-4 right-4 z-50 flex items-center gap-1 rounded bg-black/70 px-3 py-1 text-white hover:bg-black",
793
1592
  children: [
794
- copied ? /* @__PURE__ */ jsx12(Check3, { size: 16 }) : /* @__PURE__ */ jsx12(Copy2, { size: 16 }),
1593
+ copied ? /* @__PURE__ */ jsx13(Check3, { size: 16 }) : /* @__PURE__ */ jsx13(Copy2, { size: 16 }),
795
1594
  copied ? "Copied!" : "Share"
796
1595
  ]
797
1596
  }
798
1597
  ),
799
- /* @__PURE__ */ jsx12(PreviewContainer_default, { hideControls, children }),
800
- !hideControls && /* @__PURE__ */ jsx12(ControlPanel_default, {})
1598
+ isControlsOnly ? /* @__PURE__ */ jsx13(HiddenPreview, { children }) : /* @__PURE__ */ jsx13(PreviewContainer_default, { hideControls: layoutHideControls, children }),
1599
+ showControls && /* @__PURE__ */ jsx13(ControlPanel_default, {})
801
1600
  ] }) });
802
1601
  }
803
1602
 
804
- // src/components/Canvas/Canvas.tsx
805
- import React11, { useEffect as useEffect6, useRef as useRef4, useState as useState6 } from "react";
806
- import { Canvas as ThreeCanvas, useThree as useThree2 } from "@react-three/fiber";
807
- import "@react-three/fiber";
808
-
809
- // src/components/CameraLogger/CameraLogger.tsx
810
- import { useRef as useRef3, useEffect as useEffect5 } from "react";
811
- import { OrbitControls } from "@react-three/drei";
812
- import { useThree } from "@react-three/fiber";
813
- import { debounce } from "lodash";
814
- import { jsx as jsx13 } from "react/jsx-runtime";
815
- function CameraLogger() {
816
- const { camera } = useThree();
817
- const controlsRef = useRef3(null);
818
- const logRef = useRef3(null);
819
- useEffect5(() => {
820
- logRef.current = debounce(() => {
821
- console.info("Camera position:", camera.position.toArray());
822
- }, 200);
823
- }, [camera]);
824
- useEffect5(() => {
825
- const controls = controlsRef.current;
826
- const handler = logRef.current;
827
- if (!controls || !handler) return;
828
- controls.addEventListener("change", handler);
829
- return () => controls.removeEventListener("change", handler);
830
- }, []);
831
- return /* @__PURE__ */ jsx13(OrbitControls, { ref: controlsRef });
832
- }
833
-
834
- // src/components/Canvas/Canvas.tsx
835
- import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
836
- var ResponsiveCamera = ({
837
- height,
838
- width
839
- }) => {
840
- const { camera } = useThree2();
841
- useEffect6(() => {
842
- const isMobile = width < 768;
843
- const zoomFactor = isMobile ? 70 : 100;
844
- camera.position.z = height / zoomFactor;
845
- camera.updateProjectionMatrix();
846
- }, [height, camera, width]);
847
- return null;
848
- };
849
- var Canvas = ({
850
- mediaProps,
851
- children,
852
- ...otherProps
853
- }) => {
854
- const canvasRef = useRef4(null);
855
- const [parentSize, setParentSize] = useState6(null);
1603
+ // src/hooks/useAdvancedPaletteControls.ts
1604
+ import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
1605
+ var cloneForCallbacks = (palette) => clonePalette(palette);
1606
+ var useAdvancedPaletteControls = (options = {}) => {
1607
+ const resolvedDefaultPalette = useMemo5(
1608
+ () => createAdvancedPalette(options.defaultPalette),
1609
+ [options.defaultPalette]
1610
+ );
1611
+ const resolvedFallbackPalette = useMemo5(
1612
+ () => options.fallbackPalette ? createAdvancedPalette(options.fallbackPalette) : resolvedDefaultPalette,
1613
+ [options.fallbackPalette, resolvedDefaultPalette]
1614
+ );
1615
+ const [palette, setPaletteState] = useState6(
1616
+ () => clonePalette(resolvedDefaultPalette)
1617
+ );
1618
+ const defaultSignatureRef = useRef5(
1619
+ createPaletteSignature(resolvedDefaultPalette)
1620
+ );
856
1621
  useEffect6(() => {
857
- let observer = null;
858
- const tryObserve = () => {
859
- const node = canvasRef.current;
860
- if (!node || !node.parentElement) {
861
- setTimeout(tryObserve, 50);
862
- return;
863
- }
864
- const parent = node.parentElement;
865
- observer = new ResizeObserver(([entry]) => {
866
- const { width, height } = entry.contentRect;
867
- setParentSize({ width, height });
1622
+ const nextSignature = createPaletteSignature(resolvedDefaultPalette);
1623
+ if (defaultSignatureRef.current === nextSignature) return;
1624
+ defaultSignatureRef.current = nextSignature;
1625
+ setPaletteState(clonePalette(resolvedDefaultPalette));
1626
+ }, [resolvedDefaultPalette]);
1627
+ const notifyChange = useCallback4(
1628
+ (nextPalette) => {
1629
+ options.onChange?.(cloneForCallbacks(nextPalette));
1630
+ },
1631
+ [options.onChange]
1632
+ );
1633
+ const setPalette = useCallback4(
1634
+ (source) => {
1635
+ const nextPalette = createAdvancedPalette(
1636
+ source ?? resolvedDefaultPalette
1637
+ );
1638
+ setPaletteState(clonePalette(nextPalette));
1639
+ notifyChange(nextPalette);
1640
+ },
1641
+ [notifyChange, resolvedDefaultPalette]
1642
+ );
1643
+ const updatePalette = useCallback4(
1644
+ (updater) => {
1645
+ setPaletteState((current) => {
1646
+ const nextSource = updater(clonePalette(current));
1647
+ const nextPalette = createAdvancedPalette(
1648
+ nextSource ?? current ?? resolvedDefaultPalette
1649
+ );
1650
+ notifyChange(nextPalette);
1651
+ return clonePalette(nextPalette);
868
1652
  });
869
- observer.observe(parent);
870
- };
871
- tryObserve();
872
- return () => {
873
- if (observer) observer.disconnect();
874
- };
875
- }, []);
876
- const mergedMediaProps = {
877
- ...mediaProps || {},
878
- size: mediaProps?.size || { width: 400, height: 400 }
879
- };
880
- return /* @__PURE__ */ jsx14(
881
- "div",
882
- {
883
- ref: canvasRef,
884
- className: "w-full h-full pointer-events-none relative touch-none",
885
- children: /* @__PURE__ */ jsxs7(
886
- ThreeCanvas,
887
- {
888
- resize: { polyfill: ResizeObserver },
889
- style: { width: parentSize?.width, height: parentSize?.height },
890
- gl: { preserveDrawingBuffer: true },
891
- ...otherProps,
892
- children: [
893
- parentSize?.height && parentSize?.width && /* @__PURE__ */ jsx14(
894
- ResponsiveCamera,
895
- {
896
- height: parentSize.height,
897
- width: parentSize.width
898
- }
899
- ),
900
- mediaProps?.debugOrbit && /* @__PURE__ */ jsx14(CameraLogger, {}),
901
- /* @__PURE__ */ jsx14("ambientLight", { intensity: 1 }),
902
- /* @__PURE__ */ jsx14("pointLight", { position: [10, 10, 10] }),
903
- React11.cloneElement(children, mergedMediaProps)
904
- ]
905
- }
906
- )
907
- }
1653
+ },
1654
+ [notifyChange, resolvedDefaultPalette]
908
1655
  );
1656
+ const resetPalette = useCallback4(() => {
1657
+ setPaletteState(clonePalette(resolvedDefaultPalette));
1658
+ notifyChange(resolvedDefaultPalette);
1659
+ }, [notifyChange, resolvedDefaultPalette]);
1660
+ const handleControlPaletteChange = useCallback4(
1661
+ (nextPalette) => {
1662
+ setPaletteState(clonePalette(nextPalette));
1663
+ notifyChange(nextPalette);
1664
+ },
1665
+ [notifyChange]
1666
+ );
1667
+ const controlConfig = useMemo5(
1668
+ () => ({
1669
+ ...options.control ?? {},
1670
+ defaultPalette: resolvedDefaultPalette,
1671
+ onPaletteChange: handleControlPaletteChange
1672
+ }),
1673
+ [handleControlPaletteChange, options.control, resolvedDefaultPalette]
1674
+ );
1675
+ const hexColors = useMemo5(
1676
+ () => advancedPaletteToHexColors(palette, {
1677
+ sectionOrder: options.sectionOrder,
1678
+ fallbackPalette: resolvedFallbackPalette,
1679
+ defaultColor: options.defaultColor
1680
+ }),
1681
+ [
1682
+ options.defaultColor,
1683
+ options.sectionOrder,
1684
+ palette,
1685
+ resolvedFallbackPalette
1686
+ ]
1687
+ );
1688
+ const paletteSignature = useMemo5(
1689
+ () => createPaletteSignature(palette),
1690
+ [palette]
1691
+ );
1692
+ const paletteGradient = useMemo5(
1693
+ () => computePaletteGradient(palette, options.gradientSteps),
1694
+ [options.gradientSteps, palette]
1695
+ );
1696
+ return {
1697
+ palette,
1698
+ hexColors,
1699
+ controlConfig,
1700
+ paletteGradient,
1701
+ setPalette,
1702
+ updatePalette,
1703
+ resetPalette,
1704
+ paletteSignature
1705
+ };
909
1706
  };
910
- var Canvas_default = Canvas;
911
-
912
- // src/components/PlaygroundCanvas/PlaygroundCanvas.tsx
913
- import { jsx as jsx15 } from "react/jsx-runtime";
914
- var PlaygroundCanvas = ({
915
- children,
916
- mediaProps,
917
- ...otherProps
918
- }) => {
919
- return /* @__PURE__ */ jsx15(Playground, { children: /* @__PURE__ */ jsx15(Canvas_default, { mediaProps, ...otherProps, children }) });
920
- };
921
- var PlaygroundCanvas_default = PlaygroundCanvas;
1707
+ var useDefaultAdvancedPaletteControls = () => useAdvancedPaletteControls({ defaultPalette: DEFAULT_ADVANCED_PALETTE });
922
1708
  export {
923
1709
  Button,
924
- CameraLogger,
925
- Canvas_default as Canvas,
926
1710
  ControlsProvider,
1711
+ DEFAULT_ADVANCED_PALETTE,
1712
+ DEFAULT_HEX_PALETTE,
927
1713
  Playground,
928
- PlaygroundCanvas_default as PlaygroundCanvas,
1714
+ advancedPaletteToHexColors,
1715
+ clonePalette,
1716
+ computePaletteGradient,
1717
+ createAdvancedPalette,
1718
+ createPaletteSignature,
1719
+ hexToPaletteValue,
1720
+ paletteValueToHex,
1721
+ useAdvancedPaletteControls,
929
1722
  useControls,
1723
+ useDefaultAdvancedPaletteControls,
930
1724
  useUrlSyncedControls
931
1725
  };