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