@toriistudio/v0-playground 0.5.2 → 0.5.5

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,252 @@ var getUrlParams = () => {
160
168
  return entries;
161
169
  };
162
170
 
171
+ // src/lib/advancedPalette.ts
172
+ var CHANNEL_KEYS = ["r", "g", "b"];
173
+ var DEFAULT_CHANNEL_LABELS = {
174
+ r: "Red",
175
+ g: "Green",
176
+ b: "Blue"
177
+ };
178
+ var DEFAULT_SECTIONS = [
179
+ { key: "A", label: "Vector A", helper: "Base offset" },
180
+ { key: "B", label: "Vector B", helper: "Amplitude" },
181
+ { key: "C", label: "Vector C", helper: "Frequency" },
182
+ { key: "D", label: "Vector D", helper: "Phase shift" }
183
+ ];
184
+ var DEFAULT_RANGES = {
185
+ A: { min: 0, max: 1, step: 0.01 },
186
+ B: { min: -1, max: 1, step: 0.01 },
187
+ C: { min: 0, max: 2, step: 0.01 },
188
+ D: { min: 0, max: 1, step: 0.01 }
189
+ };
190
+ var DEFAULT_HIDDEN_KEY_PREFIX = "palette";
191
+ var DEFAULT_GRADIENT_STEPS = 12;
192
+ var DEFAULT_HEX_PALETTE = {
193
+ A: { r: 0.5, g: 0.5, b: 0.5 },
194
+ B: { r: 0.5, g: 0.5, b: 0.5 },
195
+ C: { r: 1, g: 1, b: 1 },
196
+ D: { r: 0, g: 0.1, b: 0.2 }
197
+ };
198
+ var createPaletteControlKey = (prefix, section, channel) => `${prefix}${section}${channel}`;
199
+ var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
200
+ var clamp01 = (value) => clamp(value, 0, 1);
201
+ var toNumberOr = (value, fallback) => {
202
+ if (typeof value === "number" && Number.isFinite(value)) return value;
203
+ if (typeof value === "string") {
204
+ const parsed = parseFloat(value);
205
+ if (Number.isFinite(parsed)) return parsed;
206
+ }
207
+ return fallback;
208
+ };
209
+ var paletteColorAt = (palette, t) => {
210
+ const twoPi = Math.PI * 2;
211
+ const computeChannel = (a, b, c, d) => {
212
+ const value = a + b * Math.cos(twoPi * (c * t + d));
213
+ return clamp(value, 0, 1);
214
+ };
215
+ return {
216
+ r: computeChannel(
217
+ palette.A?.r ?? 0,
218
+ palette.B?.r ?? 0,
219
+ palette.C?.r ?? 0,
220
+ palette.D?.r ?? 0
221
+ ),
222
+ g: computeChannel(
223
+ palette.A?.g ?? 0,
224
+ palette.B?.g ?? 0,
225
+ palette.C?.g ?? 0,
226
+ palette.D?.g ?? 0
227
+ ),
228
+ b: computeChannel(
229
+ palette.A?.b ?? 0,
230
+ palette.B?.b ?? 0,
231
+ palette.C?.b ?? 0,
232
+ palette.D?.b ?? 0
233
+ )
234
+ };
235
+ };
236
+ var toRgba = ({ r, g, b }, alpha = 0.5) => `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(
237
+ b * 255
238
+ )}, ${alpha})`;
239
+ var computePaletteGradient = (palette, steps = DEFAULT_GRADIENT_STEPS) => {
240
+ const stops = Array.from({ length: steps }, (_, index) => {
241
+ const t = index / (steps - 1);
242
+ const color = paletteColorAt(palette, t);
243
+ const stop = (t * 100).toFixed(1);
244
+ return `${toRgba(color)} ${stop}%`;
245
+ });
246
+ return `linear-gradient(to right, ${stops.join(", ")})`;
247
+ };
248
+ var createPaletteSignature = (palette) => Object.entries(palette).sort(([aKey], [bKey]) => aKey.localeCompare(bKey)).flatMap(
249
+ ([, channels]) => CHANNEL_KEYS.map((channel) => (channels?.[channel] ?? 0).toFixed(3))
250
+ ).join("-");
251
+ var isAdvancedPaletteValue = (value) => Boolean(
252
+ value && typeof value === "object" && CHANNEL_KEYS.every((channel) => {
253
+ const channelValue = value[channel];
254
+ return typeof channelValue === "number" && Number.isFinite(channelValue);
255
+ })
256
+ );
257
+ var isAdvancedPalette = (value) => Boolean(
258
+ value && typeof value === "object" && Object.values(value).every(
259
+ (entry) => isAdvancedPaletteValue(entry) || typeof entry === "object"
260
+ )
261
+ );
262
+ var normalizePaletteValue = (source) => {
263
+ if (typeof source === "string") {
264
+ return hexToPaletteValue(source);
265
+ }
266
+ const channelSource = source ?? {};
267
+ const toChannel = (channel) => clamp01(
268
+ toNumberOr(
269
+ channelSource[channel],
270
+ 0
271
+ )
272
+ );
273
+ return {
274
+ r: toChannel("r"),
275
+ g: toChannel("g"),
276
+ b: toChannel("b")
277
+ };
278
+ };
279
+ var createPaletteFromRecord = (record) => Object.entries(record).reduce((acc, [key, value]) => {
280
+ acc[key] = normalizePaletteValue(value);
281
+ return acc;
282
+ }, {});
283
+ var clonePalette = (palette) => Object.fromEntries(
284
+ Object.entries(palette).map(([sectionKey, channels]) => [
285
+ sectionKey,
286
+ { ...channels }
287
+ ])
288
+ );
289
+ var hexComponentToNormalized = (component) => clamp01(parseInt(component, 16) / 255 || 0);
290
+ var normalizedChannelToHex = (value) => Math.round(clamp01(value) * 255).toString(16).padStart(2, "0");
291
+ var sanitizeHex = (hex) => {
292
+ let sanitized = hex.trim();
293
+ if (sanitized.startsWith("#")) {
294
+ sanitized = sanitized.slice(1);
295
+ }
296
+ if (sanitized.length === 3) {
297
+ sanitized = sanitized.split("").map((char) => char + char).join("");
298
+ }
299
+ return sanitized.length === 6 ? sanitized : null;
300
+ };
301
+ var hexToPaletteValue = (hex) => {
302
+ const sanitized = sanitizeHex(hex);
303
+ if (!sanitized) {
304
+ return { r: 0, g: 0, b: 0 };
305
+ }
306
+ return {
307
+ r: hexComponentToNormalized(sanitized.slice(0, 2)),
308
+ g: hexComponentToNormalized(sanitized.slice(2, 4)),
309
+ b: hexComponentToNormalized(sanitized.slice(4, 6))
310
+ };
311
+ };
312
+ var paletteValueToHex = (value) => `#${normalizedChannelToHex(value.r)}${normalizedChannelToHex(
313
+ value.g
314
+ )}${normalizedChannelToHex(value.b)}`;
315
+ var createAdvancedPalette = (source = DEFAULT_HEX_PALETTE, options) => {
316
+ if (Array.isArray(source)) {
317
+ const order = options?.sectionOrder ?? DEFAULT_SECTIONS.map((section) => section.key);
318
+ const record = {};
319
+ source.forEach((value, index) => {
320
+ const preferredKey = order[index];
321
+ const fallbackKey = `Color${index + 1}`;
322
+ const key = preferredKey && !(preferredKey in record) ? preferredKey : fallbackKey;
323
+ record[key] = value;
324
+ });
325
+ return createPaletteFromRecord(record);
326
+ }
327
+ if (isAdvancedPalette(source)) {
328
+ return clonePalette(
329
+ Object.entries(source ?? {}).reduce(
330
+ (acc, [key, value]) => {
331
+ acc[key] = normalizePaletteValue(value);
332
+ return acc;
333
+ },
334
+ {}
335
+ )
336
+ );
337
+ }
338
+ if (source && typeof source === "object") {
339
+ return createPaletteFromRecord(source);
340
+ }
341
+ return createPaletteFromRecord(DEFAULT_HEX_PALETTE);
342
+ };
343
+ var DEFAULT_ADVANCED_PALETTE = createAdvancedPalette(
344
+ DEFAULT_HEX_PALETTE
345
+ );
346
+ var advancedPaletteToHexColors = (palette, options) => {
347
+ const fallbackPalette = options?.fallbackPalette ?? DEFAULT_ADVANCED_PALETTE;
348
+ const orderedKeys = options?.sectionOrder ?? (Object.keys(palette).length > 0 ? Object.keys(palette) : Object.keys(fallbackPalette));
349
+ const uniqueKeys = Array.from(new Set(orderedKeys));
350
+ if (uniqueKeys.length === 0) {
351
+ uniqueKeys.push(...Object.keys(DEFAULT_ADVANCED_PALETTE));
352
+ }
353
+ const defaultColor = options?.defaultColor ?? "#000000";
354
+ return uniqueKeys.map((key) => {
355
+ const paletteValue = palette[key] ?? fallbackPalette[key];
356
+ if (!paletteValue) return defaultColor;
357
+ return paletteValueToHex(paletteValue);
358
+ });
359
+ };
360
+ var createDefaultSectionsFromPalette = (palette) => {
361
+ const sectionKeys = Object.keys(palette);
362
+ if (sectionKeys.length === 0) return DEFAULT_SECTIONS;
363
+ return sectionKeys.map((key, index) => ({
364
+ key,
365
+ label: `Vector ${key}`,
366
+ helper: DEFAULT_SECTIONS[index]?.helper ?? "Palette parameter"
367
+ }));
368
+ };
369
+ var resolveAdvancedPaletteConfig = (config) => {
370
+ const defaultPalette = createAdvancedPalette(config.defaultPalette);
371
+ const sections = config.sections ?? createDefaultSectionsFromPalette(defaultPalette);
372
+ const ranges = {};
373
+ sections.forEach((section) => {
374
+ ranges[section.key] = config.ranges?.[section.key] ?? DEFAULT_RANGES[section.key] ?? {
375
+ min: 0,
376
+ max: 1,
377
+ step: 0.01
378
+ };
379
+ });
380
+ const channelLabels = {
381
+ ...DEFAULT_CHANNEL_LABELS,
382
+ ...config.channelLabels ?? {}
383
+ };
384
+ return {
385
+ ...config,
386
+ defaultPalette,
387
+ sections,
388
+ ranges,
389
+ channelLabels,
390
+ hiddenKeyPrefix: config.hiddenKeyPrefix ?? DEFAULT_HIDDEN_KEY_PREFIX,
391
+ controlKey: config.controlKey ?? "advancedPaletteControl",
392
+ gradientSteps: config.gradientSteps ?? DEFAULT_GRADIENT_STEPS
393
+ };
394
+ };
395
+ var createAdvancedPaletteSchemaEntries = (schema, resolvedConfig) => {
396
+ const { sections, hiddenKeyPrefix, defaultPalette } = resolvedConfig;
397
+ const updatedSchema = { ...schema };
398
+ sections.forEach((section) => {
399
+ CHANNEL_KEYS.forEach((channel) => {
400
+ const key = createPaletteControlKey(
401
+ hiddenKeyPrefix,
402
+ section.key,
403
+ channel
404
+ );
405
+ if (!(key in updatedSchema)) {
406
+ updatedSchema[key] = {
407
+ type: "number",
408
+ value: defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0,
409
+ hidden: true
410
+ };
411
+ }
412
+ });
413
+ });
414
+ return updatedSchema;
415
+ };
416
+
163
417
  // src/context/ControlsContext.tsx
