dialkit 0.1.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 ADDED
@@ -0,0 +1,1055 @@
1
+ "use client";
2
+
3
+ // src/hooks/useDialKit.ts
4
+ import { useEffect, useId, useSyncExternalStore, useRef } from "react";
5
+
6
+ // src/store/DialStore.ts
7
+ var DialStoreClass = class {
8
+ constructor() {
9
+ this.panels = /* @__PURE__ */ new Map();
10
+ this.listeners = /* @__PURE__ */ new Map();
11
+ this.globalListeners = /* @__PURE__ */ new Set();
12
+ this.snapshots = /* @__PURE__ */ new Map();
13
+ this.actionListeners = /* @__PURE__ */ new Map();
14
+ }
15
+ registerPanel(id, name, config) {
16
+ const controls = this.parseConfig(config, "");
17
+ const values = this.flattenValues(config, "");
18
+ this.panels.set(id, { id, name, controls, values });
19
+ this.snapshots.set(id, { ...values });
20
+ this.notifyGlobal();
21
+ }
22
+ unregisterPanel(id) {
23
+ this.panels.delete(id);
24
+ this.listeners.delete(id);
25
+ this.snapshots.delete(id);
26
+ this.notifyGlobal();
27
+ }
28
+ updateValue(panelId, path, value) {
29
+ const panel = this.panels.get(panelId);
30
+ if (!panel) return;
31
+ panel.values[path] = value;
32
+ this.snapshots.set(panelId, { ...panel.values });
33
+ this.notify(panelId);
34
+ }
35
+ updateSpringMode(panelId, path, mode) {
36
+ const panel = this.panels.get(panelId);
37
+ if (!panel) return;
38
+ panel.values[`${path}.__mode`] = mode;
39
+ this.snapshots.set(panelId, { ...panel.values });
40
+ this.notify(panelId);
41
+ }
42
+ getSpringMode(panelId, path) {
43
+ const panel = this.panels.get(panelId);
44
+ if (!panel) return "simple";
45
+ return panel.values[`${path}.__mode`] || "simple";
46
+ }
47
+ getValue(panelId, path) {
48
+ const panel = this.panels.get(panelId);
49
+ return panel?.values[path];
50
+ }
51
+ getValues(panelId) {
52
+ return this.snapshots.get(panelId) ?? {};
53
+ }
54
+ getPanels() {
55
+ return Array.from(this.panels.values());
56
+ }
57
+ getPanel(id) {
58
+ return this.panels.get(id);
59
+ }
60
+ subscribe(panelId, listener) {
61
+ if (!this.listeners.has(panelId)) {
62
+ this.listeners.set(panelId, /* @__PURE__ */ new Set());
63
+ }
64
+ this.listeners.get(panelId).add(listener);
65
+ return () => {
66
+ this.listeners.get(panelId)?.delete(listener);
67
+ };
68
+ }
69
+ subscribeGlobal(listener) {
70
+ this.globalListeners.add(listener);
71
+ return () => this.globalListeners.delete(listener);
72
+ }
73
+ subscribeActions(panelId, listener) {
74
+ if (!this.actionListeners.has(panelId)) {
75
+ this.actionListeners.set(panelId, /* @__PURE__ */ new Set());
76
+ }
77
+ this.actionListeners.get(panelId).add(listener);
78
+ return () => {
79
+ this.actionListeners.get(panelId)?.delete(listener);
80
+ };
81
+ }
82
+ triggerAction(panelId, path) {
83
+ this.actionListeners.get(panelId)?.forEach((fn) => fn(path));
84
+ }
85
+ notify(panelId) {
86
+ this.listeners.get(panelId)?.forEach((fn) => fn());
87
+ }
88
+ notifyGlobal() {
89
+ this.globalListeners.forEach((fn) => fn());
90
+ }
91
+ parseConfig(config, prefix) {
92
+ const controls = [];
93
+ for (const [key, value] of Object.entries(config)) {
94
+ const path = prefix ? `${prefix}.${key}` : key;
95
+ const label = this.formatLabel(key);
96
+ if (Array.isArray(value) && value.length === 3 && typeof value[0] === "number") {
97
+ controls.push({
98
+ type: "slider",
99
+ path,
100
+ label,
101
+ min: value[1],
102
+ max: value[2],
103
+ step: this.inferStep(value[1], value[2])
104
+ });
105
+ } else if (typeof value === "number") {
106
+ const { min, max, step } = this.inferRange(value);
107
+ controls.push({ type: "slider", path, label, min, max, step });
108
+ } else if (typeof value === "boolean") {
109
+ controls.push({ type: "toggle", path, label });
110
+ } else if (this.isSpringConfig(value)) {
111
+ controls.push({ type: "spring", path, label });
112
+ } else if (this.isActionConfig(value)) {
113
+ controls.push({ type: "action", path, label: value.label || label });
114
+ } else if (typeof value === "object" && value !== null) {
115
+ controls.push({
116
+ type: "folder",
117
+ path,
118
+ label,
119
+ children: this.parseConfig(value, path)
120
+ });
121
+ }
122
+ }
123
+ return controls;
124
+ }
125
+ flattenValues(config, prefix) {
126
+ const values = {};
127
+ for (const [key, value] of Object.entries(config)) {
128
+ const path = prefix ? `${prefix}.${key}` : key;
129
+ if (Array.isArray(value) && value.length === 3 && typeof value[0] === "number") {
130
+ values[path] = value[0];
131
+ } else if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
132
+ values[path] = value;
133
+ } else if (this.isSpringConfig(value)) {
134
+ values[path] = value;
135
+ } else if (this.isActionConfig(value)) {
136
+ values[path] = value;
137
+ } else if (typeof value === "object" && value !== null) {
138
+ Object.assign(values, this.flattenValues(value, path));
139
+ }
140
+ }
141
+ return values;
142
+ }
143
+ isSpringConfig(value) {
144
+ return typeof value === "object" && value !== null && "type" in value && value.type === "spring";
145
+ }
146
+ isActionConfig(value) {
147
+ return typeof value === "object" && value !== null && "type" in value && value.type === "action";
148
+ }
149
+ formatLabel(key) {
150
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
151
+ }
152
+ inferRange(value) {
153
+ if (value >= 0 && value <= 1) {
154
+ return { min: 0, max: 1, step: 0.01 };
155
+ } else if (value >= 0 && value <= 10) {
156
+ return { min: 0, max: value * 2 || 10, step: 0.1 };
157
+ } else if (value >= 0 && value <= 100) {
158
+ return { min: 0, max: value * 2 || 100, step: 1 };
159
+ } else if (value >= 0) {
160
+ return { min: 0, max: value * 2 || 1e3, step: 10 };
161
+ } else {
162
+ return { min: value * 2, max: -value * 2, step: 1 };
163
+ }
164
+ }
165
+ inferStep(min, max) {
166
+ const range = max - min;
167
+ if (range <= 1) return 0.01;
168
+ if (range <= 10) return 0.1;
169
+ if (range <= 100) return 1;
170
+ return 10;
171
+ }
172
+ };
173
+ var DialStore = new DialStoreClass();
174
+
175
+ // src/hooks/useDialKit.ts
176
+ function useDialKit(name, config, options) {
177
+ const instanceId = useId();
178
+ const panelId = `${name}-${instanceId}`;
179
+ const configRef = useRef(config);
180
+ const onActionRef = useRef(options?.onAction);
181
+ onActionRef.current = options?.onAction;
182
+ useEffect(() => {
183
+ DialStore.registerPanel(panelId, name, configRef.current);
184
+ return () => DialStore.unregisterPanel(panelId);
185
+ }, [panelId, name]);
186
+ useEffect(() => {
187
+ return DialStore.subscribeActions(panelId, (action) => {
188
+ onActionRef.current?.(action);
189
+ });
190
+ }, [panelId]);
191
+ const values = useSyncExternalStore(
192
+ (callback) => DialStore.subscribe(panelId, callback),
193
+ () => DialStore.getValues(panelId),
194
+ () => DialStore.getValues(panelId)
195
+ );
196
+ return buildResolvedValues(config, values, "");
197
+ }
198
+ function buildResolvedValues(config, flatValues, prefix) {
199
+ const result = {};
200
+ for (const [key, configValue] of Object.entries(config)) {
201
+ const path = prefix ? `${prefix}.${key}` : key;
202
+ if (Array.isArray(configValue) && configValue.length === 3 && typeof configValue[0] === "number") {
203
+ result[key] = flatValues[path] ?? configValue[0];
204
+ } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
205
+ result[key] = flatValues[path] ?? configValue;
206
+ } else if (isSpringConfig(configValue)) {
207
+ result[key] = flatValues[path] ?? configValue;
208
+ } else if (typeof configValue === "object" && configValue !== null) {
209
+ result[key] = buildResolvedValues(configValue, flatValues, path);
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+ function isSpringConfig(value) {
215
+ return typeof value === "object" && value !== null && "type" in value && value.type === "spring";
216
+ }
217
+
218
+ // src/components/DialRoot.tsx
219
+ import { useEffect as useEffect3, useState as useState4 } from "react";
220
+ import { createPortal } from "react-dom";
221
+
222
+ // src/components/Folder.tsx
223
+ import { useState } from "react";
224
+ import { motion, AnimatePresence } from "motion/react";
225
+ import { jsx, jsxs } from "react/jsx-runtime";
226
+ function Folder({ title, children, defaultOpen = true, isRoot = false, panelId }) {
227
+ const [isOpen, setIsOpen] = useState(defaultOpen);
228
+ const [copied, setCopied] = useState(false);
229
+ const handleCopy = (e) => {
230
+ e.stopPropagation();
231
+ if (!panelId) return;
232
+ const values = DialStore.getValues(panelId);
233
+ const jsonStr = JSON.stringify(values, null, 2);
234
+ const instruction = `Update the useDialKit configuration for "${title}" with these values:
235
+
236
+ \`\`\`json
237
+ ${jsonStr}
238
+ \`\`\`
239
+
240
+ Apply these values as the new defaults in the useDialKit call.`;
241
+ navigator.clipboard.writeText(instruction);
242
+ setCopied(true);
243
+ setTimeout(() => setCopied(false), 1500);
244
+ };
245
+ const iconTransition = { type: "spring", visualDuration: 0.4, bounce: 0.1 };
246
+ const folderContent = /* @__PURE__ */ jsxs("div", { className: `dialkit-folder ${isRoot ? "dialkit-folder-root" : ""}`, children: [
247
+ /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-header", onClick: () => setIsOpen(!isOpen), children: [
248
+ /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-title-row", children: [
249
+ /* @__PURE__ */ jsx("span", { className: `dialkit-folder-title ${isRoot ? "dialkit-folder-title-root" : ""}`, children: title }),
250
+ isRoot && panelId && /* @__PURE__ */ jsx(
251
+ motion.button,
252
+ {
253
+ className: "dialkit-folder-copy",
254
+ onClick: handleCopy,
255
+ title: "Copy parameters",
256
+ initial: false,
257
+ animate: {
258
+ opacity: isOpen ? 1 : 0,
259
+ scale: isOpen ? 1 : 0.7,
260
+ filter: isOpen ? "blur(0px)" : "blur(6px)"
261
+ },
262
+ transition: iconTransition,
263
+ style: { pointerEvents: isOpen ? "auto" : "none" },
264
+ children: /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: 14, height: 14 }, children: [
265
+ /* @__PURE__ */ jsxs(
266
+ motion.svg,
267
+ {
268
+ viewBox: "0 0 24 24",
269
+ fill: "none",
270
+ stroke: "currentColor",
271
+ strokeWidth: "2",
272
+ strokeLinecap: "round",
273
+ strokeLinejoin: "round",
274
+ style: { position: "absolute", inset: 0 },
275
+ initial: false,
276
+ animate: {
277
+ opacity: copied ? 0 : 1,
278
+ scale: copied ? 0.7 : 1,
279
+ filter: copied ? "blur(6px)" : "blur(0px)"
280
+ },
281
+ transition: iconTransition,
282
+ children: [
283
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
284
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
285
+ ]
286
+ }
287
+ ),
288
+ /* @__PURE__ */ jsx(
289
+ motion.svg,
290
+ {
291
+ viewBox: "0 0 24 24",
292
+ fill: "none",
293
+ stroke: "currentColor",
294
+ strokeWidth: "2",
295
+ strokeLinecap: "round",
296
+ strokeLinejoin: "round",
297
+ style: { position: "absolute", inset: 0 },
298
+ initial: false,
299
+ animate: {
300
+ opacity: copied ? 1 : 0,
301
+ scale: copied ? 1 : 0.7,
302
+ filter: copied ? "blur(0px)" : "blur(6px)"
303
+ },
304
+ transition: iconTransition,
305
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
306
+ }
307
+ )
308
+ ] })
309
+ }
310
+ )
311
+ ] }),
312
+ isRoot ? (
313
+ // Root panel uses minus/plus icons with crossfade
314
+ /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-icon", style: { position: "relative" }, children: [
315
+ /* @__PURE__ */ jsx(
316
+ motion.svg,
317
+ {
318
+ viewBox: "0 0 24 24",
319
+ fill: "none",
320
+ stroke: "currentColor",
321
+ strokeWidth: "2",
322
+ strokeLinecap: "round",
323
+ strokeLinejoin: "round",
324
+ style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
325
+ initial: false,
326
+ animate: {
327
+ opacity: isOpen ? 1 : 0,
328
+ scale: isOpen ? 1 : 0.7,
329
+ filter: isOpen ? "blur(0px)" : "blur(6px)"
330
+ },
331
+ transition: iconTransition,
332
+ children: /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
333
+ }
334
+ ),
335
+ /* @__PURE__ */ jsxs(
336
+ motion.svg,
337
+ {
338
+ viewBox: "0 0 24 24",
339
+ fill: "none",
340
+ stroke: "currentColor",
341
+ strokeWidth: "2",
342
+ strokeLinecap: "round",
343
+ strokeLinejoin: "round",
344
+ style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
345
+ initial: false,
346
+ animate: {
347
+ opacity: isOpen ? 0 : 1,
348
+ scale: isOpen ? 0.7 : 1,
349
+ filter: isOpen ? "blur(6px)" : "blur(0px)"
350
+ },
351
+ transition: iconTransition,
352
+ children: [
353
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
354
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
355
+ ]
356
+ }
357
+ )
358
+ ] })
359
+ ) : (
360
+ // Section folders use rotating chevron with gentle spring
361
+ /* @__PURE__ */ jsxs(
362
+ motion.svg,
363
+ {
364
+ className: "dialkit-folder-icon",
365
+ viewBox: "0 0 24 24",
366
+ fill: "none",
367
+ stroke: "currentColor",
368
+ strokeWidth: "1.5",
369
+ strokeLinecap: "round",
370
+ strokeLinejoin: "round",
371
+ initial: false,
372
+ animate: { rotate: isOpen ? 0 : 180 },
373
+ transition: { type: "spring", visualDuration: 0.35, bounce: 0.15 },
374
+ children: [
375
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "9", x2: "12", y2: "16" }),
376
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "9", x2: "12", y2: "16" })
377
+ ]
378
+ }
379
+ )
380
+ )
381
+ ] }),
382
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: isOpen && /* @__PURE__ */ jsx(
383
+ motion.div,
384
+ {
385
+ className: "dialkit-folder-content",
386
+ initial: { height: 0, opacity: 0 },
387
+ animate: { height: "auto", opacity: 1 },
388
+ exit: { height: 0, opacity: 0 },
389
+ transition: { type: "spring", visualDuration: 0.25, bounce: 0 },
390
+ children: /* @__PURE__ */ jsx("div", { className: "dialkit-folder-inner", children })
391
+ }
392
+ ) })
393
+ ] });
394
+ if (isRoot) {
395
+ return /* @__PURE__ */ jsx(
396
+ motion.div,
397
+ {
398
+ className: "dialkit-panel-inner",
399
+ initial: false,
400
+ animate: {
401
+ width: isOpen ? 280 : 160,
402
+ boxShadow: isOpen ? "0 8px 32px rgba(0, 0, 0, 0.5)" : "0 4px 16px rgba(0, 0, 0, 0.25)"
403
+ },
404
+ transition: { type: "spring", visualDuration: 0.4, bounce: 0.05 },
405
+ "data-collapsed": !isOpen,
406
+ children: folderContent
407
+ }
408
+ );
409
+ }
410
+ return folderContent;
411
+ }
412
+
413
+ // src/components/Slider.tsx
414
+ import { useRef as useRef2, useState as useState2, useCallback, useEffect as useEffect2 } from "react";
415
+ import { motion as motion2 } from "motion/react";
416
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
417
+ function Slider({
418
+ label,
419
+ value,
420
+ onChange,
421
+ min = 0,
422
+ max = 1,
423
+ step = 0.01,
424
+ unit
425
+ }) {
426
+ const trackRef = useRef2(null);
427
+ const inputRef = useRef2(null);
428
+ const [isInteracting, setIsInteracting] = useState2(false);
429
+ const [isDragging, setIsDragging] = useState2(false);
430
+ const [isHovered, setIsHovered] = useState2(false);
431
+ const [isValueHovered, setIsValueHovered] = useState2(false);
432
+ const [isValueEditable, setIsValueEditable] = useState2(false);
433
+ const [showInput, setShowInput] = useState2(false);
434
+ const [inputValue, setInputValue] = useState2("");
435
+ const hoverTimeoutRef = useRef2(null);
436
+ const percentage = (value - min) / (max - min) * 100;
437
+ const isAtMinimum = value <= min;
438
+ const isActive = isInteracting || isHovered;
439
+ const positionToValue = useCallback(
440
+ (clientX) => {
441
+ if (!trackRef.current) return value;
442
+ const rect = trackRef.current.getBoundingClientRect();
443
+ const x = clientX - rect.left;
444
+ const percent = Math.max(0, Math.min(1, x / rect.width));
445
+ const rawValue = min + percent * (max - min);
446
+ return Math.max(min, Math.min(max, rawValue));
447
+ },
448
+ [min, max, value]
449
+ );
450
+ const handleMouseDown = useCallback(
451
+ (e) => {
452
+ if (showInput) return;
453
+ e.preventDefault();
454
+ setIsInteracting(true);
455
+ onChange(positionToValue(e.clientX));
456
+ },
457
+ [positionToValue, onChange, showInput]
458
+ );
459
+ const handleMouseMove = useCallback(
460
+ (e) => {
461
+ if (!isInteracting) return;
462
+ if (!isDragging) setIsDragging(true);
463
+ onChange(positionToValue(e.clientX));
464
+ },
465
+ [isInteracting, isDragging, positionToValue, onChange]
466
+ );
467
+ const handleMouseUp = useCallback(() => {
468
+ setIsInteracting(false);
469
+ setIsDragging(false);
470
+ }, []);
471
+ const handleTouchStart = useCallback(
472
+ (e) => {
473
+ if (showInput) return;
474
+ e.preventDefault();
475
+ setIsInteracting(true);
476
+ const touch = e.touches[0];
477
+ onChange(positionToValue(touch.clientX));
478
+ },
479
+ [positionToValue, onChange, showInput]
480
+ );
481
+ const handleTouchMove = useCallback(
482
+ (e) => {
483
+ if (!isInteracting) return;
484
+ if (!isDragging) setIsDragging(true);
485
+ const touch = e.touches[0];
486
+ onChange(positionToValue(touch.clientX));
487
+ },
488
+ [isInteracting, isDragging, positionToValue, onChange]
489
+ );
490
+ const handleTouchEnd = useCallback(() => {
491
+ setIsInteracting(false);
492
+ setIsDragging(false);
493
+ }, []);
494
+ useEffect2(() => {
495
+ if (isInteracting) {
496
+ document.addEventListener("mousemove", handleMouseMove);
497
+ document.addEventListener("mouseup", handleMouseUp);
498
+ document.addEventListener("touchmove", handleTouchMove, { passive: false });
499
+ document.addEventListener("touchend", handleTouchEnd);
500
+ return () => {
501
+ document.removeEventListener("mousemove", handleMouseMove);
502
+ document.removeEventListener("mouseup", handleMouseUp);
503
+ document.removeEventListener("touchmove", handleTouchMove);
504
+ document.removeEventListener("touchend", handleTouchEnd);
505
+ };
506
+ }
507
+ }, [isInteracting, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
508
+ useEffect2(() => {
509
+ if (isValueHovered && !showInput && !isValueEditable) {
510
+ hoverTimeoutRef.current = setTimeout(() => {
511
+ setIsValueEditable(true);
512
+ }, 800);
513
+ } else if (!isValueHovered && !showInput) {
514
+ if (hoverTimeoutRef.current) {
515
+ clearTimeout(hoverTimeoutRef.current);
516
+ hoverTimeoutRef.current = null;
517
+ }
518
+ setIsValueEditable(false);
519
+ }
520
+ return () => {
521
+ if (hoverTimeoutRef.current) {
522
+ clearTimeout(hoverTimeoutRef.current);
523
+ }
524
+ };
525
+ }, [isValueHovered, showInput, isValueEditable]);
526
+ useEffect2(() => {
527
+ if (showInput && inputRef.current) {
528
+ inputRef.current.focus();
529
+ inputRef.current.select();
530
+ }
531
+ }, [showInput]);
532
+ const handleInputChange = (e) => {
533
+ setInputValue(e.target.value);
534
+ };
535
+ const handleInputSubmit = () => {
536
+ const parsed = parseFloat(inputValue);
537
+ if (!isNaN(parsed)) {
538
+ const clamped = Math.max(min, Math.min(max, parsed));
539
+ onChange(clamped);
540
+ }
541
+ setShowInput(false);
542
+ setIsValueHovered(false);
543
+ setIsValueEditable(false);
544
+ };
545
+ const handleValueClick = (e) => {
546
+ if (isValueEditable) {
547
+ e.stopPropagation();
548
+ e.preventDefault();
549
+ setShowInput(true);
550
+ setInputValue(step >= 1 ? value.toFixed(0) : value.toFixed(2));
551
+ }
552
+ };
553
+ const handleInputKeyDown = (e) => {
554
+ if (e.key === "Enter") {
555
+ handleInputSubmit();
556
+ } else if (e.key === "Escape") {
557
+ setShowInput(false);
558
+ setIsValueHovered(false);
559
+ }
560
+ };
561
+ const handleInputBlur = () => {
562
+ handleInputSubmit();
563
+ };
564
+ const displayValue = step >= 1 ? value.toFixed(0) : value.toFixed(2);
565
+ const shouldAnimate = !isDragging;
566
+ const transition = shouldAnimate ? { type: "spring", visualDuration: 0.3, bounce: 0.15 } : { duration: 0 };
567
+ const handleLeft = isAtMinimum ? 7 : `calc(${percentage}% - 9px)`;
568
+ const fillBackground = isActive ? "rgba(255, 255, 255, 0.15)" : "rgba(255, 255, 255, 0.11)";
569
+ const hashMarks = Array.from({ length: 7 }, (_, i) => /* @__PURE__ */ jsx2("span", { className: "dialkit-slider-hashmark", children: "\u2022" }, i));
570
+ return /* @__PURE__ */ jsxs2(
571
+ "div",
572
+ {
573
+ ref: trackRef,
574
+ className: `dialkit-slider ${isActive ? "dialkit-slider-active" : ""}`,
575
+ onMouseDown: handleMouseDown,
576
+ onTouchStart: handleTouchStart,
577
+ onMouseEnter: () => setIsHovered(true),
578
+ onMouseLeave: () => setIsHovered(false),
579
+ children: [
580
+ /* @__PURE__ */ jsx2("div", { className: "dialkit-slider-hashmarks", children: hashMarks }),
581
+ /* @__PURE__ */ jsx2(
582
+ motion2.div,
583
+ {
584
+ className: "dialkit-slider-fill",
585
+ style: { background: fillBackground, width: `${percentage}%`, transition: "background 0.15s" },
586
+ animate: { width: `${percentage}%` },
587
+ transition
588
+ }
589
+ ),
590
+ /* @__PURE__ */ jsx2(
591
+ motion2.div,
592
+ {
593
+ className: "dialkit-slider-handle",
594
+ style: {
595
+ background: isInteracting ? "rgba(255, 255, 255, 1)" : "rgba(255, 255, 255, 0.5)",
596
+ left: handleLeft,
597
+ opacity: isActive ? 1 : 0
598
+ },
599
+ animate: { left: handleLeft, opacity: isActive ? 1 : 0 },
600
+ transition
601
+ }
602
+ ),
603
+ /* @__PURE__ */ jsx2("span", { className: "dialkit-slider-label", children: label }),
604
+ showInput ? /* @__PURE__ */ jsx2(
605
+ "input",
606
+ {
607
+ ref: inputRef,
608
+ type: "text",
609
+ className: "dialkit-slider-input",
610
+ value: inputValue,
611
+ onChange: handleInputChange,
612
+ onKeyDown: handleInputKeyDown,
613
+ onBlur: handleInputBlur,
614
+ onClick: (e) => e.stopPropagation(),
615
+ onMouseDown: (e) => e.stopPropagation()
616
+ }
617
+ ) : /* @__PURE__ */ jsxs2(
618
+ "span",
619
+ {
620
+ className: `dialkit-slider-value ${isValueEditable ? "dialkit-slider-value-editable" : ""}`,
621
+ onMouseEnter: () => setIsValueHovered(true),
622
+ onMouseLeave: () => setIsValueHovered(false),
623
+ onClick: handleValueClick,
624
+ onMouseDown: (e) => isValueEditable && e.stopPropagation(),
625
+ style: { cursor: isValueEditable ? "text" : "default" },
626
+ children: [
627
+ displayValue,
628
+ unit
629
+ ]
630
+ }
631
+ )
632
+ ]
633
+ }
634
+ );
635
+ }
636
+
637
+ // src/components/SegmentedControl.tsx
638
+ import { useRef as useRef3, useState as useState3, useLayoutEffect } from "react";
639
+ import { motion as motion3 } from "motion/react";
640
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
641
+ function SegmentedControl({
642
+ options,
643
+ value,
644
+ onChange
645
+ }) {
646
+ const containerRef = useRef3(null);
647
+ const buttonRefs = useRef3(/* @__PURE__ */ new Map());
648
+ const [pillStyle, setPillStyle] = useState3(null);
649
+ const hasAnimated = useRef3(false);
650
+ useLayoutEffect(() => {
651
+ const button = buttonRefs.current.get(value);
652
+ const container = containerRef.current;
653
+ if (button && container) {
654
+ const containerRect = container.getBoundingClientRect();
655
+ const buttonRect = button.getBoundingClientRect();
656
+ setPillStyle({
657
+ left: buttonRect.left - containerRect.left,
658
+ width: buttonRect.width
659
+ });
660
+ }
661
+ }, [value]);
662
+ return /* @__PURE__ */ jsxs3("div", { ref: containerRef, className: "dialkit-segmented", children: [
663
+ pillStyle && /* @__PURE__ */ jsx3(
664
+ motion3.div,
665
+ {
666
+ className: "dialkit-segmented-pill",
667
+ style: { left: pillStyle.left, width: pillStyle.width },
668
+ animate: { left: pillStyle.left, width: pillStyle.width },
669
+ transition: hasAnimated.current ? { type: "spring", visualDuration: 0.2, bounce: 0.15 } : { duration: 0 },
670
+ onAnimationComplete: () => {
671
+ hasAnimated.current = true;
672
+ }
673
+ }
674
+ ),
675
+ options.map((option) => {
676
+ const isActive = value === option.value;
677
+ return /* @__PURE__ */ jsx3(
678
+ "button",
679
+ {
680
+ ref: (el) => {
681
+ if (el) buttonRefs.current.set(option.value, el);
682
+ },
683
+ onClick: () => onChange(option.value),
684
+ className: "dialkit-segmented-button",
685
+ "data-active": String(isActive),
686
+ children: option.label
687
+ },
688
+ option.value
689
+ );
690
+ })
691
+ ] });
692
+ }
693
+
694
+ // src/components/Toggle.tsx
695
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
696
+ function Toggle({ label, checked, onChange }) {
697
+ return /* @__PURE__ */ jsxs4("div", { className: "dialkit-labeled-control", children: [
698
+ /* @__PURE__ */ jsx4("span", { className: "dialkit-labeled-control-label", children: label }),
699
+ /* @__PURE__ */ jsx4(
700
+ SegmentedControl,
701
+ {
702
+ options: [
703
+ { value: "off", label: "Off" },
704
+ { value: "on", label: "On" }
705
+ ],
706
+ value: checked ? "on" : "off",
707
+ onChange: (val) => onChange(val === "on")
708
+ }
709
+ )
710
+ ] });
711
+ }
712
+
713
+ // src/components/SpringVisualization.tsx
714
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
715
+ function generateSpringCurve(stiffness, damping, mass, duration) {
716
+ const points = [];
717
+ const steps = 100;
718
+ const dt = duration / steps;
719
+ let position = 0;
720
+ let velocity = 0;
721
+ const target = 1;
722
+ for (let i = 0; i <= steps; i++) {
723
+ const time = i * dt;
724
+ points.push([time, position]);
725
+ const springForce = -stiffness * (position - target);
726
+ const dampingForce = -damping * velocity;
727
+ const acceleration = (springForce + dampingForce) / mass;
728
+ velocity += acceleration * dt;
729
+ position += velocity * dt;
730
+ }
731
+ return points;
732
+ }
733
+ function SpringVisualization({ spring, isSimpleMode }) {
734
+ const width = 256;
735
+ const height = 140;
736
+ let stiffness;
737
+ let damping;
738
+ let mass;
739
+ if (isSimpleMode) {
740
+ const visualDuration = spring.visualDuration ?? 0.3;
741
+ const bounce = spring.bounce ?? 0.2;
742
+ mass = 1;
743
+ stiffness = 2 * Math.PI / visualDuration;
744
+ stiffness = Math.pow(stiffness, 2);
745
+ const dampingRatio = 1 - bounce;
746
+ damping = 2 * dampingRatio * Math.sqrt(stiffness * mass);
747
+ } else {
748
+ stiffness = spring.stiffness ?? 400;
749
+ damping = spring.damping ?? 17;
750
+ mass = spring.mass ?? 1;
751
+ }
752
+ const duration = 2;
753
+ const points = generateSpringCurve(stiffness, damping, mass, duration);
754
+ const values = points.map(([, value]) => value);
755
+ const minValue = Math.min(...values);
756
+ const maxValue = Math.max(...values);
757
+ const valueRange = maxValue - minValue;
758
+ const pathData = points.map(([time, value], i) => {
759
+ const x = time / duration * width;
760
+ const normalizedValue = (value - minValue) / (valueRange || 1);
761
+ const y = height - (normalizedValue * height * 0.6 + height * 0.2);
762
+ return `${i === 0 ? "M" : "L"} ${x} ${y}`;
763
+ }).join(" ");
764
+ const gridLines = [];
765
+ for (let i = 1; i < 4; i++) {
766
+ const x = width / 4 * i;
767
+ const y = height / 4 * i;
768
+ gridLines.push(
769
+ /* @__PURE__ */ jsx5("line", { x1: x, y1: 0, x2: x, y2: height, stroke: "rgba(255, 255, 255, 0.08)", strokeWidth: "1" }, `v-${i}`),
770
+ /* @__PURE__ */ jsx5("line", { x1: 0, y1: y, x2: width, y2: y, stroke: "rgba(255, 255, 255, 0.08)", strokeWidth: "1" }, `h-${i}`)
771
+ );
772
+ }
773
+ return /* @__PURE__ */ jsxs5("svg", { viewBox: `0 0 ${width} ${height}`, className: "dialkit-spring-viz", children: [
774
+ gridLines,
775
+ /* @__PURE__ */ jsx5(
776
+ "line",
777
+ {
778
+ x1: 0,
779
+ y1: height / 2,
780
+ x2: width,
781
+ y2: height / 2,
782
+ stroke: "rgba(255, 255, 255, 0.15)",
783
+ strokeWidth: "1",
784
+ strokeDasharray: "4,4"
785
+ }
786
+ ),
787
+ /* @__PURE__ */ jsx5(
788
+ "path",
789
+ {
790
+ d: pathData,
791
+ fill: "none",
792
+ stroke: "rgba(255, 255, 255, 0.6)",
793
+ strokeWidth: "2",
794
+ strokeLinecap: "round",
795
+ strokeLinejoin: "round"
796
+ }
797
+ )
798
+ ] });
799
+ }
800
+
801
+ // src/components/SpringControl.tsx
802
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
803
+ import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
804
+ function SpringControl({ panelId, path, label, spring, onChange }) {
805
+ const mode = useSyncExternalStore2(
806
+ (cb) => DialStore.subscribe(panelId, cb),
807
+ () => DialStore.getSpringMode(panelId, path),
808
+ () => DialStore.getSpringMode(panelId, path)
809
+ );
810
+ const isSimpleMode = mode === "simple";
811
+ const handleModeChange = (newMode) => {
812
+ DialStore.updateSpringMode(panelId, path, newMode);
813
+ if (newMode === "simple") {
814
+ const { stiffness, damping, mass, ...rest } = spring;
815
+ onChange({
816
+ ...rest,
817
+ type: "spring",
818
+ visualDuration: spring.visualDuration ?? 0.3,
819
+ bounce: spring.bounce ?? 0.2
820
+ });
821
+ } else {
822
+ const { visualDuration, bounce, ...rest } = spring;
823
+ onChange({
824
+ ...rest,
825
+ type: "spring",
826
+ stiffness: spring.stiffness ?? 200,
827
+ damping: spring.damping ?? 25,
828
+ mass: spring.mass ?? 1
829
+ });
830
+ }
831
+ };
832
+ const handleUpdate = (key, value) => {
833
+ if (isSimpleMode) {
834
+ const { stiffness, damping, mass, ...rest } = spring;
835
+ onChange({ ...rest, [key]: value });
836
+ } else {
837
+ const { visualDuration, bounce, ...rest } = spring;
838
+ onChange({ ...rest, [key]: value });
839
+ }
840
+ };
841
+ return /* @__PURE__ */ jsx6(Folder, { title: label, defaultOpen: true, children: /* @__PURE__ */ jsxs6("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
842
+ /* @__PURE__ */ jsx6(SpringVisualization, { spring, isSimpleMode }),
843
+ /* @__PURE__ */ jsxs6("div", { className: "dialkit-labeled-control", children: [
844
+ /* @__PURE__ */ jsx6("span", { className: "dialkit-labeled-control-label", children: "Type" }),
845
+ /* @__PURE__ */ jsx6(
846
+ SegmentedControl,
847
+ {
848
+ options: [
849
+ { value: "simple", label: "Time" },
850
+ { value: "advanced", label: "Physics" }
851
+ ],
852
+ value: mode,
853
+ onChange: handleModeChange
854
+ }
855
+ )
856
+ ] }),
857
+ isSimpleMode ? /* @__PURE__ */ jsxs6(Fragment, { children: [
858
+ /* @__PURE__ */ jsx6(
859
+ Slider,
860
+ {
861
+ label: "Duration",
862
+ value: spring.visualDuration ?? 0.3,
863
+ onChange: (v) => handleUpdate("visualDuration", v),
864
+ min: 0.1,
865
+ max: 1,
866
+ step: 0.05,
867
+ unit: "s"
868
+ }
869
+ ),
870
+ /* @__PURE__ */ jsx6(
871
+ Slider,
872
+ {
873
+ label: "Bounce",
874
+ value: spring.bounce ?? 0.2,
875
+ onChange: (v) => handleUpdate("bounce", v),
876
+ min: 0,
877
+ max: 1,
878
+ step: 0.05
879
+ }
880
+ )
881
+ ] }) : /* @__PURE__ */ jsxs6(Fragment, { children: [
882
+ /* @__PURE__ */ jsx6(
883
+ Slider,
884
+ {
885
+ label: "Stiffness",
886
+ value: spring.stiffness ?? 400,
887
+ onChange: (v) => handleUpdate("stiffness", v),
888
+ min: 1,
889
+ max: 1e3,
890
+ step: 10
891
+ }
892
+ ),
893
+ /* @__PURE__ */ jsx6(
894
+ Slider,
895
+ {
896
+ label: "Damping",
897
+ value: spring.damping ?? 17,
898
+ onChange: (v) => handleUpdate("damping", v),
899
+ min: 1,
900
+ max: 100,
901
+ step: 1
902
+ }
903
+ ),
904
+ /* @__PURE__ */ jsx6(
905
+ Slider,
906
+ {
907
+ label: "Mass",
908
+ value: spring.mass ?? 1,
909
+ onChange: (v) => handleUpdate("mass", v),
910
+ min: 0.1,
911
+ max: 10,
912
+ step: 0.1
913
+ }
914
+ )
915
+ ] })
916
+ ] }) });
917
+ }
918
+
919
+ // src/components/Panel.tsx
920
+ import { useSyncExternalStore as useSyncExternalStore3 } from "react";
921
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
922
+ function Panel({ panel }) {
923
+ const values = useSyncExternalStore3(
924
+ (cb) => DialStore.subscribe(panel.id, cb),
925
+ () => DialStore.getValues(panel.id),
926
+ () => DialStore.getValues(panel.id)
927
+ );
928
+ const renderControl = (control) => {
929
+ const value = values[control.path];
930
+ switch (control.type) {
931
+ case "slider":
932
+ return /* @__PURE__ */ jsx7(
933
+ Slider,
934
+ {
935
+ label: control.label,
936
+ value,
937
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v),
938
+ min: control.min,
939
+ max: control.max,
940
+ step: control.step
941
+ },
942
+ control.path
943
+ );
944
+ case "toggle":
945
+ return /* @__PURE__ */ jsx7(
946
+ Toggle,
947
+ {
948
+ label: control.label,
949
+ checked: value,
950
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
951
+ },
952
+ control.path
953
+ );
954
+ case "spring":
955
+ return /* @__PURE__ */ jsx7(
956
+ SpringControl,
957
+ {
958
+ panelId: panel.id,
959
+ path: control.path,
960
+ label: control.label,
961
+ spring: value,
962
+ onChange: (v) => DialStore.updateValue(panel.id, control.path, v)
963
+ },
964
+ control.path
965
+ );
966
+ case "folder":
967
+ return /* @__PURE__ */ jsx7(Folder, { title: control.label, children: control.children?.map(renderControl) }, control.path);
968
+ default:
969
+ return null;
970
+ }
971
+ };
972
+ const renderControls = () => {
973
+ const result = [];
974
+ let i = 0;
975
+ while (i < panel.controls.length) {
976
+ const control = panel.controls[i];
977
+ if (control.type === "action") {
978
+ const actions = [control];
979
+ while (i + 1 < panel.controls.length && panel.controls[i + 1].type === "action") {
980
+ i++;
981
+ actions.push(panel.controls[i]);
982
+ }
983
+ result.push(
984
+ /* @__PURE__ */ jsxs7("div", { className: "dialkit-labeled-control dialkit-actions-group", children: [
985
+ /* @__PURE__ */ jsx7("span", { className: "dialkit-labeled-control-label", children: "Actions" }),
986
+ /* @__PURE__ */ jsx7("div", { className: "dialkit-actions-stack", children: actions.map((action) => /* @__PURE__ */ jsx7(
987
+ "button",
988
+ {
989
+ className: "dialkit-action-button",
990
+ onClick: () => DialStore.triggerAction(panel.id, action.path),
991
+ children: action.label
992
+ },
993
+ action.path
994
+ )) })
995
+ ] }, `actions-${actions[0].path}`)
996
+ );
997
+ } else {
998
+ result.push(renderControl(control));
999
+ }
1000
+ i++;
1001
+ }
1002
+ return result;
1003
+ };
1004
+ return /* @__PURE__ */ jsx7(Folder, { title: panel.name, defaultOpen: true, isRoot: true, panelId: panel.id, children: renderControls() });
1005
+ }
1006
+
1007
+ // src/components/DialRoot.tsx
1008
+ import { jsx as jsx8 } from "react/jsx-runtime";
1009
+ function DialRoot({ position = "top-right" }) {
1010
+ const [panels, setPanels] = useState4([]);
1011
+ const [mounted, setMounted] = useState4(false);
1012
+ useEffect3(() => {
1013
+ setMounted(true);
1014
+ setPanels(DialStore.getPanels());
1015
+ const unsubscribe = DialStore.subscribeGlobal(() => {
1016
+ setPanels(DialStore.getPanels());
1017
+ });
1018
+ return unsubscribe;
1019
+ }, []);
1020
+ if (!mounted || typeof window === "undefined") {
1021
+ return null;
1022
+ }
1023
+ if (panels.length === 0) {
1024
+ return null;
1025
+ }
1026
+ 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)) }) });
1027
+ return createPortal(content, document.body);
1028
+ }
1029
+
1030
+ // src/components/ButtonGroup.tsx
1031
+ import { jsx as jsx9 } from "react/jsx-runtime";
1032
+ function ButtonGroup({ buttons }) {
1033
+ return /* @__PURE__ */ jsx9("div", { className: "dialkit-button-group", children: buttons.map((button, index) => /* @__PURE__ */ jsx9(
1034
+ "button",
1035
+ {
1036
+ className: "dialkit-button",
1037
+ onClick: button.onClick,
1038
+ children: button.label
1039
+ },
1040
+ index
1041
+ )) });
1042
+ }
1043
+ export {
1044
+ ButtonGroup,
1045
+ DialRoot,
1046
+ DialStore,
1047
+ Folder,
1048
+ SegmentedControl,
1049
+ Slider,
1050
+ SpringControl,
1051
+ SpringVisualization,
1052
+ Toggle,
1053
+ useDialKit
1054
+ };
1055
+ //# sourceMappingURL=index.js.map