dialkit 0.1.2 → 0.2.1

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
@@ -12,12 +12,16 @@ var DialStoreClass = class {
12
12
  this.globalListeners = /* @__PURE__ */ new Set();
13
13
  this.snapshots = /* @__PURE__ */ new Map();
14
14
  this.actionListeners = /* @__PURE__ */ new Map();
15
+ this.presets = /* @__PURE__ */ new Map();
16
+ this.activePreset = /* @__PURE__ */ new Map();
17
+ this.baseValues = /* @__PURE__ */ new Map();
15
18
  }
16
19
  registerPanel(id, name, config) {
17
20
  const controls = this.parseConfig(config, "");
18
21
  const values = this.flattenValues(config, "");
19
22
  this.panels.set(id, { id, name, controls, values });
20
23
  this.snapshots.set(id, { ...values });
24
+ this.baseValues.set(id, { ...values });
21
25
  this.notifyGlobal();
22
26
  }
23
27
  unregisterPanel(id) {
@@ -30,6 +34,15 @@ var DialStoreClass = class {
30
34
  const panel = this.panels.get(panelId);
31
35
  if (!panel) return;
32
36
  panel.values[path] = value;
37
+ const activeId = this.activePreset.get(panelId);
38
+ if (activeId) {
39
+ const presets = this.presets.get(panelId) ?? [];
40
+ const preset = presets.find((p) => p.id === activeId);
41
+ if (preset) preset.values[path] = value;
42
+ } else {
43
+ const base = this.baseValues.get(panelId);
44
+ if (base) base[path] = value;
45
+ }
33
46
  this.snapshots.set(panelId, { ...panel.values });
34
47
  this.notify(panelId);
35
48
  }
@@ -83,6 +96,61 @@ var DialStoreClass = class {
83
96
  triggerAction(panelId, path) {
84
97
  this.actionListeners.get(panelId)?.forEach((fn) => fn(path));
85
98
  }
99
+ savePreset(panelId, name) {
100
+ const panel = this.panels.get(panelId);
101
+ if (!panel) throw new Error(`Panel ${panelId} not found`);
102
+ const id = `preset-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
103
+ const preset = {
104
+ id,
105
+ name,
106
+ values: { ...panel.values }
107
+ };
108
+ const existing = this.presets.get(panelId) ?? [];
109
+ this.presets.set(panelId, [...existing, preset]);
110
+ this.activePreset.set(panelId, id);
111
+ this.snapshots.set(panelId, { ...panel.values });
112
+ this.notify(panelId);
113
+ return id;
114
+ }
115
+ loadPreset(panelId, presetId) {
116
+ const panel = this.panels.get(panelId);
117
+ if (!panel) return;
118
+ const presets = this.presets.get(panelId) ?? [];
119
+ const preset = presets.find((p) => p.id === presetId);
120
+ if (!preset) return;
121
+ panel.values = { ...preset.values };
122
+ this.snapshots.set(panelId, { ...panel.values });
123
+ this.activePreset.set(panelId, presetId);
124
+ this.notify(panelId);
125
+ }
126
+ deletePreset(panelId, presetId) {
127
+ const presets = this.presets.get(panelId) ?? [];
128
+ this.presets.set(panelId, presets.filter((p) => p.id !== presetId));
129
+ if (this.activePreset.get(panelId) === presetId) {
130
+ this.activePreset.set(panelId, null);
131
+ }
132
+ const panel = this.panels.get(panelId);
133
+ if (panel) {
134
+ this.snapshots.set(panelId, { ...panel.values });
135
+ }
136
+ this.notify(panelId);
137
+ }
138
+ getPresets(panelId) {
139
+ return this.presets.get(panelId) ?? [];
140
+ }
141
+ getActivePresetId(panelId) {
142
+ return this.activePreset.get(panelId) ?? null;
143
+ }
144
+ clearActivePreset(panelId) {
145
+ const panel = this.panels.get(panelId);
146
+ const base = this.baseValues.get(panelId);
147
+ if (panel && base) {
148
+ panel.values = { ...base };
149
+ this.snapshots.set(panelId, { ...panel.values });
150
+ }
151
+ this.activePreset.set(panelId, null);
152
+ this.notify(panelId);
153
+ }
86
154
  notify(panelId) {
87
155
  this.listeners.get(panelId)?.forEach((fn) => fn());
88
156
  }
@@ -112,6 +180,18 @@ var DialStoreClass = class {
112
180
  controls.push({ type: "spring", path, label });
113
181
  } else if (this.isActionConfig(value)) {
114
182
  controls.push({ type: "action", path, label: value.label || label });
183
+ } else if (this.isSelectConfig(value)) {
184
+ controls.push({ type: "select", path, label, options: value.options });
185
+ } else if (this.isColorConfig(value)) {
186
+ controls.push({ type: "color", path, label });
187
+ } else if (this.isTextConfig(value)) {
188
+ controls.push({ type: "text", path, label, placeholder: value.placeholder });
189
+ } else if (typeof value === "string") {
190
+ if (this.isHexColor(value)) {
191
+ controls.push({ type: "color", path, label });
192
+ } else {
193
+ controls.push({ type: "text", path, label });
194
+ }
115
195
  } else if (typeof value === "object" && value !== null) {
116
196
  controls.push({
117
197
  type: "folder",
@@ -135,6 +215,14 @@ var DialStoreClass = class {
135
215
  values[path] = value;
136
216
  } else if (this.isActionConfig(value)) {
137
217
  values[path] = value;
218
+ } else if (this.isSelectConfig(value)) {
219
+ const firstOption = value.options[0];
220
+ const firstValue = typeof firstOption === "string" ? firstOption : firstOption.value;
221
+ values[path] = value.default ?? firstValue;
222
+ } else if (this.isColorConfig(value)) {
223
+ values[path] = value.default ?? "#000000";
224
+ } else if (this.isTextConfig(value)) {
225
+ values[path] = value.default ?? "";
138
226
  } else if (typeof value === "object" && value !== null) {
139
227
  Object.assign(values, this.flattenValues(value, path));
140
228
  }
@@ -147,6 +235,18 @@ var DialStoreClass = class {
147
235
  isActionConfig(value) {
148
236
  return typeof value === "object" && value !== null && "type" in value && value.type === "action";
149
237
  }
238
+ isSelectConfig(value) {
239
+ return typeof value === "object" && value !== null && "type" in value && value.type === "select" && "options" in value && Array.isArray(value.options);
240
+ }
241
+ isColorConfig(value) {
242
+ return typeof value === "object" && value !== null && "type" in value && value.type === "color";
243
+ }
244
+ isTextConfig(value) {
245
+ return typeof value === "object" && value !== null && "type" in value && value.type === "text";
246
+ }
247
+ isHexColor(value) {
248
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
249
+ }
150
250
  formatLabel(key) {
151
251
  return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
152
252
  }
@@ -154,13 +254,13 @@ var DialStoreClass = class {
154
254
  if (value >= 0 && value <= 1) {
155
255
  return { min: 0, max: 1, step: 0.01 };
156
256
  } else if (value >= 0 && value <= 10) {
157
- return { min: 0, max: value * 2 || 10, step: 0.1 };
257
+ return { min: 0, max: value * 3 || 10, step: 0.1 };
158
258
  } else if (value >= 0 && value <= 100) {
159
- return { min: 0, max: value * 2 || 100, step: 1 };
259
+ return { min: 0, max: value * 3 || 100, step: 1 };
160
260
  } else if (value >= 0) {
161
- return { min: 0, max: value * 2 || 1e3, step: 10 };
261
+ return { min: 0, max: value * 3 || 1e3, step: 10 };
162
262
  } else {
163
- return { min: value * 2, max: -value * 2, step: 1 };
263
+ return { min: value * 3, max: -value * 3, step: 1 };
164
264
  }
165
265
  }
166
266
  inferStep(min, max) {
@@ -206,204 +306,149 @@ function buildResolvedValues(config, flatValues, prefix) {
206
306
  result[key] = flatValues[path] ?? configValue;
207
307
  } else if (isSpringConfig(configValue)) {
208
308
  result[key] = flatValues[path] ?? configValue;
309
+ } else if (isActionConfig(configValue)) {
310
+ result[key] = flatValues[path] ?? configValue;
311
+ } else if (isSelectConfig(configValue)) {
312
+ const defaultValue = configValue.default ?? getFirstOptionValue(configValue.options);
313
+ result[key] = flatValues[path] ?? defaultValue;
314
+ } else if (isColorConfig(configValue)) {
315
+ result[key] = flatValues[path] ?? configValue.default ?? "#000000";
316
+ } else if (isTextConfig(configValue)) {
317
+ result[key] = flatValues[path] ?? configValue.default ?? "";
209
318
  } else if (typeof configValue === "object" && configValue !== null) {
210
319
  result[key] = buildResolvedValues(configValue, flatValues, path);
211
320
  }
212
321
  }
213
322
  return result;
214
323
  }
324
+ function hasType(value, type) {
325
+ return typeof value === "object" && value !== null && "type" in value && value.type === type;
326
+ }
215
327
  function isSpringConfig(value) {
216
- return typeof value === "object" && value !== null && "type" in value && value.type === "spring";
328
+ return hasType(value, "spring");
329
+ }
330
+ function isActionConfig(value) {
331
+ return hasType(value, "action");
332
+ }
333
+ function isSelectConfig(value) {
334
+ return hasType(value, "select") && "options" in value && Array.isArray(value.options);
335
+ }
336
+ function isColorConfig(value) {
337
+ return hasType(value, "color");
338
+ }
339
+ function isTextConfig(value) {
340
+ return hasType(value, "text");
341
+ }
342
+ function getFirstOptionValue(options) {
343
+ const first = options[0];
344
+ return typeof first === "string" ? first : first.value;
217
345
  }
218
346
 
219
347
  // src/components/DialRoot.tsx
220
- import { useEffect as useEffect3, useState as useState4 } from "react";
221
- import { createPortal } from "react-dom";
348
+ import { useEffect as useEffect7, useState as useState8 } from "react";
349
+ import { createPortal as createPortal2 } from "react-dom";
350
+
351
+ // src/components/Panel.tsx
352
+ import { useState as useState7, useSyncExternalStore as useSyncExternalStore3 } from "react";
353
+ import { motion as motion6, AnimatePresence as AnimatePresence4 } from "motion/react";
222
354
 
223
355
  // src/components/Folder.tsx
224
- import { useState } from "react";
356
+ import { useState, useRef as useRef2, useEffect as useEffect2 } from "react";
225
357
  import { motion, AnimatePresence } from "motion/react";
226
358
  import { jsx, jsxs } from "react/jsx-runtime";
227
- function Folder({ title, children, defaultOpen = true, isRoot = false, panelId }) {
359
+ function Folder({ title, children, defaultOpen = true, isRoot = false, onOpenChange, toolbar }) {
228
360
  const [isOpen, setIsOpen] = useState(defaultOpen);
229
- const [copied, setCopied] = useState(false);
230
- const handleCopy = (e) => {
231
- e.stopPropagation();
232
- if (!panelId) return;
233
- const values = DialStore.getValues(panelId);
234
- const jsonStr = JSON.stringify(values, null, 2);
235
- const instruction = `Update the useDialKit configuration for "${title}" with these values:
236
-
237
- \`\`\`json
238
- ${jsonStr}
239
- \`\`\`
240
-
241
- Apply these values as the new defaults in the useDialKit call.`;
242
- navigator.clipboard.writeText(instruction);
243
- setCopied(true);
244
- setTimeout(() => setCopied(false), 1500);
361
+ const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
362
+ const contentRef = useRef2(null);
363
+ const [contentHeight, setContentHeight] = useState(void 0);
364
+ useEffect2(() => {
365
+ const el = contentRef.current;
366
+ if (!el) return;
367
+ const ro = new ResizeObserver(() => {
368
+ if (isOpen) {
369
+ const h = el.offsetHeight;
370
+ setContentHeight((prev) => prev === h ? prev : h);
371
+ }
372
+ });
373
+ ro.observe(el);
374
+ return () => ro.disconnect();
375
+ }, [isOpen]);
376
+ const handleToggle = () => {
377
+ const next = !isOpen;
378
+ setIsOpen(next);
379
+ if (next) {
380
+ setIsCollapsed(false);
381
+ } else {
382
+ setIsCollapsed(true);
383
+ }
384
+ onOpenChange?.(next);
245
385
  };
246
- const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
247
- const folderContent = /* @__PURE__ */ jsxs("div", { className: `dialkit-folder ${isRoot ? "dialkit-folder-root" : ""}`, children: [
248
- /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-header", onClick: () => setIsOpen(!isOpen), children: [
249
- /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-title-row", children: [
250
- /* @__PURE__ */ jsx("span", { className: `dialkit-folder-title ${isRoot ? "dialkit-folder-title-root" : ""}`, children: title }),
251
- isRoot && panelId && /* @__PURE__ */ jsx(
252
- motion.button,
253
- {
254
- className: "dialkit-folder-copy",
255
- onClick: handleCopy,
256
- title: "Copy parameters",
257
- initial: false,
258
- animate: {
259
- opacity: isOpen ? 1 : 0,
260
- scale: isOpen ? 1 : 0.7,
261
- filter: isOpen ? "blur(0px)" : "blur(6px)"
262
- },
263
- transition: iconTransition,
264
- style: { pointerEvents: isOpen ? "auto" : "none" },
265
- children: /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: 14, height: 14 }, children: [
266
- /* @__PURE__ */ jsxs(
267
- motion.svg,
268
- {
269
- viewBox: "0 0 24 24",
270
- fill: "none",
271
- stroke: "currentColor",
272
- strokeWidth: "2",
273
- strokeLinecap: "round",
274
- strokeLinejoin: "round",
275
- style: { position: "absolute", inset: 0 },
276
- initial: false,
277
- animate: {
278
- opacity: copied ? 0 : 1,
279
- scale: copied ? 0.7 : 1,
280
- filter: copied ? "blur(6px)" : "blur(0px)"
281
- },
282
- transition: iconTransition,
283
- children: [
284
- /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
285
- /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
286
- ]
287
- }
288
- ),
289
- /* @__PURE__ */ jsx(
290
- motion.svg,
291
- {
292
- viewBox: "0 0 24 24",
293
- fill: "none",
294
- stroke: "currentColor",
295
- strokeWidth: "2",
296
- strokeLinecap: "round",
297
- strokeLinejoin: "round",
298
- style: { position: "absolute", inset: 0 },
299
- initial: false,
300
- animate: {
301
- opacity: copied ? 1 : 0,
302
- scale: copied ? 1 : 0.7,
303
- filter: copied ? "blur(0px)" : "blur(6px)"
304
- },
305
- transition: iconTransition,
306
- children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
307
- }
308
- )
309
- ] })
310
- }
311
- )
312
- ] }),
313
- isRoot ? (
314
- // Root panel uses minus/plus icons with crossfade
315
- /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-icon", style: { position: "relative" }, children: [
316
- /* @__PURE__ */ jsx(
317
- motion.svg,
386
+ const folderContent = /* @__PURE__ */ jsxs("div", { ref: isRoot ? contentRef : void 0, className: `dialkit-folder ${isRoot ? "dialkit-folder-root" : ""}`, children: [
387
+ /* @__PURE__ */ jsxs("div", { className: `dialkit-folder-header ${isRoot ? "dialkit-panel-header" : ""}`, onClick: handleToggle, children: [
388
+ /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-header-top", children: [
389
+ isRoot ? isOpen && /* @__PURE__ */ jsx("div", { className: "dialkit-folder-title-row", children: /* @__PURE__ */ jsx("span", { className: "dialkit-folder-title dialkit-folder-title-root", children: title }) }) : /* @__PURE__ */ jsx("div", { className: "dialkit-folder-title-row", children: /* @__PURE__ */ jsx("span", { className: "dialkit-folder-title", children: title }) }),
390
+ isRoot ? (
391
+ // Root panel icon fixed position, container morphs around it
392
+ /* @__PURE__ */ jsxs(
393
+ "svg",
318
394
  {
319
- viewBox: "0 0 24 24",
395
+ className: "dialkit-panel-icon",
396
+ viewBox: "0 0 16 16",
320
397
  fill: "none",
321
- stroke: "currentColor",
322
- strokeWidth: "2",
323
- strokeLinecap: "round",
324
- strokeLinejoin: "round",
325
- style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
326
- initial: false,
327
- animate: {
328
- opacity: isOpen ? 1 : 0,
329
- scale: isOpen ? 1 : 0.7,
330
- filter: isOpen ? "blur(0px)" : "blur(6px)"
331
- },
332
- transition: iconTransition,
333
- children: /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
398
+ children: [
399
+ /* @__PURE__ */ jsx("path", { opacity: "0.5", d: "M6.84766 11.75C6.78583 11.9899 6.75 12.2408 6.75 12.5C6.75 12.7592 6.78583 13.0101 6.84766 13.25H2C1.58579 13.25 1.25 12.9142 1.25 12.5C1.25 12.0858 1.58579 11.75 2 11.75H6.84766ZM14 11.75C14.4142 11.75 14.75 12.0858 14.75 12.5C14.75 12.9142 14.4142 13.25 14 13.25H12.6523C12.7142 13.0101 12.75 12.7592 12.75 12.5C12.75 12.2408 12.7142 11.9899 12.6523 11.75H14ZM3.09766 7.25C3.03583 7.48994 3 7.74075 3 8C3 8.25925 3.03583 8.51006 3.09766 8.75H2C1.58579 8.75 1.25 8.41421 1.25 8C1.25 7.58579 1.58579 7.25 2 7.25H3.09766ZM14 7.25C14.4142 7.25 14.75 7.58579 14.75 8C14.75 8.41421 14.4142 8.75 14 8.75H8.90234C8.96417 8.51006 9 8.25925 9 8C9 7.74075 8.96417 7.48994 8.90234 7.25H14ZM7.59766 2.75C7.53583 2.98994 7.5 3.24075 7.5 3.5C7.5 3.75925 7.53583 4.01006 7.59766 4.25H2C1.58579 4.25 1.25 3.91421 1.25 3.5C1.25 3.08579 1.58579 2.75 2 2.75H7.59766ZM14 2.75C14.4142 2.75 14.75 3.08579 14.75 3.5C14.75 3.91421 14.4142 4.25 14 4.25H13.4023C13.4642 4.01006 13.5 3.75925 13.5 3.5C13.5 3.24075 13.4642 2.98994 13.4023 2.75H14Z", fill: "currentColor" }),
400
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "8", r: "0.998596", fill: "currentColor", stroke: "currentColor", strokeWidth: "1.25" }),
401
+ /* @__PURE__ */ jsx("circle", { cx: "10.4999", cy: "3.5", r: "0.998657", fill: "currentColor", stroke: "currentColor", strokeWidth: "1.25" }),
402
+ /* @__PURE__ */ jsx("circle", { cx: "9.75015", cy: "12.5", r: "0.997986", fill: "currentColor", stroke: "currentColor", strokeWidth: "1.25" })
403
+ ]
334
404
  }
335
- ),
336
- /* @__PURE__ */ jsxs(
405
+ )
406
+ ) : (
407
+ // Section folders use rotating chevron with gentle spring
408
+ /* @__PURE__ */ jsx(
337
409
  motion.svg,
338
410
  {
411
+ className: "dialkit-folder-icon",
339
412
  viewBox: "0 0 24 24",
340
413
  fill: "none",
341
414
  stroke: "currentColor",
342
- strokeWidth: "2",
415
+ strokeWidth: "2.5",
343
416
  strokeLinecap: "round",
344
417
  strokeLinejoin: "round",
345
- style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
346
418
  initial: false,
347
- animate: {
348
- opacity: isOpen ? 0 : 1,
349
- scale: isOpen ? 0.7 : 1,
350
- filter: isOpen ? "blur(6px)" : "blur(0px)"
351
- },
352
- transition: iconTransition,
353
- children: [
354
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
355
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
356
- ]
419
+ animate: { rotate: isOpen ? 0 : 180 },
420
+ transition: { type: "spring", visualDuration: 0.35, bounce: 0.15 },
421
+ children: /* @__PURE__ */ jsx("path", { d: "M6 9.5L12 15.5L18 9.5" })
357
422
  }
358
423
  )
359
- ] })
360
- ) : (
361
- // Section folders use rotating chevron with gentle spring
362
- /* @__PURE__ */ jsxs(
363
- motion.svg,
364
- {
365
- className: "dialkit-folder-icon",
366
- viewBox: "0 0 24 24",
367
- fill: "none",
368
- stroke: "currentColor",
369
- strokeWidth: "1.5",
370
- strokeLinecap: "round",
371
- strokeLinejoin: "round",
372
- initial: false,
373
- animate: { rotate: isOpen ? 0 : 180 },
374
- transition: { type: "spring", visualDuration: 0.35, bounce: 0.15 },
375
- children: [
376
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "9", x2: "12", y2: "16" }),
377
- /* @__PURE__ */ jsx("line", { x1: "19", y1: "9", x2: "12", y2: "16" })
378
- ]
379
- }
380
424
  )
381
- )
425
+ ] }),
426
+ isRoot && toolbar && isOpen && /* @__PURE__ */ jsx("div", { className: "dialkit-panel-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar })
382
427
  ] }),