164
418
  var import_jsx_runtime2 = require("react/jsx-runtime");
165
419
  var ControlsContext = (0, import_react2.createContext)(null);
@@ -183,9 +437,16 @@ var ControlsProvider = ({ children }) => {
183
437
  setComponentName(opts.componentName);
184
438
  }
185
439
  if (opts?.config) {
440
+ const { addAdvancedPaletteControl, ...otherConfig } = opts.config;
186
441
  setConfig((prev) => ({
187
442
  ...prev,
188
- ...opts.config
443
+ ...otherConfig,
444
+ ...Object.prototype.hasOwnProperty.call(
445
+ opts.config,
446
+ "addAdvancedPaletteControl"
447
+ ) ? {
448
+ addAdvancedPaletteControl: addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(addAdvancedPaletteControl) : void 0
449
+ } : {}
189
450
  }));
190
451
  }
191
452
  setSchema((prevSchema) => ({ ...prevSchema, ...newSchema }));
@@ -218,28 +479,41 @@ var ControlsProvider = ({ children }) => {
218
479
  var useControls = (schema, options) => {
219
480
  const ctx = (0, import_react2.useContext)(ControlsContext);
220
481
  if (!ctx) throw new Error("useControls must be used within ControlsProvider");
482
+ const lastAdvancedPaletteSignature = (0, import_react2.useRef)(null);
221
483
  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
484
+ const resolvedAdvancedConfig = options?.config?.addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(options.config.addAdvancedPaletteControl) : void 0;
485
+ const schemaWithAdvanced = (0, import_react2.useMemo)(() => {
486
+ const baseSchema = { ...schema };
487
+ if (!resolvedAdvancedConfig) return baseSchema;
488
+ return createAdvancedPaletteSchemaEntries(
489
+ baseSchema,
490
+ resolvedAdvancedConfig
491
+ );
492
+ }, [schema, resolvedAdvancedConfig]);
493
+ const urlParamsKey = (0, import_react2.useMemo)(() => JSON.stringify(urlParams), [urlParams]);
494
+ const mergedSchema = (0, import_react2.useMemo)(() => {
495
+ return Object.fromEntries(
496
+ Object.entries(schemaWithAdvanced).map(([key, control]) => {
497
+ const urlValue = urlParams[key];
498
+ if (!urlValue || !("value" in control)) return [key, control];
499
+ const defaultValue = control.value;
500
+ let parsed = urlValue;
501
+ if (typeof defaultValue === "number") {
502
+ parsed = parseFloat(urlValue);
503
+ if (isNaN(parsed)) parsed = defaultValue;
504
+ } else if (typeof defaultValue === "boolean") {
505
+ parsed = urlValue === "true";
239
506
  }
240
- ];
241
- })
242
- );
507
+ return [
508
+ key,
509
+ {
510
+ ...control,
511
+ value: parsed
512
+ }
513
+ ];
514
+ })
515
+ );
516
+ }, [schemaWithAdvanced, urlParams, urlParamsKey]);
243
517
  (0, import_react2.useEffect)(() => {
244
518
  ctx.registerSchema(mergedSchema, options);
245
519
  }, [JSON.stringify(mergedSchema), JSON.stringify(options)]);
