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