dialkit 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,15 +19,67 @@ var DialStoreClass = class {
19
19
  registerPanel(id, name, config) {
20
20
  const controls = this.parseConfig(config, "");
21
21
  const values = this.flattenValues(config, "");
22
+ this.initTransitionModes(config, "", values);
22
23
  this.panels.set(id, { id, name, controls, values });
23
24
  this.snapshots.set(id, { ...values });
24
25
  this.baseValues.set(id, { ...values });
25
26
  this.notifyGlobal();
26
27
  }
28
+ updatePanel(id, name, config) {
29
+ const existing = this.panels.get(id);
30
+ if (!existing) {
31
+ this.registerPanel(id, name, config);
32
+ return;
33
+ }
34
+ const controls = this.parseConfig(config, "");
35
+ const controlsByPath = this.mapControlsByPath(controls);
36
+ const defaultValues = this.flattenValues(config, "");
37
+ const nextValues = {};
38
+ for (const [path, defaultValue] of Object.entries(defaultValues)) {
39
+ nextValues[path] = this.normalizePreservedValue(
40
+ existing.values[path],
41
+ defaultValue,
42
+ controlsByPath.get(path)
43
+ );
44
+ }
45
+ this.initTransitionModes(config, "", nextValues);
46
+ for (const [path, mode] of Object.entries(existing.values)) {
47
+ if (!path.endsWith(".__mode")) {
48
+ continue;
49
+ }
50
+ const transitionPath = path.slice(0, -"__mode".length - 1);
51
+ const transitionControl = controlsByPath.get(transitionPath);
52
+ if (transitionControl?.type === "transition") {
53
+ nextValues[path] = mode;
54
+ }
55
+ }
56
+ const nextPanel = { id, name, controls, values: nextValues };
57
+ this.panels.set(id, nextPanel);
58
+ this.snapshots.set(id, { ...nextValues });
59
+ const previousBaseValues = this.baseValues.get(id) ?? {};
60
+ const nextBaseValues = {};
61
+ for (const [path, defaultValue] of Object.entries(defaultValues)) {
62
+ nextBaseValues[path] = this.normalizePreservedValue(
63
+ previousBaseValues[path],
64
+ defaultValue,
65
+ controlsByPath.get(path)
66
+ );
67
+ }
68
+ for (const [path, value] of Object.entries(nextValues)) {
69
+ if (path.endsWith(".__mode")) {
70
+ nextBaseValues[path] = value;
71
+ }
72
+ }
73
+ this.baseValues.set(id, nextBaseValues);
74
+ this.notify(id);
75
+ this.notifyGlobal();
76
+ }
27
77
  unregisterPanel(id) {
28
78
  this.panels.delete(id);
29
79
  this.listeners.delete(id);
30
80
  this.snapshots.delete(id);
81
+ this.actionListeners.delete(id);
82
+ this.baseValues.delete(id);
31
83
  this.notifyGlobal();
32
84
  }
33
85
  updateValue(panelId, path, value) {
@@ -47,13 +99,21 @@ var DialStoreClass = class {
47
99
  this.notify(panelId);
48
100
  }
49
101
  updateSpringMode(panelId, path, mode) {
102
+ this.updateTransitionMode(panelId, path, mode);
103
+ }
104
+ getSpringMode(panelId, path) {
105
+ const mode = this.getTransitionMode(panelId, path);
106
+ if (mode === "easing") return "simple";
107
+ return mode;
108
+ }
109
+ updateTransitionMode(panelId, path, mode) {
50
110
  const panel = this.panels.get(panelId);
51
111
  if (!panel) return;
52
112
  panel.values[`${path}.__mode`] = mode;
53
113
  this.snapshots.set(panelId, { ...panel.values });
54
114
  this.notify(panelId);
55
115
  }
56
- getSpringMode(panelId, path) {
116
+ getTransitionMode(panelId, path) {
57
117
  const panel = this.panels.get(panelId);
58
118
  if (!panel) return "simple";
59
119
  return panel.values[`${path}.__mode`] || "simple";
@@ -157,27 +217,43 @@ var DialStoreClass = class {
157
217
  notifyGlobal() {
158
218
  this.globalListeners.forEach((fn) => fn());
159
219
  }
220
+ initTransitionModes(config, prefix, values) {
221
+ for (const [key, value] of Object.entries(config)) {
222
+ if (key === "_collapsed") continue;
223
+ const path = prefix ? `${prefix}.${key}` : key;
224
+ if (this.isEasingConfig(value)) {
225
+ values[`${path}.__mode`] = "easing";
226
+ } else if (this.isSpringConfig(value)) {
227
+ const hasPhysics = value.stiffness !== void 0 || value.damping !== void 0 || value.mass !== void 0;
228
+ const hasTime = value.visualDuration !== void 0 || value.bounce !== void 0;
229
+ values[`${path}.__mode`] = hasPhysics && !hasTime ? "advanced" : "simple";
230
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value) && !this.isActionConfig(value) && !this.isSelectConfig(value) && !this.isColorConfig(value) && !this.isTextConfig(value)) {
231
+ this.initTransitionModes(value, path, values);
232
+ }
233
+ }
234
+ }
160
235
  parseConfig(config, prefix) {
161
236
  const controls = [];
162
237
  for (const [key, value] of Object.entries(config)) {
238
+ if (key === "_collapsed") continue;
163
239
  const path = prefix ? `${prefix}.${key}` : key;
164
240
  const label = this.formatLabel(key);
165
- if (Array.isArray(value) && value.length === 3 && typeof value[0] === "number") {
241
+ if (Array.isArray(value) && value.length <= 4 && typeof value[0] === "number") {
166
242
  controls.push({
167
243
  type: "slider",
168
244
  path,
169
245
  label,
170
246
  min: value[1],
171
247
  max: value[2],
172
- step: this.inferStep(value[1], value[2])
248
+ step: value[3] ?? this.inferStep(value[1], value[2])
173
249
  });
174
250
  } else if (typeof value === "number") {
175
251
  const { min, max, step } = this.inferRange(value);
176
252
  controls.push({ type: "slider", path, label, min, max, step });
177
253
  } else if (typeof value === "boolean") {
178
254
  controls.push({ type: "toggle", path, label });
179
- } else if (this.isSpringConfig(value)) {
180
- controls.push({ type: "spring", path, label });
255
+ } else if (this.isSpringConfig(value) || this.isEasingConfig(value)) {
256
+ controls.push({ type: "transition", path, label });
181
257
  } else if (this.isActionConfig(value)) {
182
258
  controls.push({ type: "action", path, label: value.label || label });
183
259
  } else if (this.isSelectConfig(value)) {
@@ -193,11 +269,14 @@ var DialStoreClass = class {
193
269
  controls.push({ type: "text", path, label });
194
270
  }
195
271
  } else if (typeof value === "object" && value !== null) {
272
+ const folderConfig = value;
273
+ const defaultOpen = "_collapsed" in folderConfig ? !folderConfig._collapsed : true;
196
274
  controls.push({
197
275
  type: "folder",
198
276
  path,
199
277
  label,
200
- children: this.parseConfig(value, path)
278
+ defaultOpen,
279
+ children: this.parseConfig(folderConfig, path)
201
280
  });
202
281
  }
203
282
  }
@@ -206,12 +285,13 @@ var DialStoreClass = class {
206
285
  flattenValues(config, prefix) {
207
286
  const values = {};
208
287
  for (const [key, value] of Object.entries(config)) {
288
+ if (key === "_collapsed") continue;
209
289
  const path = prefix ? `${prefix}.${key}` : key;
210
- if (Array.isArray(value) && value.length === 3 && typeof value[0] === "number") {
290
+ if (Array.isArray(value) && value.length <= 4 && typeof value[0] === "number") {
211
291
  values[path] = value[0];
212
292
  } else if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
213
293
  values[path] = value;
214
- } else if (this.isSpringConfig(value)) {
294
+ } else if (this.isSpringConfig(value) || this.isEasingConfig(value)) {
215
295
  values[path] = value;
216
296
  } else if (this.isActionConfig(value)) {
217
297
  values[path] = value;
@@ -232,6 +312,9 @@ var DialStoreClass = class {
232
312
  isSpringConfig(value) {
233
313
  return typeof value === "object" && value !== null && "type" in value && value.type === "spring";
234
314
  }
315
+ isEasingConfig(value) {
316
+ return typeof value === "object" && value !== null && "type" in value && value.type === "easing";
317
+ }
235
318
  isActionConfig(value) {
236
319
  return typeof value === "object" && value !== null && "type" in value && value.type === "action";
237
320
  }
@@ -270,6 +353,75 @@ var DialStoreClass = class {
270
353
  if (range <= 100) return 1;
271
354
  return 10;
272
355
  }
356
+ normalizePreservedValue(existingValue, defaultValue, control) {
357
+ if (existingValue === void 0 || !control) {
358
+ return defaultValue;
359
+ }
360
+ switch (control.type) {
361
+ case "slider": {
362
+ if (typeof existingValue !== "number" || typeof defaultValue !== "number") {
363
+ return defaultValue;
364
+ }
365
+ const min = control.min ?? Number.NEGATIVE_INFINITY;
366
+ const max = control.max ?? Number.POSITIVE_INFINITY;
367
+ const clamped = Math.min(max, Math.max(min, existingValue));
368
+ if (typeof control.step !== "number" || control.step <= 0) {
369
+ return clamped;
370
+ }
371
+ return this.roundToStep(clamped, min, max, control.step);
372
+ }
373
+ case "toggle":
374
+ return typeof existingValue === "boolean" ? existingValue : defaultValue;
375
+ case "select": {
376
+ if (typeof existingValue !== "string") {
377
+ return defaultValue;
378
+ }
379
+ const options = control.options ?? [];
380
+ const validValues = new Set(options.map((option) => typeof option === "string" ? option : option.value));
381
+ return validValues.has(existingValue) ? existingValue : defaultValue;
382
+ }
383
+ case "color":
384
+ case "text":
385
+ return typeof existingValue === "string" ? existingValue : defaultValue;
386
+ case "transition":
387
+ if (this.isSpringConfig(defaultValue)) {
388
+ return this.isSpringConfig(existingValue) ? existingValue : defaultValue;
389
+ }
390
+ if (this.isEasingConfig(defaultValue)) {
391
+ return this.isEasingConfig(existingValue) ? existingValue : defaultValue;
392
+ }
393
+ return defaultValue;
394
+ case "action":
395
+ return defaultValue;
396
+ default:
397
+ return defaultValue;
398
+ }
399
+ }
400
+ roundToStep(value, min, max, step) {
401
+ const snapped = min + Math.round((value - min) / step) * step;
402
+ const clamped = Math.min(max, Math.max(min, snapped));
403
+ const precision = this.stepPrecision(step);
404
+ return Number(clamped.toFixed(precision));
405
+ }
406
+ stepPrecision(step) {
407
+ const text = String(step);
408
+ const decimalIndex = text.indexOf(".");
409
+ return decimalIndex === -1 ? 0 : text.length - decimalIndex - 1;
410
+ }
411
+ mapControlsByPath(controls) {
412
+ const map = /* @__PURE__ */ new Map();
413
+ const visit = (nodes) => {
414
+ for (const node of nodes) {
415
+ if (node.type === "folder" && node.children) {
416
+ visit(node.children);
417
+ continue;
418
+ }
419
+ map.set(node.path, node);
420
+ }
421
+ };
422
+ visit(controls);
423
+ return map;
424
+ }
273
425
  };
274
426
  var DialStore = new DialStoreClass();
275
427
 
@@ -278,12 +430,22 @@ function useDialKit(name, config, options) {
278
430
  const instanceId = useId();
279
431
  const panelId = `${name}-${instanceId}`;
280
432
  const configRef = useRef(config);
433
+ const serializedConfig = JSON.stringify(config);
434
+ configRef.current = config;
281
435
  const onActionRef = useRef(options?.onAction);
282
436
  onActionRef.current = options?.onAction;
283
437
  useEffect(() => {
284
438
  DialStore.registerPanel(panelId, name, configRef.current);
285
439
  return () => DialStore.unregisterPanel(panelId);
286
440
  }, [panelId, name]);
441
+ const mountedRef = useRef(false);
442
+ useEffect(() => {
443
+ if (!mountedRef.current) {
444
+ mountedRef.current = true;
445
+ return;
446
+ }
447
+ DialStore.updatePanel(panelId, name, configRef.current);
448
+ }, [panelId, name, serializedConfig]);
287
449
  useEffect(() => {
288
450
  return DialStore.subscribeActions(panelId, (action) => {
289
451
  onActionRef.current?.(action);
@@ -299,12 +461,13 @@ function useDialKit(name, config, options) {
299
461
  function buildResolvedValues(config, flatValues, prefix) {
300
462
  const result = {};
301
463
  for (const [key, configValue] of Object.entries(config)) {
464
+ if (key === "_collapsed") continue;
302
465
  const path = prefix ? `${prefix}.${key}` : key;
303
- if (Array.isArray(configValue) && configValue.length === 3 && typeof configValue[0] === "number") {
466
+ if (Array.isArray(configValue) && configValue.length <= 4 && typeof configValue[0] === "number") {
304
467
  result[key] = flatValues[path] ?? configValue[0];
305
468
  } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
306
469
  result[key] = flatValues[path] ?? configValue;
307
- } else if (isSpringConfig(configValue)) {
470
+ } else if (isSpringConfig(configValue) || isEasingConfig(configValue)) {
308
471
  result[key] = flatValues[path] ?? configValue;
309
472
  } else if (isActionConfig(configValue)) {
310
473
  result[key] = flatValues[path] ?? configValue;
@@ -327,6 +490,9 @@ function hasType(value, type) {
327
490
  function isSpringConfig(value) {
328
491
  return hasType(value, "spring");
329
492
  }
493
+ function isEasingConfig(value) {
494
+ return hasType(value, "easing");
495
+ }
330
496
  function isActionConfig(value) {
331
497
  return hasType(value, "action");
332
498
  }
@@ -345,11 +511,11 @@ function getFirstOptionValue(options) {
345
511
  }
346
512
 
347
513
  // src/components/DialRoot.tsx
348
- import { useEffect as useEffect7, useState as useState8 } from "react";
349
- import { createPortal as createPortal2 } from "react-dom";
514
+ import { useEffect as useEffect7, useState as useState9 } from "react";
515
+ import { createPortal as createPortal3 } from "react-dom";
350
516
 
351
517
  // src/components/Panel.tsx
352
- import { useState as useState7, useSyncExternalStore as useSyncExternalStore3 } from "react";
518
+ import { useState as useState8, useSyncExternalStore as useSyncExternalStore4 } from "react";
353
519
  import { motion as motion6, AnimatePresence as AnimatePresence4 } from "motion/react";
354
520
 
355
521
  // src/components/Folder.tsx
@@ -464,10 +630,14 @@ var CLICK_THRESHOLD = 3;
464
630
  var DEAD_ZONE = 32;
465
631
  var MAX_CURSOR_RANGE = 200;
466
632
  var MAX_STRETCH = 8;
633
+ function decimalsForStep(step) {
634
+ const s = step.toString();
635
+ const dot = s.indexOf(".");
636
+ return dot === -1 ? 0 : s.length - dot - 1;
637
+ }
467
638
  function roundValue(val, step) {
468
- const decimals = step >= 1 ? 0 : 2;
469
- const factor = 10 ** decimals;
470
- return Math.round(val * factor) / factor;
639
+ const raw = Math.round(val / step) * step;
640
+ return parseFloat(raw.toFixed(decimalsForStep(step)));
471
641
  }
472
642
  function snapToDecile(rawValue, min, max) {
473
643
  const normalized = (rawValue - min) / (max - min);
@@ -615,7 +785,8 @@ function Slider({
615
785
  if (!isInteracting) return;
616
786
  if (isClickRef.current) {
617
787
  const rawValue = positionToValue(e.clientX);
618
- const snappedValue = snapToDecile(rawValue, min, max);
788
+ const discreteSteps2 = (max - min) / step;
789
+ const snappedValue = discreteSteps2 <= 10 ? Math.max(min, Math.min(max, min + Math.round((rawValue - min) / step) * step)) : snapToDecile(rawValue, min, max);
619
790
  const newPct = percentFromValue(snappedValue);
620
791
  if (animRef.current) {
621
792
  animRef.current.stop();
@@ -695,7 +866,7 @@ function Slider({
695
866
  e.stopPropagation();
696
867
  e.preventDefault();
697
868
  setShowInput(true);
698
- setInputValue(step >= 1 ? value.toFixed(0) : value.toFixed(2));
869
+ setInputValue(value.toFixed(decimalsForStep(step)));
699
870
  }
700
871
  };
701
872
  const handleInputKeyDown = (e) => {
@@ -709,7 +880,7 @@ function Slider({
709
880
  const handleInputBlur = () => {
710
881
  handleInputSubmit();
711
882
  };
712
- const displayValue = step >= 1 ? value.toFixed(0) : value.toFixed(2);
883
+ const displayValue = value.toFixed(decimalsForStep(step));
713
884
  const HANDLE_BUFFER = 8;
714
885
  const LABEL_CSS_LEFT = 10;
715
886
  const VALUE_CSS_RIGHT = 10;
@@ -727,7 +898,18 @@ function Slider({
727
898
  const valueDodge = percentage < leftThreshold || percentage > rightThreshold;
728
899
  const handleOpacity = !isActive ? 0 : valueDodge ? 0.1 : isDragging ? 0.9 : 0.5;
729
900
  const fillBackground = isActive ? "rgba(255, 255, 255, 0.15)" : "rgba(255, 255, 255, 0.11)";
730
- const hashMarks = Array.from({ length: 9 }, (_, i) => {
901
+ const discreteSteps = (max - min) / step;
902
+ const hashMarks = discreteSteps <= 10 ? Array.from({ length: discreteSteps - 1 }, (_, i) => {
903
+ const pct = (i + 1) * step / (max - min) * 100;
904
+ return /* @__PURE__ */ jsx2(
905
+ "div",
906
+ {
907
+ className: "dialkit-slider-hashmark",
908
+ style: { left: `${pct}%` }
909
+ },
910
+ i
911
+ );
912
+ }) : Array.from({ length: 9 }, (_, i) => {
731
913
  const pct = (i + 1) * 10;
732
914
  return /* @__PURE__ */ jsx2(
733
915
  "div",
@@ -1097,12 +1279,180 @@ function SpringControl({ panelId, path, label, spring, onChange }) {
1097
1279
  ] }) });
1098
1280
  }
1099
1281
 
1100
- // src/components/TextControl.tsx
1282
+ // src/components/EasingVisualization.tsx
1101
1283
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1284
+ function EasingVisualization({ easing }) {
1285
+ const ease = easing.ease;
1286
+ const s = 200;
1287
+ const pad = 10;
1288
+ const inner = s - pad * 2;
1289
+ const unit = inner / 2;
1290
+ const toSvg = (nx, ny) => ({
1291
+ x: pad + (nx + 0.5) * unit,
1292
+ y: pad + (1.5 - ny) * unit
1293
+ });
1294
+ const start = toSvg(0, 0);
1295
+ const end = toSvg(1, 1);
1296
+ const p1 = toSvg(ease[0], ease[1]);
1297
+ const p2 = toSvg(ease[2], ease[3]);
1298
+ const curvePath = `M ${start.x} ${start.y} C ${p1.x} ${p1.y}, ${p2.x} ${p2.y}, ${end.x} ${end.y}`;
1299
+ return /* @__PURE__ */ jsxs7(
1300
+ "svg",
1301
+ {
1302
+ viewBox: `0 0 ${s} ${s}`,
1303
+ preserveAspectRatio: "xMidYMid slice",
1304
+ className: "dialkit-spring-viz dialkit-easing-viz",
1305
+ children: [
1306
+ /* @__PURE__ */ jsx7(
1307
+ "line",
1308
+ {
1309
+ x1: start.x,
1310
+ y1: start.y,
1311
+ x2: end.x,
1312
+ y2: end.y,
1313
+ stroke: "rgba(255, 255, 255, 0.15)",
1314
+ strokeWidth: "1",
1315
+ strokeDasharray: "4,4"
1316
+ }
1317
+ ),
1318
+ /* @__PURE__ */ jsx7("path", { d: curvePath, fill: "none", stroke: "rgba(255, 255, 255, 0.6)", strokeWidth: "2", strokeLinecap: "round" })
1319
+ ]
1320
+ }
1321
+ );
1322
+ }
1323
+
1324
+ // src/components/TransitionControl.tsx
1325
+ import { useSyncExternalStore as useSyncExternalStore3, useState as useState4 } from "react";
1326
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1327
+ function TransitionControl({ panelId, path, label, value, onChange }) {
1328
+ const mode = useSyncExternalStore3(
1329
+ (cb) => DialStore.subscribe(panelId, cb),
1330
+ () => DialStore.getTransitionMode(panelId, path),
1331
+ () => DialStore.getTransitionMode(panelId, path)
1332
+ );
1333
+ const isEasing = mode === "easing";
1334
+ const isSimpleSpring = mode === "simple";
1335
+ const spring = value.type === "spring" ? value : { type: "spring", visualDuration: 0.3, bounce: 0.2 };
1336
+ const easing = value.type === "easing" ? value : { type: "easing", duration: 0.3, ease: [1, -0.4, 0.5, 1] };
1337
+ const handleModeChange = (newMode) => {
1338
+ DialStore.updateTransitionMode(panelId, path, newMode);
1339
+ if (newMode === "easing") {
1340
+ const duration = value.type === "spring" ? value.visualDuration ?? 0.3 : value.duration;
1341
+ onChange({ type: "easing", duration, ease: easing.ease });
1342
+ } else if (newMode === "simple") {
1343
+ onChange({
1344
+ type: "spring",
1345
+ visualDuration: spring.visualDuration ?? (value.type === "easing" ? value.duration : 0.3),
1346
+ bounce: spring.bounce ?? 0.2
1347
+ });
1348
+ } else {
1349
+ onChange({
1350
+ type: "spring",
1351
+ stiffness: spring.stiffness ?? 200,
1352
+ damping: spring.damping ?? 25,
1353
+ mass: spring.mass ?? 1
1354
+ });
1355
+ }
1356
+ };
1357
+ const handleSpringUpdate = (key, val) => {
1358
+ if (isSimpleSpring) {
1359
+ const { stiffness, damping, mass, ...rest } = spring;
1360
+ onChange({ ...rest, [key]: val });
1361
+ } else {
1362
+ const { visualDuration, bounce, ...rest } = spring;
1363
+ onChange({ ...rest, [key]: val });
1364
+ }
1365
+ };
1366
+ const updateEase = (index, val) => {
1367
+ const newEase = [...easing.ease];
1368
+ newEase[index] = val;
1369
+ onChange({ ...easing, ease: newEase });
1370
+ };
1371
+ return /* @__PURE__ */ jsx8(Folder, { title: label, defaultOpen: true, children: /* @__PURE__ */ jsxs8("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
1372
+ isEasing ? /* @__PURE__ */ jsx8(EasingVisualization, { easing }) : /* @__PURE__ */ jsx8(SpringVisualization, { spring, isSimpleMode: isSimpleSpring }),
1373
+ /* @__PURE__ */ jsxs8("div", { className: "dialkit-labeled-control", children: [
1374
+ /* @__PURE__ */ jsx8("span", { className: "dialkit-labeled-control-label", children: "Type" }),
1375
+ /* @__PURE__ */ jsx8(
1376
+ SegmentedControl,
1377
+ {
1378
+ options: [
1379
+ { value: "easing", label: "Easing" },
1380
+ { value: "simple", label: "Time" },
1381
+ { value: "advanced", label: "Physics" }
1382
+ ],
1383
+ value: mode,
1384
+ onChange: handleModeChange
1385
+ }
1386
+ )
1387
+ ] }),
1388
+ isEasing ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
1389
+ /* @__PURE__ */ jsx8(Slider, { label: "x1", value: easing.ease[0], onChange: (v) => updateEase(0, v), min: 0, max: 1, step: 0.01 }),
1390
+ /* @__PURE__ */ jsx8(Slider, { label: "y1", value: easing.ease[1], onChange: (v) => updateEase(1, v), min: -1, max: 2, step: 0.01 }),
1391
+ /* @__PURE__ */ jsx8(Slider, { label: "x2", value: easing.ease[2], onChange: (v) => updateEase(2, v), min: 0, max: 1, step: 0.01 }),
1392
+ /* @__PURE__ */ jsx8(Slider, { label: "y2", value: easing.ease[3], onChange: (v) => updateEase(3, v), min: -1, max: 2, step: 0.01 }),
1393
+ /* @__PURE__ */ jsx8(Slider, { label: "Duration", value: easing.duration, onChange: (v) => onChange({ ...easing, duration: v }), min: 0.1, max: 2, step: 0.05, unit: "s" }),
1394
+ /* @__PURE__ */ jsx8(EaseTextInput, { ease: easing.ease, onChange: (newEase) => onChange({ ...easing, ease: newEase }) })
1395
+ ] }) : isSimpleSpring ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
1396
+ /* @__PURE__ */ jsx8(Slider, { label: "Duration", value: spring.visualDuration ?? 0.3, onChange: (v) => handleSpringUpdate("visualDuration", v), min: 0.1, max: 1, step: 0.05, unit: "s" }),
1397
+ /* @__PURE__ */ jsx8(Slider, { label: "Bounce", value: spring.bounce ?? 0.2, onChange: (v) => handleSpringUpdate("bounce", v), min: 0, max: 1, step: 0.05 })
1398
+ ] }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
1399
+ /* @__PURE__ */ jsx8(Slider, { label: "Stiffness", value: spring.stiffness ?? 400, onChange: (v) => handleSpringUpdate("stiffness", v), min: 1, max: 1e3, step: 10 }),
1400
+ /* @__PURE__ */ jsx8(Slider, { label: "Damping", value: spring.damping ?? 17, onChange: (v) => handleSpringUpdate("damping", v), min: 1, max: 100, step: 1 }),
1401
+ /* @__PURE__ */ jsx8(Slider, { label: "Mass", value: spring.mass ?? 1, onChange: (v) => handleSpringUpdate("mass", v), min: 0.1, max: 10, step: 0.1 })
1402
+ ] })
1403
+ ] }) });
1404
+ }
1405
+ function formatEase(ease) {
1406
+ return ease.map((v) => parseFloat(v.toFixed(2))).join(", ");
1407
+ }
1408
+ function parseEase(str) {
1409
+ const parts = str.split(",").map((s) => parseFloat(s.trim()));
1410
+ if (parts.length === 4 && parts.every((n) => !isNaN(n))) {
1411
+ return parts;
1412
+ }
1413
+ return null;
1414
+ }
1415
+ function EaseTextInput({ ease, onChange }) {
1416
+ const [editing, setEditing] = useState4(false);
1417
+ const [draft, setDraft] = useState4("");
1418
+ const handleFocus = () => {
1419
+ setDraft(formatEase(ease));
1420
+ setEditing(true);
1421
+ };
1422
+ const handleBlur = () => {
1423
+ const parsed = parseEase(draft);
1424
+ if (parsed) onChange(parsed);
1425
+ setEditing(false);
1426
+ };
1427
+ const handleKeyDown = (e) => {
1428
+ if (e.key === "Enter") {
1429
+ e.target.blur();
1430
+ }
1431
+ };
1432
+ return /* @__PURE__ */ jsxs8("div", { className: "dialkit-labeled-control", children: [
1433
+ /* @__PURE__ */ jsx8("span", { className: "dialkit-labeled-control-label", children: "Ease" }),
1434
+ /* @__PURE__ */ jsx8(
1435
+ "input",
1436
+ {
1437
+ type: "text",
1438
+ className: "dialkit-text-input",
1439
+ value: editing ? draft : formatEase(ease),
1440
+ onChange: (e) => setDraft(e.target.value),
1441
+ onFocus: handleFocus,
1442
+ onBlur: handleBlur,
1443
+ onKeyDown: handleKeyDown,
1444
+ spellCheck: false
1445
+ }
1446
+ )
1447
+ ] });
1448
+ }
1449
+
1450
+ // src/components/TextControl.tsx
1451
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1102
1452
  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(
1453
+ return /* @__PURE__ */ jsxs9("div", { className: "dialkit-text-control", children: [
1454
+ /* @__PURE__ */ jsx9("label", { className: "dialkit-text-label", children: label }),
1455
+ /* @__PURE__ */ jsx9(
1106
1456
  "input",
1107
1457
  {
1108
1458
  type: "text",
@@ -1116,9 +1466,10 @@ function TextControl({ label, value, onChange, placeholder }) {
1116
1466
  }
1117
1467
 
1118
1468
  // src/components/SelectControl.tsx
1119
- import { useState as useState4, useRef as useRef5, useEffect as useEffect4 } from "react";
1469
+ import { useState as useState5, useRef as useRef5, useEffect as useEffect4, useCallback as useCallback2 } from "react";
1470
+ import { createPortal } from "react-dom";
1120
1471
  import { motion as motion4, AnimatePresence as AnimatePresence2 } from "motion/react";
1121
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1472
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1122
1473
  function toTitleCase(s) {
1123
1474
  return s.replace(/\b\w/g, (c) => c.toUpperCase());
1124
1475
  }
@@ -1128,32 +1479,59 @@ function normalizeOptions(options) {
1128
1479
  );
1129
1480
  }
1130
1481
  function SelectControl({ label, value, options, onChange }) {
1131
- const [isOpen, setIsOpen] = useState4(false);
1132
- const containerRef = useRef5(null);
1482
+ const [isOpen, setIsOpen] = useState5(false);
1483
+ const triggerRef = useRef5(null);
1484
+ const dropdownRef = useRef5(null);
1485
+ const [portalTarget, setPortalTarget] = useState5(null);
1486
+ const [pos, setPos] = useState5(null);
1133
1487
  const normalized = normalizeOptions(options);
1134
1488
  const selectedOption = normalized.find((o) => o.value === value);
1489
+ const updatePos = useCallback2(() => {
1490
+ const el = triggerRef.current;
1491
+ if (!el) return;
1492
+ const rect = el.getBoundingClientRect();
1493
+ const dropdownHeight = 8 + normalized.length * 36;
1494
+ const spaceBelow = window.innerHeight - rect.bottom - 4;
1495
+ const above = spaceBelow < dropdownHeight && rect.top > spaceBelow;
1496
+ setPos({
1497
+ top: above ? rect.top - 4 : rect.bottom + 4,
1498
+ left: rect.left,
1499
+ width: rect.width,
1500
+ above
1501
+ });
1502
+ }, [normalized.length]);
1503
+ useEffect4(() => {
1504
+ const root = triggerRef.current?.closest(".dialkit-root");
1505
+ setPortalTarget(root ?? document.body);
1506
+ }, []);
1507
+ useEffect4(() => {
1508
+ if (!isOpen) return;
1509
+ updatePos();
1510
+ }, [isOpen, updatePos]);
1135
1511
  useEffect4(() => {
1136
1512
  if (!isOpen) return;
1137
1513
  const handleClick = (e) => {
1138
- if (containerRef.current && !containerRef.current.contains(e.target)) {
1514
+ const target = e.target;
1515
+ if (triggerRef.current && !triggerRef.current.contains(target) && dropdownRef.current && !dropdownRef.current.contains(target)) {
1139
1516
  setIsOpen(false);
1140
1517
  }
1141
1518
  };
1142
1519
  document.addEventListener("mousedown", handleClick);
1143
1520
  return () => document.removeEventListener("mousedown", handleClick);
1144
1521
  }, [isOpen]);
1145
- return /* @__PURE__ */ jsxs8("div", { ref: containerRef, className: "dialkit-select-row", children: [
1146
- /* @__PURE__ */ jsxs8(
1522
+ return /* @__PURE__ */ jsxs10("div", { className: "dialkit-select-row", children: [
1523
+ /* @__PURE__ */ jsxs10(
1147
1524
  "button",
1148
1525
  {
1526
+ ref: triggerRef,
1149
1527
  className: "dialkit-select-trigger",
1150
1528
  onClick: () => setIsOpen(!isOpen),
1151
1529
  "data-open": String(isOpen),
1152
1530
  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(
1531
+ /* @__PURE__ */ jsx10("span", { className: "dialkit-select-label", children: label }),
1532
+ /* @__PURE__ */ jsxs10("div", { className: "dialkit-select-right", children: [
1533
+ /* @__PURE__ */ jsx10("span", { className: "dialkit-select-value", children: selectedOption?.label ?? value }),
1534
+ /* @__PURE__ */ jsx10(
1157
1535
  motion4.svg,
1158
1536
  {
1159
1537
  className: "dialkit-select-chevron",
@@ -1165,46 +1543,56 @@ function SelectControl({ label, value, options, onChange }) {
1165
1543
  strokeLinejoin: "round",
1166
1544
  animate: { rotate: isOpen ? 180 : 0 },
1167
1545
  transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
1168
- children: /* @__PURE__ */ jsx8("path", { d: "M6 9.5L12 15.5L18 9.5" })
1546
+ children: /* @__PURE__ */ jsx10("path", { d: "M6 9.5L12 15.5L18 9.5" })
1169
1547
  }
1170
1548
  )
1171
1549
  ] })
1172
1550
  ]
1173
1551
  }
1174
1552
  ),
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
1553
+ portalTarget && createPortal(
1554
+ /* @__PURE__ */ jsx10(AnimatePresence2, { children: isOpen && pos && /* @__PURE__ */ jsx10(
1555
+ motion4.div,
1556
+ {
1557
+ ref: dropdownRef,
1558
+ className: "dialkit-select-dropdown",
1559
+ initial: { opacity: 0, y: pos.above ? 8 : -8, scale: 0.95 },
1560
+ animate: { opacity: 1, y: 0, scale: 1 },
1561
+ exit: { opacity: 0, y: pos.above ? 8 : -8, scale: 0.95 },
1562
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1563
+ style: {
1564
+ position: "fixed",
1565
+ left: pos.left,
1566
+ width: pos.width,
1567
+ ...pos.above ? { bottom: window.innerHeight - pos.top, transformOrigin: "bottom" } : { top: pos.top, transformOrigin: "top" }
1193
1568
  },
1194
- option.value
1195
- ))
1196
- }
1197
- ) })
1569
+ children: normalized.map((option) => /* @__PURE__ */ jsx10(
1570
+ "button",
1571
+ {
1572
+ className: "dialkit-select-option",
1573
+ "data-selected": String(option.value === value),
1574
+ onClick: () => {
1575
+ onChange(option.value);
1576
+ setIsOpen(false);
1577
+ },
1578
+ children: option.label
1579
+ },
1580
+ option.value
1581
+ ))
1582
+ }
1583
+ ) }),
1584
+ portalTarget
1585
+ )
1198
1586
  ] });
1199
1587
  }
1200
1588
 
1201
1589
  // 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";
1590
+ import { useState as useState6, useRef as useRef6, useEffect as useEffect5 } from "react";
1591
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1204
1592
  var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
1205
1593
  function ColorControl({ label, value, onChange }) {
1206
- const [isEditing, setIsEditing] = useState5(false);
1207
- const [editValue, setEditValue] = useState5(value);
1594
+ const [isEditing, setIsEditing] = useState6(false);
1595
+ const [editValue, setEditValue] = useState6(value);
1208
1596
  const colorInputRef = useRef6(null);
1209
1597
  useEffect5(() => {
1210
1598
  if (!isEditing) {
@@ -1227,10 +1615,10 @@ function ColorControl({ label, value, onChange }) {
1227
1615
  setEditValue(value);
1228
1616
  }
1229
1617
  }
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(
1618
+ return /* @__PURE__ */ jsxs11("div", { className: "dialkit-color-control", children: [
1619
+ /* @__PURE__ */ jsx11("span", { className: "dialkit-color-label", children: label }),
1620
+ /* @__PURE__ */ jsxs11("div", { className: "dialkit-color-inputs", children: [
1621
+ isEditing ? /* @__PURE__ */ jsx11(
1234
1622
  "input",
1235
1623
  {
1236
1624
  type: "text",
@@ -1241,7 +1629,7 @@ function ColorControl({ label, value, onChange }) {
1241
1629
  onKeyDown: handleKeyDown,
1242
1630
  autoFocus: true
1243
1631
  }
1244
- ) : /* @__PURE__ */ jsx9(
1632
+ ) : /* @__PURE__ */ jsx11(
1245
1633
  "span",
1246
1634
  {
1247
1635
  className: "dialkit-color-hex",
@@ -1249,7 +1637,7 @@ function ColorControl({ label, value, onChange }) {
1249
1637
  children: (value ?? "").toUpperCase()
1250
1638
  }
1251
1639
  ),
1252
- /* @__PURE__ */ jsx9(
1640
+ /* @__PURE__ */ jsx11(
1253
1641
  "button",
1254
1642
  {
1255
1643
  className: "dialkit-color-swatch",
@@ -1258,7 +1646,7 @@ function ColorControl({ label, value, onChange }) {
1258
1646
  title: "Pick color"
1259
1647
  }
1260
1648
  ),
1261
- /* @__PURE__ */ jsx9(
1649
+ /* @__PURE__ */ jsx11(
1262
1650
  "input",
1263
1651
  {
1264
1652
  ref: colorInputRef,
@@ -1277,18 +1665,18 @@ function expandShorthandHex(hex) {
1277
1665
  }
1278
1666
 
1279
1667
  // 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";
1668
+ import { useState as useState7, useRef as useRef7, useEffect as useEffect6, useCallback as useCallback3 } from "react";
1669
+ import { createPortal as createPortal2 } from "react-dom";
1282
1670
  import { motion as motion5, AnimatePresence as AnimatePresence3 } from "motion/react";
1283
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1671
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1284
1672
  function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1285
- const [isOpen, setIsOpen] = useState6(false);
1673
+ const [isOpen, setIsOpen] = useState7(false);
1286
1674
  const triggerRef = useRef7(null);
1287
1675
  const dropdownRef = useRef7(null);
1288
- const [pos, setPos] = useState6({ top: 0, left: 0, width: 0 });
1676
+ const [pos, setPos] = useState7({ top: 0, left: 0, width: 0 });
1289
1677
  const hasPresets = presets.length > 0;
1290
1678
  const activePreset = presets.find((p) => p.id === activePresetId);
1291
- const open = useCallback2(() => {
1679
+ const open = useCallback3(() => {
1292
1680
  if (!hasPresets) return;
1293
1681
  const rect = triggerRef.current?.getBoundingClientRect();
1294
1682
  if (rect) {
@@ -1296,8 +1684,8 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1296
1684
  }
1297
1685
  setIsOpen(true);
1298
1686
  }, [hasPresets]);
1299
- const close = useCallback2(() => setIsOpen(false), []);
1300
- const toggle = useCallback2(() => {
1687
+ const close = useCallback3(() => setIsOpen(false), []);
1688
+ const toggle = useCallback3(() => {
1301
1689
  if (isOpen) close();
1302
1690
  else open();
1303
1691
  }, [isOpen, open, close]);
@@ -1323,8 +1711,8 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1323
1711
  e.stopPropagation();
1324
1712
  DialStore.deletePreset(panelId, presetId);
1325
1713
  };
1326
- return /* @__PURE__ */ jsxs10("div", { className: "dialkit-preset-manager", children: [
1327
- /* @__PURE__ */ jsxs10(
1714
+ return /* @__PURE__ */ jsxs12("div", { className: "dialkit-preset-manager", children: [
1715
+ /* @__PURE__ */ jsxs12(
1328
1716
  "button",
1329
1717
  {
1330
1718
  ref: triggerRef,
@@ -1334,8 +1722,8 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1334
1722
  "data-has-preset": String(!!activePreset),
1335
1723
  "data-disabled": String(!hasPresets),
1336
1724
  children: [
1337
- /* @__PURE__ */ jsx10("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
1338
- /* @__PURE__ */ jsx10(
1725
+ /* @__PURE__ */ jsx12("span", { className: "dialkit-preset-label", children: activePreset ? activePreset.name : "Version 1" }),
1726
+ /* @__PURE__ */ jsx12(
1339
1727
  motion5.svg,
1340
1728
  {
1341
1729
  className: "dialkit-select-chevron",
@@ -1347,53 +1735,53 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1347
1735
  strokeLinejoin: "round",
1348
1736
  animate: { rotate: isOpen ? 180 : 0, opacity: hasPresets ? 0.6 : 0.25 },
1349
1737
  transition: { type: "spring", visualDuration: 0.2, bounce: 0.15 },
1350
- children: /* @__PURE__ */ jsx10("path", { d: "M6 9.5L12 15.5L18 9.5" })
1738
+ children: /* @__PURE__ */ jsx12("path", { d: "M6 9.5L12 15.5L18 9.5" })
1351
1739
  }
1352
1740
  )
1353
1741
  ]
1354
1742
  }
1355
1743
  ),
1356
- createPortal(
1357
- /* @__PURE__ */ jsx10(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs10(
1744
+ createPortal2(
1745
+ /* @__PURE__ */ jsx12(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs12(
1358
1746
  motion5.div,
1359
1747
  {
1360
1748
  ref: dropdownRef,
1361
- className: "dialkit-preset-dropdown",
1749
+ className: "dialkit-root dialkit-preset-dropdown",
1362
1750
  style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
1363
1751
  initial: { opacity: 0, y: 4, scale: 0.97 },
1364
1752
  animate: { opacity: 1, y: 0, scale: 1 },
1365
1753
  exit: { opacity: 0, y: 4, scale: 0.97, pointerEvents: "none" },
1366
1754
  transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1367
1755
  children: [
1368
- /* @__PURE__ */ jsx10(
1756
+ /* @__PURE__ */ jsx12(
1369
1757
  "div",
1370
1758
  {
1371
1759
  className: "dialkit-preset-item",
1372
1760
  "data-active": String(!activePresetId),
1373
1761
  onClick: () => handleSelect(null),
1374
- children: /* @__PURE__ */ jsx10("span", { className: "dialkit-preset-name", children: "Version 1" })
1762
+ children: /* @__PURE__ */ jsx12("span", { className: "dialkit-preset-name", children: "Version 1" })
1375
1763
  }
1376
1764
  ),
1377
- presets.map((preset) => /* @__PURE__ */ jsxs10(
1765
+ presets.map((preset) => /* @__PURE__ */ jsxs12(
1378
1766
  "div",
1379
1767
  {
1380
1768
  className: "dialkit-preset-item",
1381
1769
  "data-active": String(preset.id === activePresetId),
1382
1770
  onClick: () => handleSelect(preset.id),
1383
1771
  children: [
1384
- /* @__PURE__ */ jsx10("span", { className: "dialkit-preset-name", children: preset.name }),
1385
- /* @__PURE__ */ jsx10(
1772
+ /* @__PURE__ */ jsx12("span", { className: "dialkit-preset-name", children: preset.name }),
1773
+ /* @__PURE__ */ jsx12(
1386
1774
  "button",
1387
1775
  {
1388
1776
  className: "dialkit-preset-delete",
1389
1777
  onClick: (e) => handleDelete(e, preset.id),
1390
1778
  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" })
1779
+ children: /* @__PURE__ */ jsxs12("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1780
+ /* @__PURE__ */ jsx12("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" }),
1781
+ /* @__PURE__ */ jsx12("path", { d: "M10 11V16" }),
1782
+ /* @__PURE__ */ jsx12("path", { d: "M14 11V16" }),
1783
+ /* @__PURE__ */ jsx12("path", { d: "M3.5 6H20.5" }),
1784
+ /* @__PURE__ */ jsx12("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
1785
  ] })
1398
1786
  }
1399
1787
  )
@@ -1410,11 +1798,11 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1410
1798
  }
1411
1799
 
1412
1800
  // src/components/Panel.tsx
1413
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1414
- function Panel({ panel }) {
1415
- const [copied, setCopied] = useState7(false);
1416
- const [isPanelOpen, setIsPanelOpen] = useState7(true);
1417
- const values = useSyncExternalStore3(
1801
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1802
+ function Panel({ panel, defaultOpen = true }) {
1803
+ const [copied, setCopied] = useState8(false);
1804
+ const [isPanelOpen, setIsPanelOpen] = useState8(defaultOpen);
1805
+ const values = useSyncExternalStore4(
1418
1806
  (cb) => DialStore.subscribe(panel.id, cb),
1419
1807
  () => DialStore.getValues(panel.id),
1420
1808
  () => DialStore.getValues(panel.id)
@@ -1442,7 +1830,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1442
1830
  const value = values[control.path];
1443
1831
  switch (control.type) {
1444
1832
  case "slider":
1445
- return /* @__PURE__ */ jsx11(
1833
+ return /* @__PURE__ */ jsx13(
1446
1834
  Slider,
1447
1835
  {
1448
1836
  label: control.label,
@@ -1455,7 +1843,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1455
1843
  control.path
1456
1844
  );
1457
1845
  case "toggle":
1458
- return /* @__PURE__ */ jsx11(
1846
+ return /* @__PURE__ */ jsx13(
1459
1847
  Toggle,
1460
1848
  {
1461
1849
  label: control.label,
@@ -1465,7 +1853,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1465
1853
  control.path
1466
1854
  );
1467
1855
  case "spring":
1468
- return /* @__PURE__ */ jsx11(
1856
+ return /* @__PURE__ */ jsx13(
1469
1857
  SpringControl,
1470
1858
  {
1471
1859
  panelId: panel.id,
@@ -1476,10 +1864,22 @@ Apply these values as the new defaults in the useDialKit call.`;
1476
1864
  },
1477
1865
  control.path
1478
1866
  );
1867
+ case "transition":
1868
+ return /* @__PURE__ */ jsx13(
1869
+ TransitionControl,
1870
+ {
1871
+ panelId: panel.id,
1872
+ path: control.path,
1873
+ label: control.label,
1874
+ value,
1875
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
1876
+ },
1877
+ control.path
1878
+ );
1479
1879
  case "folder":
1480
- return /* @__PURE__ */ jsx11(Folder, { title: control.label, children: control.children?.map(renderControl) }, control.path);
1880
+ return /* @__PURE__ */ jsx13(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
1481
1881
  case "text":
1482
- return /* @__PURE__ */ jsx11(
1882
+ return /* @__PURE__ */ jsx13(
1483
1883
  TextControl,
1484
1884
  {
1485
1885
  label: control.label,
@@ -1490,7 +1890,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1490
1890
  control.path
1491
1891
  );
1492
1892
  case "select":
1493
- return /* @__PURE__ */ jsx11(
1893
+ return /* @__PURE__ */ jsx13(
1494
1894
  SelectControl,
1495
1895
  {
1496
1896
  label: control.label,
@@ -1501,7 +1901,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1501
1901
  control.path
1502
1902
  );
1503
1903
  case "color":
1504
- return /* @__PURE__ */ jsx11(
1904
+ return /* @__PURE__ */ jsx13(
1505
1905
  ColorControl,
1506
1906
  {
1507
1907
  label: control.label,
@@ -1510,37 +1910,26 @@ Apply these values as the new defaults in the useDialKit call.`;
1510
1910
  },
1511
1911
  control.path
1512
1912
  );
1913
+ case "action":
1914
+ return /* @__PURE__ */ jsx13(
1915
+ "button",
1916
+ {
1917
+ className: "dialkit-button",
1918
+ onClick: () => DialStore.triggerAction(panel.id, control.path),
1919
+ children: control.label
1920
+ },
1921
+ control.path
1922
+ );
1513
1923
  default:
1514
1924
  return null;
1515
1925
  }
1516
1926
  };
1517
1927
  const renderControls = () => {
1518
- const result = [];
1519
- let i = 0;
1520
- while (i < panel.controls.length) {
1521
- const control = panel.controls[i];
1522
- if (control.type === "action") {
1523
- result.push(
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
- )
1533
- );
1534
- } else {
1535
- result.push(renderControl(control));
1536
- }
1537
- i++;
1538
- }
1539
- return result;
1928
+ return panel.controls.map(renderControl);
1540
1929
  };
1541
1930
  const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
1542
- const toolbar = /* @__PURE__ */ jsxs11(Fragment2, { children: [
1543
- /* @__PURE__ */ jsx11(
1931
+ const toolbar = /* @__PURE__ */ jsxs13(Fragment3, { children: [
1932
+ /* @__PURE__ */ jsx13(
1544
1933
  motion6.button,
1545
1934
  {
1546
1935
  className: "dialkit-toolbar-add",
@@ -1548,16 +1937,16 @@ Apply these values as the new defaults in the useDialKit call.`;
1548
1937
  title: "Add preset",
1549
1938
  whileTap: { scale: 0.9 },
1550
1939
  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" })
1940
+ children: /* @__PURE__ */ jsxs13("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1941
+ /* @__PURE__ */ jsx13("path", { d: "M4 6H20" }),
1942
+ /* @__PURE__ */ jsx13("path", { d: "M4 12H10" }),
1943
+ /* @__PURE__ */ jsx13("path", { d: "M15 15L21 15" }),
1944
+ /* @__PURE__ */ jsx13("path", { d: "M18 12V18" }),
1945
+ /* @__PURE__ */ jsx13("path", { d: "M4 18H10" })
1557
1946
  ] })
1558
1947
  }
1559
1948
  ),
1560
- /* @__PURE__ */ jsx11(
1949
+ /* @__PURE__ */ jsx13(
1561
1950
  PresetManager,
1562
1951
  {
1563
1952
  panelId: panel.id,
@@ -1566,7 +1955,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1566
1955
  onAdd: handleAddPreset
1567
1956
  }
1568
1957
  ),
1569
- /* @__PURE__ */ jsxs11(
1958
+ /* @__PURE__ */ jsxs13(
1570
1959
  motion6.button,
1571
1960
  {
1572
1961
  className: "dialkit-toolbar-copy",
@@ -1575,7 +1964,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1575
1964
  whileTap: { scale: 0.95 },
1576
1965
  transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
1577
1966
  children: [
1578
- /* @__PURE__ */ jsx11("span", { className: "dialkit-toolbar-copy-icon-wrap", children: /* @__PURE__ */ jsx11(AnimatePresence4, { initial: false, mode: "popLayout", children: copied ? /* @__PURE__ */ jsx11(
1967
+ /* @__PURE__ */ jsx13("span", { className: "dialkit-toolbar-copy-icon-wrap", children: /* @__PURE__ */ jsx13(AnimatePresence4, { initial: false, mode: "popLayout", children: copied ? /* @__PURE__ */ jsx13(
1579
1968
  motion6.svg,
1580
1969
  {
1581
1970
  className: "dialkit-toolbar-copy-icon",
@@ -1589,10 +1978,10 @@ Apply these values as the new defaults in the useDialKit call.`;
1589
1978
  animate: { scale: 1, opacity: 1, filter: "blur(0px)" },
1590
1979
  exit: { scale: 0.5, opacity: 0, filter: "blur(4px)" },
1591
1980
  transition: { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1592
- children: /* @__PURE__ */ jsx11("path", { d: "M5 12.75L10 19L19 5" })
1981
+ children: /* @__PURE__ */ jsx13("path", { d: "M5 12.75L10 19L19 5" })
1593
1982
  },
1594
1983
  "check"
1595
- ) : /* @__PURE__ */ jsxs11(
1984
+ ) : /* @__PURE__ */ jsxs13(
1596
1985
  motion6.svg,
1597
1986
  {
1598
1987
  className: "dialkit-toolbar-copy-icon",
@@ -1603,9 +1992,9 @@ Apply these values as the new defaults in the useDialKit call.`;
1603
1992
  exit: { scale: 0.5, opacity: 0, filter: "blur(4px)" },
1604
1993
  transition: { type: "spring", visualDuration: 0.3, bounce: 0.2 },
1605
1994
  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" })
1995
+ /* @__PURE__ */ jsx13("path", { d: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
1996
+ /* @__PURE__ */ jsx13("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" }),
1997
+ /* @__PURE__ */ jsx13("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
1998
  ]
1610
1999
  },
1611
2000
  "clipboard"
@@ -1615,14 +2004,14 @@ Apply these values as the new defaults in the useDialKit call.`;
1615
2004
  }
1616
2005
  )
1617
2006
  ] });
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() }) });
2007
+ return /* @__PURE__ */ jsx13("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx13(Folder, { title: panel.name, defaultOpen, isRoot: true, onOpenChange: setIsPanelOpen, toolbar, children: renderControls() }) });
1619
2008
  }
1620
2009
 
1621
2010
  // src/components/DialRoot.tsx
1622
- import { jsx as jsx12 } from "react/jsx-runtime";
1623
- function DialRoot({ position = "top-right" }) {
1624
- const [panels, setPanels] = useState8([]);
1625
- const [mounted, setMounted] = useState8(false);
2011
+ import { jsx as jsx14 } from "react/jsx-runtime";
2012
+ function DialRoot({ position = "top-right", defaultOpen = true }) {
2013
+ const [panels, setPanels] = useState9([]);
2014
+ const [mounted, setMounted] = useState9(false);
1626
2015
  useEffect7(() => {
1627
2016
  setMounted(true);
1628
2017
  setPanels(DialStore.getPanels());
@@ -1637,14 +2026,14 @@ function DialRoot({ position = "top-right" }) {
1637
2026
  if (panels.length === 0) {
1638
2027
  return null;
1639
2028
  }
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);
2029
+ const content = /* @__PURE__ */ jsx14("div", { className: "dialkit-root", children: /* @__PURE__ */ jsx14("div", { className: "dialkit-panel", "data-position": position, children: panels.map((panel) => /* @__PURE__ */ jsx14(Panel, { panel, defaultOpen }, panel.id)) }) });
2030
+ return createPortal3(content, document.body);
1642
2031
  }
1643
2032
 
1644
2033
  // src/components/ButtonGroup.tsx
1645
- import { jsx as jsx13 } from "react/jsx-runtime";
2034
+ import { jsx as jsx15 } from "react/jsx-runtime";
1646
2035
  function ButtonGroup({ buttons }) {
1647
- return /* @__PURE__ */ jsx13("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx13(
2036
+ return /* @__PURE__ */ jsx15("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx15(
1648
2037
  "button",
1649
2038
  {
1650
2039
  className: "dialkit-button",
@@ -1659,6 +2048,7 @@ export {
1659
2048
  ColorControl,
1660
2049
  DialRoot,
1661
2050
  DialStore,
2051
+ EasingVisualization,
1662
2052
  Folder,
1663
2053
  PresetManager,
1664
2054
  SelectControl,
@@ -1667,6 +2057,7 @@ export {
1667
2057
  SpringVisualization,
1668
2058
  TextControl,
1669
2059
  Toggle,
2060
+ TransitionControl,
1670
2061
  useDialKit
1671
2062
  };
1672
2063
  //# sourceMappingURL=index.js.map