@@ -250,8 +524,35 @@ var useControls = (schema, options) => {
250
524
  }
251
525
  }
252
526
  }, [JSON.stringify(mergedSchema), JSON.stringify(ctx.values)]);
527
+ (0, import_react2.useEffect)(() => {
528
+ if (!resolvedAdvancedConfig?.onPaletteChange) return;
529
+ const palette = resolvedAdvancedConfig.sections.reduce(
530
+ (acc, section) => {
531
+ const channels = CHANNEL_KEYS.reduce(
532
+ (channelAcc, channel) => {
533
+ const key = createPaletteControlKey(
534
+ resolvedAdvancedConfig.hiddenKeyPrefix,
535
+ section.key,
536
+ channel
537
+ );
538
+ const fallback = resolvedAdvancedConfig.defaultPalette?.[section.key]?.[channel] ?? 0;
539
+ channelAcc[channel] = toNumberOr(ctx.values[key], fallback);
540
+ return channelAcc;
541
+ },
542
+ {}
543
+ );
544
+ acc[section.key] = channels;
545
+ return acc;
546
+ },
547
+ {}
548
+ );
549
+ const signature = createPaletteSignature(palette);
550
+ if (lastAdvancedPaletteSignature.current === signature) return;
551
+ lastAdvancedPaletteSignature.current = signature;
552
+ resolvedAdvancedConfig.onPaletteChange(clonePalette(palette));
553
+ }, [ctx.values, resolvedAdvancedConfig]);
253
554
  const typedValues = ctx.values;