383
428
  /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: isOpen && /* @__PURE__ */ jsx(
384
429
  motion.div,
385
430
  {
386
431
  className: "dialkit-folder-content",
387
- initial: { height: 0, opacity: 0 },
388
- animate: { height: "auto", opacity: 1 },
389
- exit: { height: 0, opacity: 0 },
390
- transition: { type: "spring", visualDuration: 0.25, bounce: 0 },
432
+ initial: isRoot ? void 0 : { height: 0, opacity: 0 },
433
+ animate: isRoot ? void 0 : { height: "auto", opacity: 1 },
434
+ exit: isRoot ? void 0 : { height: 0, opacity: 0 },
435
+ transition: isRoot ? void 0 : { type: "spring", visualDuration: 0.35, bounce: 0.1 },
436
+ style: isRoot ? void 0 : { clipPath: "inset(0 -20px)" },
391
437
  children: /* @__PURE__ */ jsx("div", { className: "dialkit-folder-inner", children })
392
438
  }
393
439
  ) })
394
440
  ] });
395
441
  if (isRoot) {
442
+ const panelStyle = isOpen ? { width: 280, height: contentHeight !== void 0 ? contentHeight + 24 : "auto", borderRadius: 14, boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5)", cursor: void 0 } : { width: 42, height: 42, borderRadius: 21, boxShadow: "0 4px 16px rgba(0, 0, 0, 0.25)", overflow: "hidden", cursor: "pointer" };
396
443
  return /* @__PURE__ */ jsx(
397
444
  motion.div,
398
445
  {
399
446
  className: "dialkit-panel-inner",
400
- initial: false,
401
- animate: {
402
- width: isOpen ? 280 : 160,
403
- boxShadow: isOpen ? "0 8px 32px rgba(0, 0, 0, 0.5)" : "0 4px 16px rgba(0, 0, 0, 0.25)"
404
- },
405
- transition: { type: "spring", visualDuration: 0.4, bounce: 0.05 },
406
- "data-collapsed": !isOpen,
447
+ style: panelStyle,
448
+ onClick: !isOpen ? handleToggle : void 0,
449
+ "data-collapsed": isCollapsed,
450
+ whileTap: !isOpen ? { scale: 0.9 } : void 0,
451
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
407
452
  children: folderContent
408
453
  }
409
454
  );
@@ -412,9 +457,26 @@ Apply these values as the new defaults in the useDialKit call.`;
412
457
  }
413
458
 
414
459
  // src/components/Slider.tsx
415
- import { useRef as useRef2, useState as useState2, useCallback, useEffect as useEffect2 } from "react";
416
- import { motion as motion2 } from "motion/react";
460
+ import { useRef as useRef3, useState as useState2, useCallback, useEffect as useEffect3 } from "react";
461
+ import { motion as motion2, useMotionValue, useTransform, animate } from "motion/react";
417
462
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
463
+ var CLICK_THRESHOLD = 3;
464
+ var DEAD_ZONE = 32;
465
+ var MAX_CURSOR_RANGE = 200;
466
+ var MAX_STRETCH = 8;
467
+ function roundValue(val, step) {
468
+ const decimals = step >= 1 ? 0 : 2;
469
+ const factor = 10 ** decimals;
470
+ return Math.round(val * factor) / factor;
471
+ }
472
+ function snapToDecile(rawValue, min, max) {
473
+ const normalized = (rawValue - min) / (max - min);
474
+ const nearest = Math.round(normalized * 10) / 10;
475
+ if (Math.abs(normalized - nearest) <= 0.03125) {
476
+ return min + nearest * (max - min);
477
+ }
478
+ return rawValue;
479
+ }
418
480
  function Slider({
419
481
  label,
420
482
  value,
@@ -424,8 +486,11 @@ function Slider({
424
486
  step = 0.01,
425
487
  unit
426
488
  }) {
427
- const trackRef = useRef2(null);
428
- const inputRef = useRef2(null);
489
+ const wrapperRef = useRef3(null);
490
+ const trackRef = useRef3(null);
491
+ const inputRef = useRef3(null);
492
+ const labelRef = useRef3(null);
493
+ const valueSpanRef = useRef3(null);
429
494
  const [isInteracting, setIsInteracting] = useState2(false);
430
495
  const [isDragging, setIsDragging] = useState2(false);
431
496
  const [isHovered, setIsHovered] = useState2(false);
@@ -433,80 +498,162 @@ function Slider({
433
498
  const [isValueEditable, setIsValueEditable] = useState2(false);
434
499
  const [showInput, setShowInput] = useState2(false);
435
500
  const [inputValue, setInputValue] = useState2("");
436
- const hoverTimeoutRef = useRef2(null);
501
+ const hoverTimeoutRef = useRef3(null);
502
+ const pointerDownPos = useRef3(null);
503
+ const isClickRef = useRef3(true);
504
+ const animRef = useRef3(null);
505
+ const wrapperRectRef = useRef3(null);
506
+ const scaleRef = useRef3(1);
437
507
  const percentage = (value - min) / (max - min) * 100;
438
- const isAtMinimum = value <= min;
439
508
  const isActive = isInteracting || isHovered;
509
+ const fillPercent = useMotionValue(percentage);
510
+ const fillWidth = useTransform(fillPercent, (pct) => `${pct}%`);
511
+ const handleLeft = useTransform(
512
+ fillPercent,
513
+ (pct) => `max(5px, calc(${pct}% - 9px))`
514
+ );
515
+ const rubberStretchPx = useMotionValue(0);
516
+ const rubberBandWidth = useTransform(
517
+ rubberStretchPx,
518
+ (stretch) => `calc(100% + ${Math.abs(stretch)}px)`
519
+ );
520
+ const rubberBandX = useTransform(
521
+ rubberStretchPx,
522
+ (stretch) => stretch < 0 ? stretch : 0
523
+ );
524
+ useEffect3(() => {
525
+ if (!isInteracting && !animRef.current) {
526
+ fillPercent.jump(percentage);
527
+ }
528
+ }, [percentage, isInteracting, fillPercent]);
440
529
  const positionToValue = useCallback(
441
530
  (clientX) => {
442
- if (!trackRef.current) return value;
443
- const rect = trackRef.current.getBoundingClientRect();
444
- const x = clientX - rect.left;
445
- const percent = Math.max(0, Math.min(1, x / rect.width));
531
+ const rect = wrapperRectRef.current;
532
+ if (!rect) return value;
533
+ const screenX = clientX - rect.left;
534
+ const sceneX = screenX / scaleRef.current;
535
+ const nativeWidth = wrapperRef.current ? wrapperRef.current.offsetWidth : rect.width;
536
+ const percent = Math.max(0, Math.min(1, sceneX / nativeWidth));
446
537
  const rawValue = min + percent * (max - min);
447
538
  return Math.max(min, Math.min(max, rawValue));
448
539
  },
449
540
  [min, max, value]
450
541
  );
451
- const handleMouseDown = useCallback(
452
- (e) => {
453
- if (showInput) return;
454
- e.preventDefault();
455
- setIsInteracting(true);
456
- onChange(positionToValue(e.clientX));
457
- },
458
- [positionToValue, onChange, showInput]
542
+ const percentFromValue = useCallback(
543
+ (v) => (v - min) / (max - min) * 100,
544
+ [min, max]
459
545
  );
460
- const handleMouseMove = useCallback(
461
- (e) => {
462
- if (!isInteracting) return;
463
- if (!isDragging) setIsDragging(true);
464
- onChange(positionToValue(e.clientX));
546
+ const computeRubberStretch = useCallback(
547
+ (clientX, sign) => {
548
+ const rect = wrapperRectRef.current;
549
+ if (!rect) return 0;
550
+ const distancePast = sign < 0 ? rect.left - clientX : clientX - rect.right;
551
+ const overflow = Math.max(0, distancePast - DEAD_ZONE);
552
+ return sign * MAX_STRETCH * Math.sqrt(Math.min(overflow / MAX_CURSOR_RANGE, 1));
465
553
  },
466
- [isInteracting, isDragging, positionToValue, onChange]
554
+ []
467
555
  );
468
- const handleMouseUp = useCallback(() => {
469
- setIsInteracting(false);
470
- setIsDragging(false);
471
- }, []);
472
- const handleTouchStart = useCallback(
556
+ const handlePointerDown = useCallback(
473
557
  (e) => {
474
558
  if (showInput) return;
475
559
  e.preventDefault();
560
+ e.target.setPointerCapture(e.pointerId);
561
+ pointerDownPos.current = { x: e.clientX, y: e.clientY };
562
+ isClickRef.current = true;
476
563
  setIsInteracting(true);
477
- const touch = e.touches[0];
478
- onChange(positionToValue(touch.clientX));
564
+ if (wrapperRef.current) {
565
+ wrapperRectRef.current = wrapperRef.current.getBoundingClientRect();
566
+ const nativeWidth = wrapperRef.current.offsetWidth;
567
+ scaleRef.current = wrapperRectRef.current.width / nativeWidth;
568
+ }
569
+ },
570
+ [showInput]
571
+ );
572
+ const handlePointerMove = useCallback(
573
+ (e) => {
574
+ if (!isInteracting || !pointerDownPos.current) return;
575
+ const dx = e.clientX - pointerDownPos.current.x;
576
+ const dy = e.clientY - pointerDownPos.current.y;
577
+ const distance = Math.sqrt(dx * dx + dy * dy);
578
+ if (isClickRef.current && distance > CLICK_THRESHOLD) {
579
+ isClickRef.current = false;
580
+ setIsDragging(true);
581
+ }
582
+ if (!isClickRef.current) {
583
+ const rect = wrapperRectRef.current;
584
+ if (rect) {
585
+ if (e.clientX < rect.left) {
586
+ rubberStretchPx.jump(computeRubberStretch(e.clientX, -1));
587
+ } else if (e.clientX > rect.right) {
588
+ rubberStretchPx.jump(computeRubberStretch(e.clientX, 1));
589
+ } else {
590
+ rubberStretchPx.jump(0);
591
+ }
592
+ }
593
+ const newValue = positionToValue(e.clientX);
594
+ const newPct = percentFromValue(newValue);
595
+ if (animRef.current) {
596
+ animRef.current.stop();
597
+ animRef.current = null;
598
+ }
599
+ fillPercent.jump(newPct);
600
+ onChange(roundValue(newValue, step));
601
+ }
479
602
  },
480
- [positionToValue, onChange, showInput]
603
+ [
604
+ isInteracting,
605
+ positionToValue,
606
+ percentFromValue,
607
+ onChange,
608
+ fillPercent,
609
+ rubberStretchPx,
610
+ computeRubberStretch
611
+ ]
481
612
  );
482
- const handleTouchMove = useCallback(
613
+ const handlePointerUp = useCallback(
483
614
  (e) => {
484
615
  if (!isInteracting) return;
485
- if (!isDragging) setIsDragging(true);
486
- const touch = e.touches[0];
487
- onChange(positionToValue(touch.clientX));
616
+ if (isClickRef.current) {
617
+ const rawValue = positionToValue(e.clientX);
618
+ const snappedValue = snapToDecile(rawValue, min, max);
619
+ const newPct = percentFromValue(snappedValue);
620
+ if (animRef.current) {
621
+ animRef.current.stop();
622
+ }
623
+ animRef.current = animate(fillPercent, newPct, {
624
+ type: "spring",
625
+ stiffness: 300,
626
+ damping: 25,
627
+ mass: 0.8,
628
+ onComplete: () => {
629
+ animRef.current = null;
630
+ }
631
+ });
632
+ onChange(roundValue(snappedValue, step));
633
+ }
634
+ if (rubberStretchPx.get() !== 0) {
635
+ animate(rubberStretchPx, 0, {
636
+ type: "spring",
637
+ visualDuration: 0.35,
638
+ bounce: 0.15
639
+ });
640
+ }
641
+ setIsInteracting(false);
642
+ setIsDragging(false);
643
+ pointerDownPos.current = null;
488
644
  },
489
- [isInteracting, isDragging, positionToValue, onChange]
645
+ [
646
+ isInteracting,
647
+ positionToValue,
648
+ percentFromValue,
649
+ onChange,
650
+ min,
651
+ max,
652
+ fillPercent,
653
+ rubberStretchPx
654
+ ]
490
655
  );
491
- const handleTouchEnd = useCallback(() => {
492
- setIsInteracting(false);
493
- setIsDragging(false);
494
- }, []);
495
- useEffect2(() => {
496
- if (isInteracting) {
497
- document.addEventListener("mousemove", handleMouseMove);
498
- document.addEventListener("mouseup", handleMouseUp);
499
- document.addEventListener("touchmove", handleTouchMove, { passive: false });
500
- document.addEventListener("touchend", handleTouchEnd);
501
- return () => {
502
- document.removeEventListener("mousemove", handleMouseMove);
503
- document.removeEventListener("mouseup", handleMouseUp);
504
- document.removeEventListener("touchmove", handleTouchMove);
505
- document.removeEventListener("touchend", handleTouchEnd);
506
- };
507
- }
508
- }, [isInteracting, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
509
- useEffect2(() => {
656
+ useEffect3(() => {
510
657
  if (isValueHovered && !showInput && !isValueEditable) {
511
658
  hoverTimeoutRef.current = setTimeout(() => {
512
659
  setIsValueEditable(true);
@@ -524,7 +671,7 @@ function Slider({
524
671
  }
525
672
  };
526
673
  }, [isValueHovered, showInput, isValueEditable]);
527
- useEffect2(() => {
674
+ useEffect3(() => {
528
675
  if (showInput && inputRef.current) {
529
676
  inputRef.current.focus();
530
677
  inputRef.current.select();
@@ -537,7 +684,7 @@ function Slider({
537
684
  const parsed = parseFloat(inputValue);
538
685
  if (!isNaN(parsed)) {
539
686
  const clamped = Math.max(min, Math.min(max, parsed));
540
- onChange(clamped);
687
+ onChange(roundValue(clamped, step));
541
688
  }
542
689
  setShowInput(false);
543
690
  setIsValueHovered(false);
@@ -563,29 +710,56 @@ function Slider({
563
710
  handleInputSubmit();
564
711
  };
565
712
  const displayValue = step >= 1 ? value.toFixed(0) : value.toFixed(2);
566
- const shouldAnimate = !isDragging;
567
- const transition = shouldAnimate ? { type: "spring", visualDuration: 0.3, bounce: 0.15 } : { duration: 0 };
568
- const handleLeft = isAtMinimum ? 7 : `calc(${percentage}% - 9px)`;
713
+ const HANDLE_BUFFER = 8;
714
+ const LABEL_CSS_LEFT = 10;
715
+ const VALUE_CSS_RIGHT = 10;
716
+ let leftThreshold = 30;
717
+ let rightThreshold = 78;
718
+ const trackWidth = wrapperRef.current?.offsetWidth;
719
+ if (trackWidth && trackWidth > 0) {
720
+ if (labelRef.current) {
721
+ leftThreshold = (LABEL_CSS_LEFT + labelRef.current.offsetWidth + HANDLE_BUFFER) / trackWidth * 100;
722
+ }
723
+ if (valueSpanRef.current) {
724
+ rightThreshold = (trackWidth - VALUE_CSS_RIGHT - valueSpanRef.current.offsetWidth - HANDLE_BUFFER) / trackWidth * 100;
725
+ }
726
+ }
727
+ const valueDodge = percentage < leftThreshold || percentage > rightThreshold;
728
+ const handleOpacity = !isActive ? 0 : valueDodge ? 0.1 : isDragging ? 0.9 : 0.5;
569
729
  const fillBackground = isActive ? "rgba(255, 255, 255, 0.15)" : "rgba(255, 255, 255, 0.11)";
570
- const hashMarks = Array.from({ length: 7 }, (_, i) => /* @__PURE__ */ jsx2("span", { className: "dialkit-slider-hashmark", children: "\u2022" }, i));
571
- return /* @__PURE__ */ jsxs2(
572
- "div",
730
+ const hashMarks = Array.from({ length: 9 }, (_, i) => {
731
+ const pct = (i + 1) * 10;
732
+ return /* @__PURE__ */ jsx2(
733
+ "div",
734
+ {
735
+ className: "dialkit-slider-hashmark",
736
+ style: { left: `${pct}%` }
737
+ },
738
+ i
739
+ );
740
+ });
741
+ return /* @__PURE__ */ jsx2("div", { ref: wrapperRef, className: "dialkit-slider-wrapper", children: /* @__PURE__ */ jsxs2(
742
+ motion2.div,
573
743
  {
574
744
  ref: trackRef,
575
745
  className: `dialkit-slider ${isActive ? "dialkit-slider-active" : ""}`,
576
- onMouseDown: handleMouseDown,
577
- onTouchStart: handleTouchStart,
746
+ onPointerDown: handlePointerDown,
747
+ onPointerMove: handlePointerMove,
748
+ onPointerUp: handlePointerUp,
578
749
  onMouseEnter: () => setIsHovered(true),
579
750
  onMouseLeave: () => setIsHovered(false),
751
+ style: { width: rubberBandWidth, x: rubberBandX },
580
752
  children: [
581
753
  /* @__PURE__ */ jsx2("div", { className: "dialkit-slider-hashmarks", children: hashMarks }),
582
754
  /* @__PURE__ */ jsx2(
583
755
  motion2.div,
584
756
  {
585
757
  className: "dialkit-slider-fill",
586
- style: { background: fillBackground, width: `${percentage}%`, transition: "background 0.15s" },
587
- animate: { width: `${percentage}%` },
588
- transition
758
+ style: {
759
+ background: fillBackground,
760
+ width: fillWidth,
761
+ transition: "background 0.15s"
762
+ }
589
763
  }
590
764
  ),
591
765
  /* @__PURE__ */ jsx2(
@@ -593,15 +767,23 @@ function Slider({
593
767
  {
594
768
  className: "dialkit-slider-handle",
595
769
  style: {
596
- background: isInteracting ? "rgba(255, 255, 255, 1)" : "rgba(255, 255, 255, 0.5)",
597
770
  left: handleLeft,
598
- opacity: isActive ? 1 : 0
771
+ y: "-50%",
772
+ background: "rgba(255, 255, 255, 0.9)"
599
773
  },
600
- animate: { left: handleLeft, opacity: isActive ? 1 : 0 },
601
- transition
774
+ animate: {
775
+ opacity: handleOpacity,
776
+ scaleX: isActive ? 1 : 0.25,
777
+ scaleY: isActive && valueDodge ? 0.75 : 1
778
+ },
779
+ transition: {
780
+ scaleX: { type: "spring", visualDuration: 0.25, bounce: 0.15 },
781
+ scaleY: { type: "spring", visualDuration: 0.2, bounce: 0.1 },
782
+ opacity: { duration: 0.15 }
783
+ }
602
784
  }
603
785
  ),
604
- /* @__PURE__ */ jsx2("span", { className: "dialkit-slider-label", children: label }),
786
+ /* @__PURE__ */ jsx2("span", { ref: labelRef, className: "dialkit-slider-label", children: label }),
605
787
  showInput ? /* @__PURE__ */ jsx2(
606
788
  "input",
607
789
  {
@@ -615,28 +797,26 @@ function Slider({
615
797
  onClick: (e) => e.stopPropagation(),
616
798
  onMouseDown: (e) => e.stopPropagation()
617
799
  }
618
- ) : /* @__PURE__ */ jsxs2(
800
+ ) : /* @__PURE__ */ jsx2(
619
801
  "span",
620
802
  {
803
+ ref: valueSpanRef,
621
804
  className: `dialkit-slider-value ${isValueEditable ? "dialkit-slider-value-editable" : ""}`,
622
805
  onMouseEnter: () => setIsValueHovered(true),
623
806
  onMouseLeave: () => setIsValueHovered(false),
624
807
  onClick: handleValueClick,
625
808
  onMouseDown: (e) => isValueEditable && e.stopPropagation(),
626
809
  style: { cursor: isValueEditable ? "text" : "default" },
627
- children: [
628
- displayValue,
629
- unit
630
- ]
810
+ children: displayValue
631
811
  }
632
812
  )
633
813
  ]
634
814
  }
635
- );
815
+ ) });
636
816
  }
637
817
 
638
818
  // src/components/SegmentedControl.tsx
639
- import { useRef as useRef3, useState as useState3, useLayoutEffect } from "react";
819
+ import { useRef as useRef4, useState as useState3, useLayoutEffect } from "react";
640
820
  import { motion as motion3 } from "motion/react";
641
821
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
642
822
  function SegmentedControl({
@@ -644,10 +824,10 @@ function SegmentedControl({
644
824
  value,
645
825
  onChange
646
826
  }) {
647
- const containerRef = useRef3(null);
648
- const buttonRefs = useRef3(/* @__PURE__ */ new Map());
827
+ const containerRef = useRef4(null);
828
+ const buttonRefs = useRef4(/* @__PURE__ */ new Map());
649
829
  const [pillStyle, setPillStyle] = useState3(null);
650
- const hasAnimated = useRef3(false);
830
+ const hasAnimated = useRef4(false);
651
831
  useLayoutEffect(() => {
652
832
  const button = buttonRefs.current.get(value);
653
833
  const container = containerRef.current;
@@ -917,20 +1097,352 @@ function SpringControl({ panelId, path, label, spring, onChange }) {
917
1097
  ] }) });
918
1098
  }
919
1099
 
920
- // src/components/Panel.tsx
921
- import { useSyncExternalStore as useSyncExternalStore3 } from "react";
1100
+ // src/components/TextControl.tsx
922
1101
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1102
+ function TextControl({ label, value, onChange, placeholder }) {
1103
+ return /* @__PURE__ */ jsxs7("div", { className: "dialkit-text-control", children: [
1104
+ /* @__PURE__ */ jsx7("label", { className: "dialkit-text-label", children: label }),
1105
+ /* @__PURE__ */ jsx7(
1106
+ "input",
1107
+ {
1108
+ type: "text",
1109
+ className: "dialkit-text-input",
1110
+ value,
1111
+ onChange: (e) => onChange(e.target.value),
1112
+ placeholder
1113
+ }
1114
+ )
1115
+ ] });
1116
+ }
1117
+
1118
+ // src/components/SelectControl.tsx
1119
+ import { useState as useState4, useRef as useRef5, useEffect as useEffect4 } from "react";
1120
+ import { motion as motion4, AnimatePresence as AnimatePresence2 } from "motion/react";
1121
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1122
+ function toTitleCase(s) {
1123
+ return s.replace(/\b\w/g, (c) => c.toUpperCase());
1124
+ }
1125
+ function normalizeOptions(options) {
1126
+ return options.map(
1127
+ (opt) => typeof opt === "string" ? { value: opt, label: toTitleCase(opt) } : opt
1128
+ );
1129
+ }
1130
+ function SelectControl({ label, value, options, onChange }) {
1131
+ const [isOpen, setIsOpen] = useState4(false);
1132
+ const containerRef = useRef5(null);
1133
+ const normalized = normalizeOptions(options);
1134
+ const selectedOption = normalized.find((o) => o.value === value);
1135
+ useEffect4(() => {
1136
+ if (!isOpen) return;
1137
+ const handleClick = (e) => {
1138
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
1139
+ setIsOpen(false);
1140
+ }
1141
+ };
1142
+ document.addEventListener("mousedown", handleClick);
1143
+ return () => document.removeEventListener("mousedown", handleClick);
1144
+ }, [isOpen]);
1145
+ return /* @__PURE__ */ jsxs8("div", { ref: containerRef, className: "dialkit-select-row", children: [
1146
+ /* @__PURE__ */ jsxs8(
1147
+ "button",
1148
+ {
1149
+ className: "dialkit-select-trigger",
1150
+ onClick: () => setIsOpen(!isOpen),
1151
+ "data-open": String(isOpen),
1152
+ children: [
1153
+ /* @__PURE__ */ jsx8("span", { className: "dialkit-select-label", children: label }),
1154
+ /* @__PURE__ */ jsxs8("div", { className: "dialkit-select-right", children: [
1155
+ /* @__PURE__ */ jsx8("span", { className: "dialkit-select-value", children: selectedOption?.label ?? value }),
1156
+ /* @__PURE__ */ jsx8(
1157
+ motion4.svg,
1158
+ {
1159
+ className: "dialkit-select-chevron",
1160
+ viewBox: "0 0 24 24",
1161
+ fill: "none",
1162
+ stroke: "currentColor",
1163
+ strokeWidth: "2.5",
1164
+ strokeLinecap: "round",
1165
+ strokeLinejoin: "round",
1166
+ animate: { rotate: isOpen ? 180 : 0 },
1167
+ transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
1168
+ children: /* @__PURE__ */ jsx8("path", { d: "M6 9.5L12 15.5L18 9.5" })
1169
+ }
1170
+ )
1171
+ ] })
1172
+ ]
1173
+ }
1174
+ ),
1175
+ /* @__PURE__ */ jsx8(AnimatePresence2, { children: isOpen && /* @__PURE__ */ jsx8(
1176
+ motion4.div,
1177
+ {
1178
+ className: "dialkit-select-dropdown",
1179
+ initial: { opacity: 0, y: -8, scale: 0.95 },
1180
+ animate: { opacity: 1, y: 0, scale: 1 },
1181
+ exit: { opacity: 0, y: -8, scale: 0.95 },
1182
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1183
+ children: normalized.map((option) => /* @__PURE__ */ jsx8(
1184
+ "button",
1185
+ {
1186
+ className: "dialkit-select-option",
1187
+ "data-selected": String(option.value === value),
1188
+ onClick: () => {
1189
+ onChange(option.value);
1190
+ setIsOpen(false);
1191
+ },
1192
+ children: option.label
1193
+ },
1194
+ option.value
1195
+ ))
1196
+ }
1197
+ ) })
1198
+ ] });
1199
+ }
1200
+
1201
+ // src/components/ColorControl.tsx
1202
+ import { useState as useState5, useRef as useRef6, useEffect as useEffect5 } from "react";
1203
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1204
+ var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
1205
+ function ColorControl({ label, value, onChange }) {
1206
+ const [isEditing, setIsEditing] = useState5(false);
1207
+ const [editValue, setEditValue] = useState5(value);
1208
+ const colorInputRef = useRef6(null);
1209
+ useEffect5(() => {
1210
+ if (!isEditing) {
1211
+ setEditValue(value);
1212
+ }
1213
+ }, [value, isEditing]);
1214
+ function handleTextSubmit() {
1215
+ setIsEditing(false);
1216
+ if (HEX_COLOR_REGEX.test(editValue)) {
1217
+ onChange(editValue);
1218
+ } else {
1219
+ setEditValue(value);
1220
+ }
1221
+ }
1222
+ function handleKeyDown(e) {
1223
+ if (e.key === "Enter") {
1224
+ handleTextSubmit();
1225
+ } else if (e.key === "Escape") {
1226
+ setIsEditing(false);
1227
+ setEditValue(value);
1228
+ }
1229
+ }
1230
+ return /* @__PURE__ */ jsxs9("div", { className: "dialkit-color-control", children: [
1231
+ /* @__PURE__ */ jsx9("span", { className: "dialkit-color-label", children: label }),
1232
+ /* @__PURE__ */ jsxs9("div", { className: "dialkit-color-inputs", children: [
1233
+ isEditing ? /* @__PURE__ */ jsx9(
1234
+ "input",
1235
+ {
1236
+ type: "text",
1237
+ className: "dialkit-color-hex-input",
1238
+ value: editValue,
1239
+ onChange: (e) => setEditValue(e.target.value),
1240
+ onBlur: handleTextSubmit,
1241
+ onKeyDown: handleKeyDown,
1242
+ autoFocus: true
1243
+ }
1244
+ ) : /* @__PURE__ */ jsx9(
1245
+ "span",
1246
+ {
1247
+ className: "dialkit-color-hex",
1248
+ onClick: () => setIsEditing(true),
1249
+ children: (value ?? "").toUpperCase()
1250
+ }
1251
+ ),
1252
+ /* @__PURE__ */ jsx9(
1253
+ "button",
1254
+ {
1255
+ className: "dialkit-color-swatch",
1256
+ style: { backgroundColor: value },
1257
+ onClick: () => colorInputRef.current?.click(),
1258
+ title: "Pick color"
1259
+ }
1260
+ ),
1261
+ /* @__PURE__ */ jsx9(
1262
+ "input",
1263
+ {
1264
+ ref: colorInputRef,
1265
+ type: "color",
1266
+ className: "dialkit-color-picker-native",
1267
+ value: value.length === 4 ? expandShorthandHex(value) : value.slice(0, 7),
1268
+ onChange: (e) => onChange(e.target.value)
1269
+ }
1270
+ )
1271
+ ] })
1272
+ ] });
1273
+ }
1274
+ function expandShorthandHex(hex) {
1275
+ if (hex.length !== 4) return hex;
1276
+ return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
1277
+ }
1278
+
1279
+ // src/components/PresetManager.tsx
1280
+ import { useState as useState6, useRef as useRef7, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1281
+ import { createPortal } from "react-dom";
1282
+ import { motion as motion5, AnimatePresence as AnimatePresence3 } from "motion/react";
1283
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1284
+ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1285
+ const [isOpen, setIsOpen] = useState6(false);
1286
+ const triggerRef = useRef7(null);
1287
+ const dropdownRef = useRef7(null);
1288
+ const [pos, setPos] = useState6({ top: 0, left: 0, width: 0 });
1289
+ const hasPresets = presets.length > 0;
1290
+ const activePreset = presets.find((p) => p.id === activePresetId);
1291
+ const open = useCallback2(() => {
1292
+ if (!hasPresets) return;
1293
+ const rect = triggerRef.current?.getBoundingClientRect();
1294
+ if (rect) {
1295
+ setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
1296
+ }
1297
+ setIsOpen(true);
1298
+ }, [hasPresets]);
1299
+ const close = useCallback2(() => setIsOpen(false), []);
1300
+ const toggle = useCallback2(() => {
1301
+ if (isOpen) close();
1302
+ else open();
1303
+ }, [isOpen, open, close]);
1304
+ useEffect6(() => {
1305
+ if (!isOpen) return;
1306
+ const handler = (e) => {
1307
+ const target = e.target;
1308
+ if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
1309
+ close();
1310
+ };
1311
+ document.addEventListener("mousedown", handler);
1312
+ return () => document.removeEventListener("mousedown", handler);
1313
+ }, [isOpen, close]);
1314
+ const handleSelect = (presetId) => {
1315
+ if (presetId) {
1316
+ DialStore.loadPreset(panelId, presetId);
1317
+ } else {
1318
+ DialStore.clearActivePreset(panelId);
1319
+ }
1320
+ close();
1321
+ };
1322
+ const handleDelete = (e, presetId) => {
1323
+ e.stopPropagation();
1324
+ DialStore.deletePreset(panelId, presetId);
1325
+ };
1326
+ return /* @__PURE__ */ jsxs10("div", { className: "dialkit-preset-manager", children: [
1327
+ /* @__PURE__ */ jsxs10(
1328
+ "button",
1329
+ {
1330
+ ref: triggerRef,
1331
+ className: "dialkit-preset-trigger",
1332
+ onClick: toggle,
1333
+ "data-open": String(isOpen),
1334
+ "data-has-preset": String(!!activePreset),
1335
+ "data-disabled": String(!hasPresets),
1336
+ children: [
1337
+ /* @__PURE__ */ jsx10("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
1338
+ /* @__PURE__ */ jsx10(
1339
+ motion5.svg,
1340
+ {
1341
+ className: "dialkit-select-chevron",
1342
+ viewBox: "0 0 24 24",
1343
+ fill: "none",
1344
+ stroke: "currentColor",
1345
+ strokeWidth: "2.5",
1346
+ strokeLinecap: "round",
1347
+ strokeLinejoin: "round",
1348
+ animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
1349
+ transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
1350
+ children: /* @__PURE__ */ jsx10("path", { d: "M6 9.5L12 15.5L18 9.5" })
1351
+ }
1352
+ )
1353
+ ]
1354
+ }
1355
+ ),
1356
+ createPortal(
1357
+ /* @__PURE__ */ jsx10(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs10(
1358
+ motion5.div,
1359
+ {
1360
+ ref: dropdownRef,
1361
+ className: "dialkit-preset-dropdown",
1362
+ style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
1363
+ initial: { opacity: 0, y: 4, scale: 0.97 },
1364
+ animate: { opacity: 1, y: 0, scale: 1 },
1365
+ exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
1366
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1367
+ children: [
1368
+ /* @__PURE__ */ jsx10(
1369
+ "div",
1370
+ {
1371
+ className: "dialkit-preset-item",
1372
+ "data-active": String(!activePresetId),
1373
+ onClick: () => handleSelect(null),
1374
+ children: /* @__PURE__ */ jsx10("span", { className: "dialkit-preset-name", children: "Version 1" })
1375
+ }
1376
+ ),
1377
+ presets.map((preset) => /* @__PURE__ */ jsxs10(
1378
+ "div",
1379
+ {
1380
+ className: "dialkit-preset-item",
1381
+ "data-active": String(preset.id === activePresetId),
1382
+ onClick: () => handleSelect(preset.id),
1383
+ children: [
1384
+ /* @__PURE__ */ jsx10("span", { className: "dialkit-preset-name", children: preset.name }),
1385
+ /* @__PURE__ */ jsx10(
1386
+ "button",
1387
+ {
1388
+ className: "dialkit-preset-delete",
1389
+ onClick: (e) => handleDelete(e, preset.id),
1390
+ title: "Delete preset",
1391
+ children: /* @__PURE__ */ jsxs10("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1392
+ /* @__PURE__ */ jsx10("path", { d: "M5 6.5L5.80734 18.2064C5.91582 19.7794 7.22348 21 8.80023 21H15.1998C16.7765 21 18.0842 19.7794 18.1927 18.2064L19 6.5" }),
1393
+ /* @__PURE__ */ jsx10("path", { d: "M10 11V16" }),
1394
+ /* @__PURE__ */ jsx10("path", { d: "M14 11V16" }),
1395
+ /* @__PURE__ */ jsx10("path", { d: "M3.5 6H20.5" }),
1396
+ /* @__PURE__ */ jsx10("path", { d: "M8.07092 5.74621C8.42348 3.89745 10.0485 2.5 12 2.5C13.9515 2.5 15.5765 3.89745 15.9291 5.74621" })
1397
+ ] })
1398
+ }
1399
+ )
1400
+ ]
1401
+ },
1402
+ preset.id
1403
+ ))
1404
+ ]
1405
+ }
1406
+ ) }),
1407
+ document.body
1408
+ )
1409
+ ] });
1410
+ }
1411
+
1412
+ // src/components/Panel.tsx
1413
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
923
1414
  function Panel({ panel }) {
1415
+ const [copied, setCopied] = useState7(false);
1416
+ const [isPanelOpen, setIsPanelOpen] = useState7(true);
924
1417
  const values = useSyncExternalStore3(
925
1418
  (cb) => DialStore.subscribe(panel.id, cb),
926
1419
  () => DialStore.getValues(panel.id),
927
1420
  () => DialStore.getValues(panel.id)
928
1421
  );
1422
+ const presets = DialStore.getPresets(panel.id);
1423
+ const activePresetId = DialStore.getActivePresetId(panel.id);
1424
+ const handleAddPreset = () => {
1425
+ const nextNum = presets.length + 2;
1426
+ DialStore.savePreset(panel.id, `Version ${nextNum}`);
1427
+ };
1428
+ const handleCopy = () => {
1429
+ const jsonStr = JSON.stringify(values, null, 2);
1430
+ const instruction = `Update the useDialKit configuration for "${panel.name}" with these values:
1431
+
1432
+ \`\`\`json
1433
+ ${jsonStr}
1434
+ \`\`\`
1435
+
1436
+ Apply these values as the new defaults in the useDialKit call.`;
1437
+ navigator.clipboard.writeText(instruction);
1438
+ setCopied(true);
1439
+ setTimeout(() => setCopied(false), 1500);
1440
+ };
929
1441
  const renderControl = (control) => {
930
1442
  const value = values[control.path];
931
1443
  switch (control.type) {
932
1444
  case "slider":
933
- return /* @__PURE__ */ jsx7(
1445
+ return /* @__PURE__ */ jsx11(
934
1446
  Slider,
935
1447
  {
936
1448
  label: control.label,
@@ -943,7 +1455,7 @@ function Panel({ panel }) {
943
1455
  control.path
944
1456
  );
945
1457
  case "toggle":
946
- return /* @__PURE__ */ jsx7(
1458
+ return /* @__PURE__ */ jsx11(
947
1459
  Toggle,
948
1460
  {
949
1461
  label: control.label,
@@ -953,7 +1465,7 @@ function Panel({ panel }) {
953
1465
  control.path
954
1466
  );
955
1467
  case "spring":
956
- return /* @__PURE__ */ jsx7(
1468
+ return /* @__PURE__ */ jsx11(
957
1469
  SpringControl,
958
1470
  {
959
1471
  panelId: panel.id,
@@ -965,7 +1477,39 @@ function Panel({ panel }) {
965
1477
  control.path
966
1478
  );
967
1479
  case "folder":
968
- return /* @__PURE__ */ jsx7(Folder, { title: control.label, children: control.children?.map(renderControl) }, control.path);
1480
+ return /* @__PURE__ */ jsx11(Folder, { title: control.label, children: control.children?.map(renderControl) }, control.path);
1481
+ case "text":
1482
+ return /* @__PURE__ */ jsx11(
1483
+ TextControl,
1484
+ {
1485
+ label: control.label,
1486
+ value,
1487
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
1488
+ placeholder: control.placeholder
1489
+ },
1490
+ control.path
1491
+ );
1492
+ case "select":
1493
+ return /* @__PURE__ */ jsx11(
1494
+ SelectControl,
1495
+ {
1496
+ label: control.label,
1497
+ value,
1498
+ options: control.options ?? [],
1499
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
1500
+ },
1501
+ control.path
1502
+ );
1503
+ case "color":
1504
+ return /* @__PURE__ */ jsx11(
1505
+ ColorControl,
1506
+ {
1507
+ label: control.label,
1508
+ value,
1509
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
1510
+ },
1511
+ control.path
1512
+ );
969
1513
  default:
970
1514
  return null;
971
1515
  }
@@ -976,24 +1520,16 @@ function Panel({ panel }) {
976
1520
  while (i < panel.controls.length) {
977
1521
  const control = panel.controls[i];
978
1522
  if (control.type === "action") {
979
- const actions = [control];
980
- while (i + 1 < panel.controls.length && panel.controls[i + 1].type === "action") {
981
- i++;
982
- actions.push(panel.controls[i]);
983
- }
984
1523
  result.push(
985
- /* @__PURE__ */ jsxs7("div", { className: "dialkit-labeled-control dialkit-actions-group", children: [
986
- /* @__PURE__ */ jsx7("span", { className: "dialkit-labeled-control-label", children: "Actions" }),
987
- /* @__PURE__ */ jsx7("div", { className: "dialkit-actions-stack", children: actions.map((action) => /* @__PURE__ */ jsx7(
988
- "button",
989
- {
990
- className: "dialkit-action-button",
991
- onClick: () => DialStore.triggerAction(panel.id, action.path),
992
- children: action.label
993
- },
994
- action.path
995
- )) })
996
- ] }, `actions-${actions[0].path}`)
1524
+ /* @__PURE__ */ jsx11(
1525
+ "button",
1526
+ {
1527
+ className: "dialkit-button",
1528
+ onClick: () => DialStore.triggerAction(panel.id, control.path),
1529
+ children: control.label
1530
+ },
1531
+ control.path
1532
+ )
997
1533
  );
998
1534
  } else {
999
1535
  result.push(renderControl(control));
@@ -1002,15 +1538,92 @@ function Panel({ panel }) {
1002
1538
  }
1003
1539
  return result;
1004
1540
  };
1005
- return /* @__PURE__ */ jsx7(Folder, { title: panel.name, defaultOpen: true, isRoot: true, panelId: panel.id, children: renderControls() });
1541
+ const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
1542
+ const toolbar = /* @__PURE__ */ jsxs11(Fragment2, { children: [
1543
+ /* @__PURE__ */ jsx11(
1544
+ motion6.button,
1545
+ {
1546
+ className: "dialkit-toolbar-add",
1547
+ onClick: handleAddPreset,
1548
+ title: "Add preset",
1549
+ whileTap: { scale: 0.9 },
1550
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
1551
+ children: /* @__PURE__ */ jsxs11("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1552
+ /* @__PURE__ */ jsx11("path", { d: "M4 6H20" }),
1553
+ /* @__PURE__ */ jsx11("path", { d: "M4 12H10" }),
1554
+ /* @__PURE__ */ jsx11("path", { d: "M15 15L21 15" }),
1555
+ /* @__PURE__ */ jsx11("path", { d: "M18 12V18" }),
1556
+ /* @__PURE__ */ jsx11("path", { d: "M4 18H10" })
1557
+ ] })
1558
+ }
1559
+ ),
1560
+ /* @__PURE__ */ jsx11(
1561
+ PresetManager,
1562
+ {
1563
+ panelId: panel.id,
1564
+ presets,
1565
+ activePresetId,
1566
+ onAdd: handleAddPreset
1567
+ }
1568
+ ),
1569
+ /* @__PURE__ */ jsxs11(
1570
+ motion6.button,
1571
+ {
1572
+ className: "dialkit-toolbar-copy",
1573
+ onClick: handleCopy,
1574
+ title: "Copy parameters",
1575
+ whileTap: { scale: 0.95 },
1576
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
1577
+ children: [
1578
+ /* @__PURE__ */ jsx11("span", { className: "dialkit-toolbar-copy-icon-wrap", children: /* @__PURE__ */ jsx11(AnimatePresence4, { initial: false, mode: "popLayout", children: copied ? /* @__PURE__ */ jsx11(
1579
+ motion6.svg,
1580
+ {
1581
+ className: "dialkit-toolbar-copy-icon",
1582
+ viewBox: "0 0 24 24",
1583
+ fill: "none",
1584
+ stroke: "currentColor",
1585
+ strokeWidth: "2",
1586
+ strokeLinecap: "round",
1587
+ strokeLinejoin: "round",
1588
+ initial: { scale: 0.5, opacity: 0, filter: "blur(4px)" },
1589
+ animate: { scale: 1, opacity: 1, filter: "blur(0px)" },
1590
+ exit: { scale: 0.5, opacity: 0, filter: "blur(4px)" },
1591
+ transition: { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1592
+ children: /* @__PURE__ */ jsx11("path", { d: "M5 12.75L10 19L19 5" })
1593
+ },
1594
+ "check"
1595
+ ) : /* @__PURE__ */ jsxs11(
1596
+ motion6.svg,
1597
+ {
1598
+ className: "dialkit-toolbar-copy-icon",
1599
+ viewBox: "0 0 24 24",
1600
+ fill: "none",
1601
+ initial: { scale: 0.5, opacity: 0, filter: "blur(4px)" },
1602
+ animate: { scale: 1, opacity: 1, filter: "blur(0px)" },
1603
+ exit: { scale: 0.5, opacity: 0, filter: "blur(4px)" },
1604
+ transition: { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1605
+ children: [
1606
+ /* @__PURE__ */ jsx11("path", { d: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
1607
+ /* @__PURE__ */ jsx11("path", { d: "M19.2405 16.1852L18.5436 14.3733C18.4571 14.1484 18.241 14 18 14C17.759 14 17.5429 14.1484 17.4564 14.3733L16.7595 16.1852C16.658 16.4493 16.4493 16.658 16.1852 16.7595L14.3733 17.4564C14.1484 17.5429 14 17.759 14 18C14 18.241 14.1484 18.4571 14.3733 18.5436L16.1852 19.2405C16.4493 19.342 16.658 19.5507 16.7595 19.8148L17.4564 21.6267C17.5429 21.8516 17.759 22 18 22C18.241 22 18.4571 21.8516 18.5436 21.6267L19.2405 19.8148C19.342 19.5507 19.5507 19.342 19.8148 19.2405L21.6267 18.5436C21.8516 18.4571 22 18.241 22 18C22 17.759 21.8516 17.5429 21.6267 17.4564L19.8148 16.7595C19.5507 16.658 19.342 16.4493 19.2405 16.1852Z", fill: "currentColor" }),
1608
+ /* @__PURE__ */ jsx11("path", { d: "M16 5H17C18.6569 5 20 6.34315 20 8V11M8 5H7C5.34315 5 4 6.34315 4 8V18C4 19.6569 5.34315 21 7 21H12", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
1609
+ ]
1610
+ },
1611
+ "clipboard"
1612
+ ) }) }),
1613
+ "Copy"
1614
+ ]
1615
+ }
1616
+ )
1617
+ ] });
1618
+ return /* @__PURE__ */ jsx11("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx11(Folder, { title: panel.name, defaultOpen: true, isRoot: true, onOpenChange: setIsPanelOpen, toolbar, children: renderControls() }) });
1006
1619
  }
1007
1620
 
1008
1621
  // src/components/DialRoot.tsx
1009
- import { jsx as jsx8 } from "react/jsx-runtime";
1622
+ import { jsx as jsx12 } from "react/jsx-runtime";
1010
1623
  function DialRoot({ position = "top-right" }) {
1011
- const [panels, setPanels] = useState4([]);
1012
- const [mounted, setMounted] = useState4(false);
1013
- useEffect3(() => {
1624
+ const [panels, setPanels] = useState8([]);
1625
+ const [mounted, setMounted] = useState8(false);
1626
+ useEffect7(() => {
1014
1627
  setMounted(true);
1015
1628
  setPanels(DialStore.getPanels());
1016
1629
  const unsubscribe = DialStore.subscribeGlobal(() => {
@@ -1024,14 +1637,14 @@ function DialRoot({ position = "top-right" }) {
1024
1637
  if (panels.length === 0) {
1025
1638
  return null;
1026
1639
  }
1027
- const content = /* @__PURE__ */ jsx8("div", { className: "dialkit-root", children: /* @__PURE__ */ jsx8("div", { className: "dialkit-panel", "data-position": position, children: panels.map((panel) => /* @__PURE__ */ jsx8(Panel, { panel }, panel.id)) }) });
1028
- return createPortal(content, document.body);
1640
+ const content = /* @__PURE__ */ jsx12("div", { className: "dialkit-root", children: /* @__PURE__ */ jsx12("div", { className: "dialkit-panel", "data-position": position, children: panels.map((panel) => /* @__PURE__ */ jsx12(Panel, { panel }, panel.id)) }) });
1641
+ return createPortal2(content, document.body);
1029
1642
  }
1030
1643
 
1031
1644
  // src/components/ButtonGroup.tsx
1032
- import { jsx as jsx9 } from "react/jsx-runtime";
1645
+ import { jsx as jsx13 } from "react/jsx-runtime";
1033
1646
  function ButtonGroup({ buttons }) {
1034
- return /* @__PURE__ */ jsx9("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx9(
1647
+ return /* @__PURE__ */ jsx13("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx13(
1035
1648
  "button",
1036
1649
  {
1037
1650
  className: "dialkit-button",
@@ -1043,13 +1656,16 @@ function ButtonGroup({ buttons }) {
1043
1656
  }
1044
1657
  export {
1045
1658
  ButtonGroup,
1659
+ ColorControl,
1046
1660
  DialRoot,
1047
1661
  DialStore,
1048
1662
  Folder,
1049
- SegmentedControl,
1663
+ PresetManager,
1664
+ SelectControl,
1050
1665
  Slider,
1051
1666
  SpringControl,
1052
1667
  SpringVisualization,
1668
+ TextControl,
1053
1669
  Toggle,
1054
1670
  useDialKit
1055
1671
  };