254
- const jsx16 = (0, import_react2.useCallback)(() => {
555
+ const jsx14 = (0, import_react2.useCallback)(() => {
255
556
  if (!options?.componentName) return "";
256
557
  const props = Object.entries(typedValues).map(([key, val]) => {
257
558
  if (typeof val === "string") return `${key}="${val}"`;
@@ -265,13 +566,13 @@ var useControls = (schema, options) => {
265
566
  controls: ctx.values,
266
567
  schema: ctx.schema,
267
568
  setValue: ctx.setValue,
268
- jsx: jsx16
569
+ jsx: jsx14
269
570
  };
270
571
  };
271
572
  var useUrlSyncedControls = useControls;
272
573
 
273
574
  // src/components/ControlPanel/ControlPanel.tsx
274
- var import_react4 = require("react");
575
+ var import_react5 = require("react");
275
576
  var import_lucide_react3 = require("lucide-react");
276
577
 
277
578
  // src/hooks/usePreviewUrl.ts
@@ -544,20 +845,166 @@ var Button = React8.forwardRef(
544
845
  );
545
846
  Button.displayName = "Button";
546
847
 
547
- // src/components/ControlPanel/ControlPanel.tsx
848
+ // src/constants/layout.ts
849
+ var MOBILE_CONTROL_PANEL_PEEK = 112;
850
+
851
+ // src/components/AdvancedPaletteControl/AdvancedPaletteControl.tsx
852
+ var import_react4 = require("react");
548
853
  var import_jsx_runtime9 = require("react/jsx-runtime");
854
+ var AdvancedPaletteControl = ({
855
+ config
856
+ }) => {
857
+ const { values, setValue } = useControlsContext();
858
+ const palette = (0, import_react4.useMemo)(() => {
859
+ const result = {};
860
+ config.sections.forEach((section) => {
861
+ result[section.key] = CHANNEL_KEYS.reduce((acc, channel) => {
862
+ const key = createPaletteControlKey(
863
+ config.hiddenKeyPrefix,
864
+ section.key,
865
+ channel
866
+ );
867
+ const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0;
868
+ acc[channel] = toNumberOr(values?.[key], defaultValue);
869
+ return acc;
870
+ }, {});
871
+ });
872
+ return result;
873
+ }, [config.defaultPalette, config.hiddenKeyPrefix, config.sections, values]);
874
+ const paletteGradient = (0, import_react4.useMemo)(
875
+ () => computePaletteGradient(palette, config.gradientSteps),
876
+ [palette, config.gradientSteps]
877
+ );
878
+ const paletteSignature = (0, import_react4.useMemo)(
879
+ () => createPaletteSignature(palette),
880
+ [palette]
881
+ );
882
+ const lastSignatureRef = (0, import_react4.useRef)(null);
883
+ (0, import_react4.useEffect)(() => {
884
+ if (!config.onPaletteChange) return;
885
+ if (lastSignatureRef.current === paletteSignature) return;
886
+ lastSignatureRef.current = paletteSignature;
887
+ config.onPaletteChange(palette);
888
+ }, [config, palette, paletteSignature]);
889
+ const updatePaletteValue = (0, import_react4.useCallback)(
890
+ (sectionKey, channel, nextValue) => {
891
+ const range = config.ranges[sectionKey] ?? DEFAULT_RANGES[sectionKey] ?? {
892
+ min: 0,
893
+ max: 1,
894
+ step: 0.01
895
+ };
896
+ const clamped = Math.min(Math.max(nextValue, range.min), range.max);
897
+ config.onInteraction?.();
898
+ const controlKey = createPaletteControlKey(
899
+ config.hiddenKeyPrefix,
900
+ sectionKey,
901
+ channel
902
+ );
903
+ setValue(controlKey, clamped);
904
+ },
905
+ [config, setValue]
906
+ );
907
+ const handleResetPalette = (0, import_react4.useCallback)(() => {
908
+ config.onInteraction?.();
909
+ config.sections.forEach((section) => {
910
+ CHANNEL_KEYS.forEach((channel) => {
911
+ const controlKey = createPaletteControlKey(
912
+ config.hiddenKeyPrefix,
913
+ section.key,
914
+ channel
915
+ );
916
+ const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0;
917
+ setValue(controlKey, defaultValue);
918
+ });
919
+ });
920
+ }, [config, setValue]);
921
+ 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: [
922
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-between", children: [
923
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-xs font-semibold uppercase tracking-wide text-stone-200", children: "Palette" }),
924
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
925
+ "button",
926
+ {
927
+ type: "button",
928
+ onClick: handleResetPalette,
929
+ 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",
930
+ children: "Reset Palette"
931
+ }
932
+ )
933
+ ] }),
934
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
935
+ "div",
936
+ {
937
+ className: "h-4 w-full rounded border border-stone-700",
938
+ style: { background: paletteGradient }
939
+ }
940
+ ),
941
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex flex-col gap-4", children: config.sections.map((section) => {
942
+ const range = config.ranges[section.key];
943
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-3", children: [
944
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-between text-[11px] uppercase tracking-widest text-stone-300", children: [
945
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: section.label }),
946
+ section.helper && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-stone-500", children: section.helper })
947
+ ] }),
948
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "grid grid-cols-3 gap-3", children: CHANNEL_KEYS.map((channel) => {
949
+ const value = palette[section.key][channel];
950
+ const channelLabel = config.channelLabels?.[channel] ?? DEFAULT_CHANNEL_LABELS[channel];
951
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-2", children: [
952
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-between text-[10px] uppercase tracking-widest text-stone-400", children: [
953
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: channelLabel }),
954
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: value.toFixed(2) })
955
+ ] }),
956
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
957
+ "input",
958
+ {
959
+ type: "range",
960
+ min: range.min,
961
+ max: range.max,
962
+ step: range.step,
963
+ value,
964
+ onPointerDown: config.onInteraction,
965
+ onChange: (event) => updatePaletteValue(
966
+ section.key,
967
+ channel,
968
+ parseFloat(event.target.value)
969
+ ),
970
+ className: "w-full cursor-pointer accent-stone-300"
971
+ }
972
+ ),
973
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
974
+ "input",
975
+ {
976
+ type: "number",
977
+ min: range.min,
978
+ max: range.max,
979
+ step: range.step,
980
+ value: value.toFixed(3),
981
+ onPointerDown: config.onInteraction,
982
+ onFocus: config.onInteraction,
983
+ onChange: (event) => {
984
+ const parsed = parseFloat(event.target.value);
985
+ if (Number.isNaN(parsed)) return;
986
+ updatePaletteValue(section.key, channel, parsed);
987
+ },
988
+ 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"
989
+ }
990
+ )
991
+ ] }, channel);
992
+ }) })
993
+ ] }, section.key);
994
+ }) })
995
+ ] }) });
996
+ };
997
+ var AdvancedPaletteControl_default = AdvancedPaletteControl;
998
+
999
+ // src/components/ControlPanel/ControlPanel.tsx
1000
+ var import_jsx_runtime10 = require("react/jsx-runtime");
549
1001
  var ControlPanel = () => {
550
- const [copied, setCopied] = (0, import_react4.useState)(false);
1002
+ const [copied, setCopied] = (0, import_react5.useState)(false);
1003
+ const [folderStates, setFolderStates] = (0, import_react5.useState)({});
551
1004
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
552
1005
  const { schema, setValue, values, componentName, config } = useControlsContext();
553
1006
  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
559
- );
560
- const jsx16 = (0, import_react4.useMemo)(() => {
1007
+ const jsx14 = (0, import_react5.useMemo)(() => {
561
1008
  if (!componentName) return "";
562
1009
  const props = Object.entries(values).map(([key, val]) => {
563
1010
  if (typeof val === "string") return `${key}="${val}"`;
@@ -566,178 +1013,348 @@ var ControlPanel = () => {
566
1013
  }).join(" ");
567
1014
  return `<${componentName} ${props} />`;
568
1015
  }, [componentName, values]);
569
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1016
+ const visibleEntries = Object.entries(schema).filter(
1017
+ ([, control]) => !control.hidden
1018
+ );
1019
+ const rootControls = [];
1020
+ const folderOrder = [];
1021
+ const folderControls = /* @__PURE__ */ new Map();
1022
+ const folderExtras = /* @__PURE__ */ new Map();
1023
+ const folderPlacement = /* @__PURE__ */ new Map();
1024
+ const seenFolders = /* @__PURE__ */ new Set();
1025
+ const ensureFolder = (folder) => {
1026
+ if (!seenFolders.has(folder)) {
1027
+ seenFolders.add(folder);
1028
+ folderOrder.push(folder);
1029
+ }
1030
+ };
1031
+ visibleEntries.forEach((entry) => {
1032
+ const [key, control] = entry;
1033
+ const folder = control.folder?.trim();
1034
+ if (folder) {
1035
+ const placement = control.folderPlacement ?? "bottom";
1036
+ ensureFolder(folder);
1037
+ if (!folderControls.has(folder)) {
1038
+ folderControls.set(folder, []);
1039
+ }
1040
+ folderControls.get(folder).push(entry);
1041
+ const existingPlacement = folderPlacement.get(folder);
1042
+ if (!existingPlacement || placement === "top") {
1043
+ folderPlacement.set(folder, placement);
1044
+ }
1045
+ } else {
1046
+ rootControls.push(entry);
1047
+ }
1048
+ });
1049
+ const advancedConfig = config?.addAdvancedPaletteControl;
1050
+ let advancedPaletteControlNode = null;
1051
+ if (advancedConfig) {
1052
+ const advancedNode = /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1053
+ AdvancedPaletteControl_default,
1054
+ {
1055
+ config: advancedConfig
1056
+ },
1057
+ "advancedPaletteControl"
1058
+ );
1059
+ const advancedFolder = advancedConfig.folder?.trim();
1060
+ if (advancedFolder) {
1061
+ const placement = advancedConfig.folderPlacement ?? "bottom";
1062
+ ensureFolder(advancedFolder);
1063
+ if (!folderControls.has(advancedFolder)) {
1064
+ folderControls.set(advancedFolder, []);
1065
+ }
1066
+ const existingPlacement = folderPlacement.get(advancedFolder);
1067
+ if (!existingPlacement || placement === "top") {
1068
+ folderPlacement.set(advancedFolder, placement);
1069
+ }
1070
+ if (!folderExtras.has(advancedFolder)) {
1071
+ folderExtras.set(advancedFolder, []);
1072
+ }
1073
+ folderExtras.get(advancedFolder).push(advancedNode);
1074
+ } else {
1075
+ advancedPaletteControlNode = advancedNode;
1076
+ }
1077
+ }
1078
+ const rootButtonControls = rootControls.filter(
1079
+ ([, control]) => control.type === "button"
1080
+ );
1081
+ const rootNormalControls = rootControls.filter(
1082
+ ([, control]) => control.type !== "button"
1083
+ );
1084
+ const folderGroups = folderOrder.map((folder) => ({
1085
+ folder,
1086
+ entries: folderControls.get(folder) ?? [],
1087
+ extras: folderExtras.get(folder) ?? [],
1088
+ placement: folderPlacement.get(folder) ?? "bottom"
1089
+ })).filter((group) => group.entries.length > 0 || group.extras.length > 0);
1090
+ const hasRootButtonControls = rootButtonControls.length > 0;
1091
+ const hasAnyFolders = folderGroups.length > 0;
1092
+ const jsonToComponentString = (0, import_react5.useCallback)(
1093
+ ({
1094
+ componentName: componentNameOverride,
1095
+ props
1096
+ }) => {
1097
+ const resolvedComponentName = componentNameOverride ?? componentName;
1098
+ if (!resolvedComponentName) return "";
1099
+ const formatProp = (key, value) => {
1100
+ if (value === void 0) return null;
1101
+ if (value === null) return `${key}={null}`;
1102
+ if (typeof value === "string") return `${key}="${value}"`;
1103
+ if (typeof value === "number" || typeof value === "boolean") {
1104
+ return `${key}={${value}}`;
1105
+ }
1106
+ if (typeof value === "bigint") {
1107
+ return `${key}={${value.toString()}n}`;
1108
+ }
1109
+ return `${key}={${JSON.stringify(value)}}`;
1110
+ };
1111
+ const formattedProps = Object.entries(props ?? {}).map(([key, value]) => formatProp(key, value)).filter((prop) => Boolean(prop)).join(" ");
1112
+ if (!formattedProps) {
1113
+ return `<${resolvedComponentName} />`;
1114
+ }
1115
+ return `<${resolvedComponentName} ${formattedProps} />`;
1116
+ },
1117
+ [componentName]
1118
+ );
1119
+ const copyText = config?.showCopyButtonFn?.({
1120
+ componentName,
1121
+ values,
1122
+ schema,
1123
+ jsx: jsx14,
1124
+ jsonToComponentString
1125
+ }) ?? jsx14;
1126
+ const shouldShowCopyButton = config?.showCopyButton !== false && Boolean(copyText);
1127
+ const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
1128
+ const renderButtonControl = (key, control, variant) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
570
1129
  "div",
571
1130
  {
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;
1131
+ className: variant === "root" ? "flex-1 [&_[data-slot=button]]:w-full" : "[&_[data-slot=button]]:w-full",
1132
+ children: control.render ? control.render() : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1133
+ "button",
1134
+ {
1135
+ onClick: control.onClick,
1136
+ className: "w-full px-4 py-2 text-sm bg-stone-800 hover:bg-stone-700 text-white rounded-md shadow",
1137
+ children: control.label ?? key
1138
+ }
1139
+ )
1140
+ },
1141
+ `control-panel-custom-${key}`
1142
+ );
1143
+ const renderControl = (key, control, variant) => {
1144
+ if (control.type === "button") {
1145
+ return renderButtonControl(key, control, variant);
1146
+ }
1147
+ const value = values[key];
1148
+ switch (control.type) {
1149
+ case "boolean":
1150
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center justify-between", children: [
1151
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Label, { htmlFor: key, className: "cursor-pointer", children: labelize(key) }),
1152
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1153
+ Switch,
1154
+ {
1155
+ id: key,
1156
+ checked: value,
1157
+ onCheckedChange: (v) => setValue(key, v),
1158
+ className: "cursor-pointer scale-90"
1159
+ }
1160
+ )
1161
+ ] }, key);
1162
+ case "number":
1163
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-3 w-full", children: [
1164
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center justify-between", children: [
1165
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
1166
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1167
+ Input,
1168
+ {
1169
+ type: "number",
1170
+ value,
1171
+ min: control.min ?? 0,
1172
+ max: control.max ?? 100,
1173
+ step: control.step ?? 1,
1174
+ onChange: (e) => {
1175
+ const v = parseFloat(e.target.value);
1176
+ if (Number.isNaN(v)) return;
1177
+ setValue(key, v);
1178
+ },
1179
+ className: "w-20 text-center cursor-text"
1180
+ }
1181
+ )
1182
+ ] }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1184
+ Slider,
1185
+ {
1186
+ id: key,
1187
+ min: control.min ?? 0,
1188
+ max: control.max ?? 100,
1189
+ step: control.step ?? 1,
1190
+ value: [value],
1191
+ onValueChange: ([v]) => setValue(key, v),
1192
+ className: "w-full cursor-pointer"
686
1193
  }
687
- }),
688
- (buttonControls.length > 0 || jsx16) && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
689
- "div",
1194
+ )
1195
+ ] }, key);
1196
+ case "string":
1197
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-2 w-full", children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1200
+ Input,
1201
+ {
1202
+ id: key,
1203
+ value,
1204
+ placeholder: key,
1205
+ onChange: (e) => setValue(key, e.target.value),
1206
+ className: "bg-stone-900"
1207
+ }
1208
+ )
1209
+ ] }, key);
1210
+ case "color":
1211
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-2 w-full", children: [
1212
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
1213
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1214
+ "input",
1215
+ {
1216
+ type: "color",
1217
+ id: key,
1218
+ value,
1219
+ onChange: (e) => setValue(key, e.target.value),
1220
+ className: "w-full h-10 rounded border border-stone-600 bg-transparent"
1221
+ }
1222
+ )
1223
+ ] }, key);
1224
+ case "select":
1225
+ 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: [
1226
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Label, { className: "min-w-fit", htmlFor: key, children: labelize(key) }),
1227
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Select, { value, onValueChange: (val) => setValue(key, val), children: [
1228
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SelectTrigger, { className: "flex-1 cursor-pointer", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SelectValue, { placeholder: "Select option" }) }),
1229
+ /* @__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)(
1230
+ SelectItem,
1231
+ {
1232
+ value: label,
1233
+ className: "cursor-pointer",
1234
+ children: label
1235
+ },
1236
+ label
1237
+ )) })
1238
+ ] })
1239
+ ] }) }, key);
1240
+ default:
1241
+ return null;
1242
+ }
1243
+ };
1244
+ const renderFolder = (folder, entries, extras = []) => {
1245
+ const isOpen = folderStates[folder] ?? true;
1246
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1247
+ "div",
1248
+ {
1249
+ className: "border border-stone-700/60 rounded-lg bg-stone-900/70",
1250
+ children: [
1251
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1252
+ "button",
690
1253
  {
691
- className: `${normalControls.length > 0 ? "border-t" : ""} border-stone-700`,
1254
+ type: "button",
1255
+ onClick: () => setFolderStates((prev) => ({
1256
+ ...prev,
1257
+ [folder]: !isOpen
1258
+ })),
1259
+ className: "w-full flex items-center justify-between px-4 py-3 text-left font-semibold text-stone-200 tracking-wide",
692
1260
  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",
1261
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: folder }),
1262
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1263
+ import_lucide_react3.ChevronDown,
695
1264
  {
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
- ] })
1265
+ className: `w-4 h-4 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`
709
1266
  }
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
- ) })
1267
+ )
728
1268
  ]
729
1269
  }
730
- )
1270
+ ),
1271
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "px-4 pb-4 pt-0 space-y-5", children: [
1272
+ entries.map(
1273
+ ([key, control]) => renderControl(key, control, "folder")
1274
+ ),
1275
+ extras.map((extra) => extra)
1276
+ ] })
1277
+ ]
1278
+ },
1279
+ folder
1280
+ );
1281
+ };
1282
+ const topFolderSections = hasAnyFolders ? folderGroups.filter(({ placement }) => placement === "top").map(
1283
+ ({ folder, entries, extras }) => renderFolder(folder, entries, extras)
1284
+ ) : null;
1285
+ const bottomFolderSections = hasAnyFolders ? folderGroups.filter(({ placement }) => placement === "bottom").map(
1286
+ ({ folder, entries, extras }) => renderFolder(folder, entries, extras)
1287
+ ) : null;
1288
+ const panelStyle = {
1289
+ width: "100%",
1290
+ height: "auto",
1291
+ flex: "0 0 auto"
1292
+ };
1293
+ if (isHydrated) {
1294
+ if (isDesktop) {
1295
+ Object.assign(panelStyle, {
1296
+ position: "absolute",
1297
+ left: 0,
1298
+ top: 0,
1299
+ bottom: 0,
1300
+ width: `${leftPanelWidth}%`,
1301
+ overflowY: "auto"
1302
+ });
1303
+ } else {
1304
+ Object.assign(panelStyle, {
1305
+ marginTop: `calc(-1 * (${MOBILE_CONTROL_PANEL_PEEK}px + env(safe-area-inset-bottom, 0px)))`,
1306
+ paddingBottom: `calc(${MOBILE_CONTROL_PANEL_PEEK}px + env(safe-area-inset-bottom, 0px))`
1307
+ });
1308
+ }
1309
+ }
1310
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1311
+ "div",
1312
+ {
1313
+ 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"}`,
1314
+ onPointerDown: (e) => e.stopPropagation(),
1315
+ onTouchStart: (e) => e.stopPropagation(),
1316
+ style: panelStyle,
1317
+ 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: [
1318
+ /* @__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" }) }),
1319
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-6", children: [
1320
+ topFolderSections,
1321
+ hasRootButtonControls && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex flex-wrap gap-2", children: rootButtonControls.map(
1322
+ ([key, control]) => renderButtonControl(key, control, "root")
1323
+ ) }),
1324
+ advancedPaletteControlNode,
1325
+ rootNormalControls.map(
1326
+ ([key, control]) => renderControl(key, control, "root")
1327
+ ),
1328
+ bottomFolderSections,
1329
+ shouldShowCopyButton && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1330
+ "button",
1331
+ {
1332
+ onClick: () => {
1333
+ if (!copyText) return;
1334
+ navigator.clipboard.writeText(copyText);
1335
+ setCopied(true);
1336
+ setTimeout(() => setCopied(false), 5e3);
1337
+ },
1338
+ 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",
1339
+ children: copied ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1340
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.Check, { className: "w-4 h-4" }),
1341
+ "Copied"
1342
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1343
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.Copy, { className: "w-4 h-4" }),
1344
+ "Copy to Clipboard"
1345
+ ] })
1346
+ }
1347
+ ) }, "control-panel-jsx")
731
1348
  ] }),
732
- previewUrl && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Button, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1349
+ previewUrl && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
733
1350
  "a",
734
1351
  {
735
1352
  href: previewUrl,
736
1353
  target: "_blank",
737
1354
  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",
1355
+ 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",
739
1356
  children: [
740
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.SquareArrowOutUpRight, {}),
1357
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.SquareArrowOutUpRight, {}),
741
1358
  " Open in a New Tab"
742
1359
  ]
743
1360
  }
@@ -749,12 +1366,12 @@ var ControlPanel = () => {
749
1366
  var ControlPanel_default = ControlPanel;
750
1367
 
751
1368
  // src/components/PreviewContainer/PreviewContainer.tsx
752
- var import_react5 = require("react");
1369
+ var import_react6 = require("react");
753
1370
 
754
1371
  // src/components/Grid/Grid.tsx
755
- var import_jsx_runtime10 = require("react/jsx-runtime");
1372
+ var import_jsx_runtime11 = require("react/jsx-runtime");
756
1373
  function Grid() {
757
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1374
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
758
1375
  "div",
759
1376
  {
760
1377
  className: "absolute inset-0 w-screen h-screen z-[0] blur-[1px]",
@@ -772,12 +1389,12 @@ function Grid() {
772
1389
  var Grid_default = Grid;
773
1390
 
774
1391
  // src/components/PreviewContainer/PreviewContainer.tsx
775
- var import_jsx_runtime11 = require("react/jsx-runtime");
1392
+ var import_jsx_runtime12 = require("react/jsx-runtime");
776
1393
  var PreviewContainer = ({ children, hideControls }) => {
777
1394
  const { config } = useControlsContext();
778
1395
  const { leftPanelWidth, isDesktop, isHydrated, containerRef } = useResizableLayout();
779
- const previewRef = (0, import_react5.useRef)(null);
780
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1396
+ const previewRef = (0, import_react6.useRef)(null);
1397
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
781
1398
  "div",
782
1399
  {
783
1400
  ref: previewRef,
@@ -786,9 +1403,9 @@ var PreviewContainer = ({ children, hideControls }) => {
786
1403
  width: `${100 - leftPanelWidth}%`,
787
1404
  marginLeft: `${leftPanelWidth}%`
788
1405
  } : {},
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 })
1406
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "w-screen h-screen", children: [
1407
+ config?.showGrid && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Grid_default, {}),
1408
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "w-screen h-screen flex items-center justify-center relative", children })
792
1409
  ] })
793
1410
  }
794
1411
  );
@@ -796,15 +1413,15 @@ var PreviewContainer = ({ children, hideControls }) => {
796
1413
  var PreviewContainer_default = PreviewContainer;
797
1414
 
798
1415
  // src/components/Playground/Playground.tsx
799
- var import_jsx_runtime12 = require("react/jsx-runtime");
1416
+ var import_jsx_runtime13 = require("react/jsx-runtime");
800
1417
  var NO_CONTROLS_PARAM = "nocontrols";
801
1418
  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)(() => {
1419
+ const [isHydrated, setIsHydrated] = (0, import_react7.useState)(false);
1420
+ const [copied, setCopied] = (0, import_react7.useState)(false);
1421
+ (0, import_react7.useEffect)(() => {
805
1422
  setIsHydrated(true);
806
1423
  }, []);
807
- const hideControls = (0, import_react6.useMemo)(() => {
1424
+ const hideControls = (0, import_react7.useMemo)(() => {
808
1425
  if (typeof window === "undefined") return false;
809
1426
  return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
810
1427
  }, []);
@@ -814,143 +1431,144 @@ function Playground({ children }) {
814
1431
  setTimeout(() => setCopied(false), 2e3);
815
1432
  };
816
1433
  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)(
1434
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ResizableLayout, { hideControls, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ControlsProvider, { children: [
1435
+ hideControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
819
1436
  "button",
820
1437
  {
821
1438
  onClick: handleCopy,
822
1439
  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
1440
  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 }),
1441
+ 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
1442
  copied ? "Copied!" : "Share"
826
1443
  ]
827
1444
  }
828
1445
  ),
829
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PreviewContainer_default, { hideControls, children }),
830
- !hideControls && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ControlPanel_default, {})
1446
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PreviewContainer_default, { hideControls, children }),
1447
+ !hideControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ControlPanel_default, {})
831
1448
  ] }) });
832
1449
  }
833
1450
 
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 = ({ mediaProps, children }) => {
880
- const canvasRef = (0, import_react8.useRef)(null);
881
- const [parentSize, setParentSize] = (0, import_react8.useState)(null);
1451
+ // src/hooks/useAdvancedPaletteControls.ts
1452
+ var import_react8 = require("react");
1453
+ var cloneForCallbacks = (palette) => clonePalette(palette);
1454
+ var useAdvancedPaletteControls = (options = {}) => {
1455
+ const resolvedDefaultPalette = (0, import_react8.useMemo)(
1456
+ () => createAdvancedPalette(options.defaultPalette),
1457
+ [options.defaultPalette]
1458
+ );
1459
+ const resolvedFallbackPalette = (0, import_react8.useMemo)(
1460
+ () => options.fallbackPalette ? createAdvancedPalette(options.fallbackPalette) : resolvedDefaultPalette,
1461
+ [options.fallbackPalette, resolvedDefaultPalette]
1462
+ );
1463
+ const [palette, setPaletteState] = (0, import_react8.useState)(
1464
+ () => clonePalette(resolvedDefaultPalette)
1465
+ );
1466
+ const defaultSignatureRef = (0, import_react8.useRef)(
1467
+ createPaletteSignature(resolvedDefaultPalette)
1468
+ );
882
1469
  (0, import_react8.useEffect)(() => {
883
- let observer = null;
884
- const tryObserve = () => {
885
- const node = canvasRef.current;
886
- if (!node || !node.parentElement) {
887
- setTimeout(tryObserve, 50);
888
- return;
889
- }
890
- const parent = node.parentElement;
891
- observer = new ResizeObserver(([entry]) => {
892
- const { width, height } = entry.contentRect;
893
- setParentSize({ width, height });
1470
+ const nextSignature = createPaletteSignature(resolvedDefaultPalette);
1471
+ if (defaultSignatureRef.current === nextSignature) return;
1472
+ defaultSignatureRef.current = nextSignature;
1473
+ setPaletteState(clonePalette(resolvedDefaultPalette));
1474
+ }, [resolvedDefaultPalette]);
1475
+ const notifyChange = (0, import_react8.useCallback)(
1476
+ (nextPalette) => {
1477
+ options.onChange?.(cloneForCallbacks(nextPalette));
1478
+ },
1479
+ [options.onChange]
1480
+ );
1481
+ const setPalette = (0, import_react8.useCallback)(
1482
+ (source) => {
1483
+ const nextPalette = createAdvancedPalette(
1484
+ source ?? resolvedDefaultPalette
1485
+ );
1486
+ setPaletteState(clonePalette(nextPalette));
1487
+ notifyChange(nextPalette);
1488
+ },
1489
+ [notifyChange, resolvedDefaultPalette]
1490
+ );
1491
+ const updatePalette = (0, import_react8.useCallback)(
1492
+ (updater) => {
1493
+ setPaletteState((current) => {
1494
+ const nextSource = updater(clonePalette(current));
1495
+ const nextPalette = createAdvancedPalette(
1496
+ nextSource ?? current ?? resolvedDefaultPalette
1497
+ );
1498
+ notifyChange(nextPalette);
1499
+ return clonePalette(nextPalette);
894
1500
  });
895
- observer.observe(parent);
896
- };
897
- tryObserve();
898
- return () => {
899
- if (observer) observer.disconnect();
900
- };
901
- }, []);
902
- const mergedMediaProps = {
903
- ...mediaProps || {},
904
- size: mediaProps?.size || { width: 400, height: 400 }
905
- };
906
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
907
- "div",
908
- {
909
- ref: canvasRef,
910
- className: "w-full h-full pointer-events-none relative touch-none",
911
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
912
- import_fiber2.Canvas,
913
- {
914
- resize: { polyfill: ResizeObserver },
915
- style: { width: parentSize?.width, height: parentSize?.height },
916
- gl: { preserveDrawingBuffer: true },
917
- children: [
918
- parentSize?.height && parentSize?.width && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
919
- ResponsiveCamera,
920
- {
921
- height: parentSize.height,
922
- width: parentSize.width
923
- }
924
- ),
925
- mediaProps?.debugOrbit && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CameraLogger, {}),
926
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ambientLight", { intensity: 1 }),
927
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("pointLight", { position: [10, 10, 10] }),
928
- import_react8.default.cloneElement(children, mergedMediaProps)
929
- ]
930
- }
931
- )
932
- }
1501
+ },
1502
+ [notifyChange, resolvedDefaultPalette]
933
1503
  );
1504
+ const resetPalette = (0, import_react8.useCallback)(() => {
1505
+ setPaletteState(clonePalette(resolvedDefaultPalette));
1506
+ notifyChange(resolvedDefaultPalette);
1507
+ }, [notifyChange, resolvedDefaultPalette]);
1508
+ const handleControlPaletteChange = (0, import_react8.useCallback)(
1509
+ (nextPalette) => {
1510
+ setPaletteState(clonePalette(nextPalette));
1511
+ notifyChange(nextPalette);
1512
+ },
1513
+ [notifyChange]
1514
+ );
1515
+ const controlConfig = (0, import_react8.useMemo)(
1516
+ () => ({
1517
+ ...options.control ?? {},
1518
+ defaultPalette: resolvedDefaultPalette,
1519
+ onPaletteChange: handleControlPaletteChange
1520
+ }),
1521
+ [handleControlPaletteChange, options.control, resolvedDefaultPalette]
1522
+ );
1523
+ const hexColors = (0, import_react8.useMemo)(
1524
+ () => advancedPaletteToHexColors(palette, {
1525
+ sectionOrder: options.sectionOrder,
1526
+ fallbackPalette: resolvedFallbackPalette,
1527
+ defaultColor: options.defaultColor
1528
+ }),
1529
+ [
1530
+ options.defaultColor,
1531
+ options.sectionOrder,
1532
+ palette,
1533
+ resolvedFallbackPalette
1534
+ ]
1535
+ );
1536
+ const paletteSignature = (0, import_react8.useMemo)(
1537
+ () => createPaletteSignature(palette),
1538
+ [palette]
1539
+ );
1540
+ const paletteGradient = (0, import_react8.useMemo)(
1541
+ () => computePaletteGradient(palette, options.gradientSteps),
1542
+ [options.gradientSteps, palette]
1543
+ );
1544
+ return {
1545
+ palette,
1546
+ hexColors,
1547
+ controlConfig,
1548
+ paletteGradient,
1549
+ setPalette,
1550
+ updatePalette,
1551
+ resetPalette,
1552
+ paletteSignature
1553
+ };
934
1554
  };
935
- var Canvas_default = Canvas;
936
-
937
- // src/components/PlaygroundCanvas/PlaygroundCanvas.tsx
938
- var import_jsx_runtime15 = require("react/jsx-runtime");
939
- var PlaygroundCanvas = ({
940
- children,
941
- mediaProps
942
- }) => {
943
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Playground, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Canvas_default, { mediaProps, children }) });
944
- };
945
- var PlaygroundCanvas_default = PlaygroundCanvas;
1555
+ var useDefaultAdvancedPaletteControls = () => useAdvancedPaletteControls({ defaultPalette: DEFAULT_ADVANCED_PALETTE });
946
1556
  // Annotate the CommonJS export names for ESM import in node:
947
1557
  0 && (module.exports = {
948
1558
  Button,
949
- CameraLogger,
950
- Canvas,
951
1559
  ControlsProvider,
1560
+ DEFAULT_ADVANCED_PALETTE,
1561
+ DEFAULT_HEX_PALETTE,
952
1562
  Playground,
953
- PlaygroundCanvas,
1563
+ advancedPaletteToHexColors,
1564
+ clonePalette,
1565
+ computePaletteGradient,
1566
+ createAdvancedPalette,
1567
+ createPaletteSignature,
1568
+ hexToPaletteValue,
1569
+ paletteValueToHex,
1570
+ useAdvancedPaletteControls,
954
1571
  useControls,
1572
+ useDefaultAdvancedPaletteControls,
955
1573
  useUrlSyncedControls
956
1574
  });