dialkit 1.2.1 → 1.3.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.
Files changed (55) hide show
  1. package/README.md +220 -3
  2. package/dist/dropdown-position.d.ts +15 -0
  3. package/dist/dropdown-position.js +22 -0
  4. package/dist/dropdown-position.js.map +1 -0
  5. package/dist/index.cjs +697 -307
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +42 -5
  8. package/dist/index.d.ts +42 -5
  9. package/dist/index.js +720 -331
  10. package/dist/index.js.map +1 -1
  11. package/dist/panel-drag.d.ts +19 -0
  12. package/dist/panel-drag.js +72 -0
  13. package/dist/panel-drag.js.map +1 -0
  14. package/dist/solid/index.cjs +661 -204
  15. package/dist/solid/index.cjs.map +1 -1
  16. package/dist/solid/index.d.cts +40 -3
  17. package/dist/solid/index.d.ts +40 -3
  18. package/dist/solid/index.js +646 -190
  19. package/dist/solid/index.js.map +1 -1
  20. package/dist/store/index.cjs +272 -41
  21. package/dist/store/index.cjs.map +1 -1
  22. package/dist/store/index.d.cts +30 -3
  23. package/dist/store/index.d.ts +30 -3
  24. package/dist/store/index.js +269 -40
  25. package/dist/store/index.js.map +1 -1
  26. package/dist/styles.css +105 -7
  27. package/dist/svelte/components/DialRoot.svelte +176 -11
  28. package/dist/svelte/components/DialRoot.svelte.d.ts +1 -0
  29. package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -1
  30. package/dist/svelte/components/Folder.svelte +24 -13
  31. package/dist/svelte/components/Folder.svelte.d.ts +1 -0
  32. package/dist/svelte/components/Folder.svelte.d.ts.map +1 -1
  33. package/dist/svelte/components/Panel.svelte +98 -71
  34. package/dist/svelte/components/Panel.svelte.d.ts +2 -0
  35. package/dist/svelte/components/Panel.svelte.d.ts.map +1 -1
  36. package/dist/svelte/components/PresetManager.svelte +5 -5
  37. package/dist/svelte/components/PresetManager.svelte.d.ts.map +1 -1
  38. package/dist/svelte/components/SelectControl.svelte +6 -14
  39. package/dist/svelte/components/SelectControl.svelte.d.ts.map +1 -1
  40. package/dist/svelte/createDialKit.svelte.d.ts +11 -1
  41. package/dist/svelte/createDialKit.svelte.d.ts.map +1 -1
  42. package/dist/svelte/createDialKit.svelte.js +61 -34
  43. package/dist/svelte/index.d.ts +3 -3
  44. package/dist/svelte/index.d.ts.map +1 -1
  45. package/dist/svelte/index.js +1 -1
  46. package/dist/svelte/theme-css.d.ts +1 -1
  47. package/dist/svelte/theme-css.d.ts.map +1 -1
  48. package/dist/svelte/theme-css.js +105 -7
  49. package/dist/vue/index.cjs +573 -132
  50. package/dist/vue/index.cjs.map +1 -1
  51. package/dist/vue/index.d.cts +53 -6
  52. package/dist/vue/index.d.ts +53 -6
  53. package/dist/vue/index.js +573 -133
  54. package/dist/vue/index.js.map +1 -1
  55. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,10 +1,93 @@
1
1
  "use client";
2
2
 
3
3
  // src/hooks/useDialKit.ts
4
- import { useEffect, useId, useSyncExternalStore, useRef } from "react";
4
+ import { useCallback, useEffect, useId, useMemo, useRef, useSyncExternalStore } from "react";
5
5
 
6
6
  // src/store/DialStore.ts
7
7
  var EMPTY_VALUES = Object.freeze({});
8
+ function resolveDialValues(config, flatValues) {
9
+ return resolveConfigValues(config, flatValues, "");
10
+ }
11
+ function flattenDialValueUpdates(config, updates) {
12
+ const values = {};
13
+ if (typeof updates === "object" && updates !== null) {
14
+ flattenConfigUpdates(config, updates, "", values);
15
+ }
16
+ return values;
17
+ }
18
+ function resolveConfigValues(config, flatValues, prefix) {
19
+ const result = {};
20
+ for (const [key, configValue] of Object.entries(config)) {
21
+ if (key === "_collapsed") continue;
22
+ const path = prefix ? `${prefix}.${key}` : key;
23
+ if (Array.isArray(configValue) && configValue.length <= 4 && typeof configValue[0] === "number") {
24
+ result[key] = flatValues[path] ?? configValue[0];
25
+ } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
26
+ result[key] = flatValues[path] ?? configValue;
27
+ } else if (isSpringConfigValue(configValue) || isEasingConfigValue(configValue)) {
28
+ result[key] = flatValues[path] ?? configValue;
29
+ } else if (isActionConfigValue(configValue)) {
30
+ result[key] = flatValues[path] ?? configValue;
31
+ } else if (isSelectConfigValue(configValue)) {
32
+ const defaultValue = configValue.default ?? getFirstOptionValue(configValue.options);
33
+ result[key] = flatValues[path] ?? defaultValue;
34
+ } else if (isColorConfigValue(configValue)) {
35
+ result[key] = flatValues[path] ?? configValue.default ?? "#000000";
36
+ } else if (isTextConfigValue(configValue)) {
37
+ result[key] = flatValues[path] ?? configValue.default ?? "";
38
+ } else if (typeof configValue === "object" && configValue !== null) {
39
+ result[key] = resolveConfigValues(configValue, flatValues, path);
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+ function flattenConfigUpdates(config, updates, prefix, values) {
45
+ for (const [key, configValue] of Object.entries(config)) {
46
+ if (key === "_collapsed" || !(key in updates)) continue;
47
+ const nextValue = updates[key];
48
+ if (nextValue === void 0) continue;
49
+ const path = prefix ? `${prefix}.${key}` : key;
50
+ if (isActionConfigValue(configValue)) {
51
+ continue;
52
+ }
53
+ if (isLeafConfigValue(configValue)) {
54
+ values[path] = nextValue;
55
+ continue;
56
+ }
57
+ if (typeof configValue === "object" && configValue !== null && typeof nextValue === "object" && nextValue !== null && !Array.isArray(nextValue)) {
58
+ flattenConfigUpdates(configValue, nextValue, path, values);
59
+ }
60
+ }
61
+ }
62
+ function isLeafConfigValue(value) {
63
+ return Array.isArray(value) && value.length <= 4 && typeof value[0] === "number" || typeof value === "number" || typeof value === "boolean" || typeof value === "string" || isSpringConfigValue(value) || isEasingConfigValue(value) || isActionConfigValue(value) || isSelectConfigValue(value) || isColorConfigValue(value) || isTextConfigValue(value);
64
+ }
65
+ function hasType(value, type) {
66
+ return typeof value === "object" && value !== null && "type" in value && value.type === type;
67
+ }
68
+ function isSpringConfigValue(value) {
69
+ return hasType(value, "spring");
70
+ }
71
+ function isEasingConfigValue(value) {
72
+ return hasType(value, "easing");
73
+ }
74
+ function isActionConfigValue(value) {
75
+ return hasType(value, "action");
76
+ }
77
+ function isSelectConfigValue(value) {
78
+ return hasType(value, "select") && "options" in value && Array.isArray(value.options);
79
+ }
80
+ function isColorConfigValue(value) {
81
+ return hasType(value, "color");
82
+ }
83
+ function isTextConfigValue(value) {
84
+ return hasType(value, "text");
85
+ }
86
+ function getFirstOptionValue(options) {
87
+ const first = options[0];
88
+ if (first === void 0) return "";
89
+ return typeof first === "string" ? first : first.value;
90
+ }
8
91
  var DialStoreClass = class {
9
92
  constructor() {
10
93
  this.panels = /* @__PURE__ */ new Map();
@@ -15,87 +98,138 @@ var DialStoreClass = class {
15
98
  this.presets = /* @__PURE__ */ new Map();
16
99
  this.activePreset = /* @__PURE__ */ new Map();
17
100
  this.baseValues = /* @__PURE__ */ new Map();
101
+ this.defaultValues = /* @__PURE__ */ new Map();
102
+ this.registrationCounts = /* @__PURE__ */ new Map();
103
+ this.retainedPanels = /* @__PURE__ */ new Set();
104
+ this.persistConfigs = /* @__PURE__ */ new Map();
18
105
  }
19
- registerPanel(id, name, config, shortcuts) {
106
+ registerPanel(id, name, config, shortcuts, options = {}) {
107
+ this.configurePanelRetention(id, options);
108
+ this.registrationCounts.set(id, (this.registrationCounts.get(id) ?? 0) + 1);
20
109
  const controls = this.parseConfig(config, "", shortcuts);
21
- const values = this.flattenValues(config, "");
22
- this.initTransitionModes(config, "", values);
110
+ const controlsByPath = this.mapControlsByPath(controls);
111
+ const defaultValues = this.flattenValues(config, "");
112
+ this.initTransitionModes(config, "", defaultValues);
113
+ const persisted = this.loadPersistedPanel(id);
114
+ const previousValues = this.panels.get(id)?.values ?? this.snapshots.get(id) ?? persisted?.values ?? {};
115
+ const values = this.reconcileValues(defaultValues, previousValues, controlsByPath);
116
+ const previousBaseValues = this.baseValues.get(id) ?? persisted?.baseValues ?? persisted?.values ?? {};
117
+ const baseValues = this.reconcileValues(defaultValues, previousBaseValues, controlsByPath);
23
118
  this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {} });
24
119
  this.snapshots.set(id, { ...values });
25
- this.baseValues.set(id, { ...values });
120
+ this.baseValues.set(id, baseValues);
121
+ this.defaultValues.set(id, { ...defaultValues });
122
+ const existingPresets = this.presets.get(id) ?? persisted?.presets;
123
+ if (existingPresets) {
124
+ this.presets.set(id, this.reconcilePresets(existingPresets, defaultValues, controlsByPath));
125
+ }
126
+ if (!this.activePreset.has(id) && persisted?.activePresetId !== void 0) {
127
+ this.activePreset.set(id, persisted.activePresetId);
128
+ }
129
+ this.persistPanel(id);
130
+ this.notify(id);
26
131
  this.notifyGlobal();
27
132
  }
28
- updatePanel(id, name, config, shortcuts) {
133
+ updatePanel(id, name, config, shortcuts, options = {}) {
134
+ this.configurePanelRetention(id, options);
29
135
  const existing = this.panels.get(id);
30
136
  if (!existing) {
31
- this.registerPanel(id, name, config, shortcuts);
137
+ this.registerPanel(id, name, config, shortcuts, options);
32
138
  return;
33
139
  }
34
140
  const controls = this.parseConfig(config, "", shortcuts);
35
141
  const controlsByPath = this.mapControlsByPath(controls);
36
142
  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
- }
143
+ this.initTransitionModes(config, "", defaultValues);
144
+ const nextValues = this.reconcileValues(defaultValues, existing.values, controlsByPath);
56
145
  const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts };
57
146
  this.panels.set(id, nextPanel);
58
147
  this.snapshots.set(id, { ...nextValues });
59
148
  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
- }
149
+ const nextBaseValues = this.reconcileValues(defaultValues, previousBaseValues, controlsByPath);
68
150
  for (const [path, value] of Object.entries(nextValues)) {
69
151
  if (path.endsWith(".__mode")) {
70
152
  nextBaseValues[path] = value;
71
153
  }
72
154
  }
73
155
  this.baseValues.set(id, nextBaseValues);
156
+ this.defaultValues.set(id, { ...defaultValues });
157
+ this.presets.set(id, this.reconcilePresets(this.presets.get(id) ?? [], defaultValues, controlsByPath));
158
+ this.persistPanel(id);
74
159
  this.notify(id);
75
160
  this.notifyGlobal();
76
161
  }
77
162
  unregisterPanel(id) {
163
+ const nextCount = (this.registrationCounts.get(id) ?? 1) - 1;
164
+ if (nextCount > 0) {
165
+ this.registrationCounts.set(id, nextCount);
166
+ return;
167
+ }
168
+ this.registrationCounts.delete(id);
78
169
  this.panels.delete(id);
79
170
  this.listeners.delete(id);
80
- this.snapshots.delete(id);
81
171
  this.actionListeners.delete(id);
82
- this.baseValues.delete(id);
172
+ if (!this.retainedPanels.has(id)) {
173
+ this.snapshots.delete(id);
174
+ this.baseValues.delete(id);
175
+ this.defaultValues.delete(id);
176
+ this.presets.delete(id);
177
+ this.activePreset.delete(id);
178
+ this.persistConfigs.delete(id);
179
+ }
83
180
  this.notifyGlobal();
84
181
  }
85
182
  updateValue(panelId, path, value) {
183
+ this.updateValues(panelId, { [path]: value });
184
+ }
185
+ updateValues(panelId, updates) {
86
186
  const panel = this.panels.get(panelId);
87
187
  if (!panel) return;
88
- panel.values[path] = value;
188
+ const validUpdates = {};
189
+ for (const [path, value] of Object.entries(updates)) {
190
+ if (!Object.prototype.hasOwnProperty.call(panel.values, path)) {
191
+ continue;
192
+ }
193
+ const control = this.findControlByPath(panel.controls, path);
194
+ if (control?.type === "action") {
195
+ continue;
196
+ }
197
+ panel.values[path] = value;
198
+ validUpdates[path] = value;
199
+ }
200
+ if (Object.keys(validUpdates).length === 0) {
201
+ return;
202
+ }
89
203
  const activeId = this.activePreset.get(panelId);
90
204
  if (activeId) {
91
205
  const presets = this.presets.get(panelId) ?? [];
92
206
  const preset = presets.find((p) => p.id === activeId);
93
- if (preset) preset.values[path] = value;
207
+ if (preset) {
208
+ for (const [path, value] of Object.entries(validUpdates)) {
209
+ preset.values[path] = value;
210
+ }
211
+ }
94
212
  } else {
95
213
  const base = this.baseValues.get(panelId);
96
- if (base) base[path] = value;
214
+ if (base) {
215
+ for (const [path, value] of Object.entries(validUpdates)) {
216
+ base[path] = value;
217
+ }
218
+ }
97
219
  }
98
220
  this.snapshots.set(panelId, { ...panel.values });
221
+ this.persistPanel(panelId);
222
+ this.notify(panelId);
223
+ }
224
+ resetValues(panelId) {
225
+ const panel = this.panels.get(panelId);
226
+ const defaults = this.defaultValues.get(panelId);
227
+ if (!panel || !defaults) return;
228
+ panel.values = { ...defaults };
229
+ this.snapshots.set(panelId, { ...panel.values });
230
+ this.baseValues.set(panelId, { ...defaults });
231
+ this.activePreset.set(panelId, null);
232
+ this.persistPanel(panelId);
99
233
  this.notify(panelId);
100
234
  }
101
235
  updateSpringMode(panelId, path, mode) {
@@ -111,6 +245,7 @@ var DialStoreClass = class {
111
245
  if (!panel) return;
112
246
  panel.values[`${path}.__mode`] = mode;
113
247
  this.snapshots.set(panelId, { ...panel.values });
248
+ this.persistPanel(panelId);
114
249
  this.notify(panelId);
115
250
  }
116
251
  getTransitionMode(panelId, path) {
@@ -169,6 +304,7 @@ var DialStoreClass = class {
169
304
  this.presets.set(panelId, [...existing, preset]);
170
305
  this.activePreset.set(panelId, id);
171
306
  this.snapshots.set(panelId, { ...panel.values });
307
+ this.persistPanel(panelId);
172
308
  this.notify(panelId);
173
309
  return id;
174
310
  }
@@ -181,6 +317,7 @@ var DialStoreClass = class {
181
317
  panel.values = { ...preset.values };
182
318
  this.snapshots.set(panelId, { ...panel.values });
183
319
  this.activePreset.set(panelId, presetId);
320
+ this.persistPanel(panelId);
184
321
  this.notify(panelId);
185
322
  }
186
323
  deletePreset(panelId, presetId) {
@@ -193,6 +330,7 @@ var DialStoreClass = class {
193
330
  if (panel) {
194
331
  this.snapshots.set(panelId, { ...panel.values });
195
332
  }
333
+ this.persistPanel(panelId);
196
334
  this.notify(panelId);
197
335
  }
198
336
  getPresets(panelId) {
@@ -209,6 +347,7 @@ var DialStoreClass = class {
209
347
  this.snapshots.set(panelId, { ...panel.values });
210
348
  }
211
349
  this.activePreset.set(panelId, null);
350
+ this.persistPanel(panelId);
212
351
  this.notify(panelId);
213
352
  }
214
353
  resolveShortcutTarget(key, modifier) {
@@ -239,6 +378,94 @@ var DialStoreClass = class {
239
378
  }
240
379
  return results;
241
380
  }
381
+ configurePanelRetention(id, options) {
382
+ if (options.retainOnUnmount) {
383
+ this.retainedPanels.add(id);
384
+ }
385
+ const persistConfig = this.normalizePersistConfig(id, options.persist);
386
+ if (persistConfig) {
387
+ this.persistConfigs.set(id, persistConfig);
388
+ this.retainedPanels.add(id);
389
+ }
390
+ }
391
+ reconcileValues(defaultValues, previousValues, controlsByPath) {
392
+ const nextValues = {};
393
+ for (const [path, defaultValue] of Object.entries(defaultValues)) {
394
+ if (path.endsWith(".__mode")) {
395
+ const transitionPath = path.slice(0, -".__mode".length);
396
+ const transitionControl = controlsByPath.get(transitionPath);
397
+ nextValues[path] = transitionControl?.type === "transition" && previousValues[path] !== void 0 ? previousValues[path] : defaultValue;
398
+ continue;
399
+ }
400
+ nextValues[path] = this.normalizePreservedValue(
401
+ previousValues[path],
402
+ defaultValue,
403
+ controlsByPath.get(path)
404
+ );
405
+ }
406
+ return nextValues;
407
+ }
408
+ reconcilePresets(presets, defaultValues, controlsByPath) {
409
+ return presets.map((preset) => ({
410
+ ...preset,
411
+ values: this.reconcileValues(defaultValues, preset.values, controlsByPath)
412
+ }));
413
+ }
414
+ normalizePersistConfig(id, persist) {
415
+ if (!persist) return null;
416
+ const options = typeof persist === "object" ? persist : {};
417
+ return {
418
+ key: options.key ?? `dialkit:${id}`,
419
+ storage: options.storage ?? "localStorage",
420
+ presets: options.presets ?? true
421
+ };
422
+ }
423
+ loadPersistedPanel(id) {
424
+ const config = this.persistConfigs.get(id);
425
+ if (!config) return null;
426
+ const storage = this.getStorage(config.storage);
427
+ if (!storage) return null;
428
+ try {
429
+ const raw = storage.getItem(config.key);
430
+ if (!raw) return null;
431
+ const parsed = JSON.parse(raw);
432
+ if (parsed?.version !== 1 || typeof parsed !== "object") return null;
433
+ return parsed;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+ persistPanel(id) {
439
+ const config = this.persistConfigs.get(id);
440
+ if (!config) return;
441
+ const storage = this.getStorage(config.storage);
442
+ if (!storage) return;
443
+ const values = this.snapshots.get(id) ?? this.panels.get(id)?.values;
444
+ if (!values) return;
445
+ const state = {
446
+ version: 1,
447
+ values,
448
+ baseValues: this.baseValues.get(id) ?? values,
449
+ activePresetId: this.activePreset.get(id) ?? null
450
+ };
451
+ if (config.presets) {
452
+ state.presets = this.presets.get(id) ?? [];
453
+ }
454
+ try {
455
+ storage.setItem(config.key, JSON.stringify(state));
456
+ } catch {
457
+ }
458
+ }
459
+ getStorage(kind) {
460
+ if (typeof globalThis === "undefined" || !("window" in globalThis)) {
461
+ return null;
462
+ }
463
+ try {
464
+ return kind === "sessionStorage" ? globalThis.window?.sessionStorage ?? null : globalThis.window?.localStorage ?? null;
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
242
469
  findControlByPath(controls, path) {
243
470
  for (const control of controls) {
244
471
  if (control.path === path) return control;
@@ -467,8 +694,12 @@ var DialStore = new DialStoreClass();
467
694
 
468
695
  // src/hooks/useDialKit.ts
469
696
  function useDialKit(name, config, options) {
697
+ return useDialKitController(name, config, options).values;
698
+ }
699
+ function useDialKitController(name, config, options) {
470
700
  const instanceId = useId();
471
- const panelId = `${name}-${instanceId}`;
701
+ const hasStableId = options?.id !== void 0;
702
+ const panelId = options?.id ?? `${name}-${instanceId}`;
472
703
  const configRef = useRef(config);
473
704
  const serializedConfig = JSON.stringify(config);
474
705
  configRef.current = config;
@@ -476,93 +707,236 @@ function useDialKit(name, config, options) {
476
707
  onActionRef.current = options?.onAction;
477
708
  const shortcutsRef = useRef(options?.shortcuts);
478
709
  shortcutsRef.current = options?.shortcuts;
710
+ const persistRef = useRef(options?.persist);
711
+ persistRef.current = options?.persist;
479
712
  const serializedShortcuts = JSON.stringify(options?.shortcuts);
713
+ const serializedPersist = JSON.stringify(options?.persist);
480
714
  useEffect(() => {
481
- DialStore.registerPanel(panelId, name, configRef.current, shortcutsRef.current);
715
+ DialStore.registerPanel(panelId, name, configRef.current, shortcutsRef.current, {
716
+ retainOnUnmount: hasStableId,
717
+ persist: persistRef.current
718
+ });
482
719
  return () => DialStore.unregisterPanel(panelId);
483
- }, [panelId, name]);
720
+ }, [hasStableId, panelId, name]);
484
721
  const mountedRef = useRef(false);
485
722
  useEffect(() => {
486
723
  if (!mountedRef.current) {
487
724
  mountedRef.current = true;
488
725
  return;
489
726
  }
490
- DialStore.updatePanel(panelId, name, configRef.current, shortcutsRef.current);
491
- }, [panelId, name, serializedConfig, serializedShortcuts]);
727
+ DialStore.updatePanel(panelId, name, configRef.current, shortcutsRef.current, {
728
+ retainOnUnmount: hasStableId,
729
+ persist: persistRef.current
730
+ });
731
+ }, [hasStableId, panelId, name, serializedConfig, serializedShortcuts, serializedPersist]);
492
732
  useEffect(() => {
493
733
  return DialStore.subscribeActions(panelId, (action) => {
494
734
  onActionRef.current?.(action);
495
735
  });
496
736
  }, [panelId]);
497
- const values = useSyncExternalStore(
737
+ const subscribe = useCallback(
498
738
  (callback) => DialStore.subscribe(panelId, callback),
739
+ [panelId]
740
+ );
741
+ const getSnapshot = useCallback(
499
742
  () => DialStore.getValues(panelId),
500
- () => DialStore.getValues(panelId)
743
+ [panelId]
744
+ );
745
+ const flatValues = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
746
+ const values = useMemo(
747
+ () => resolveDialValues(configRef.current, flatValues),
748
+ [flatValues, serializedConfig]
749
+ );
750
+ const setValue = useCallback(
751
+ (path, value) => {
752
+ DialStore.updateValue(panelId, path, value);
753
+ },
754
+ [panelId]
755
+ );
756
+ const setValues = useCallback(
757
+ (nextValues) => {
758
+ DialStore.updateValues(panelId, flattenDialValueUpdates(configRef.current, nextValues));
759
+ },
760
+ [panelId]
761
+ );
762
+ const resetValues = useCallback(() => {
763
+ DialStore.resetValues(panelId);
764
+ }, [panelId]);
765
+ const getValues = useCallback(
766
+ () => resolveDialValues(configRef.current, DialStore.getValues(panelId)),
767
+ [panelId]
768
+ );
769
+ return useMemo(
770
+ () => ({
771
+ values,
772
+ setValue,
773
+ setValues,
774
+ resetValues,
775
+ getValues
776
+ }),
777
+ [getValues, resetValues, setValue, setValues, values]
501
778
  );
502
- return buildResolvedValues(config, values, "");
503
- }
504
- function buildResolvedValues(config, flatValues, prefix) {
505
- const result = {};
506
- for (const [key, configValue] of Object.entries(config)) {
507
- if (key === "_collapsed") continue;
508
- const path = prefix ? `${prefix}.${key}` : key;
509
- if (Array.isArray(configValue) && configValue.length <= 4 && typeof configValue[0] === "number") {
510
- result[key] = flatValues[path] ?? configValue[0];
511
- } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
512
- result[key] = flatValues[path] ?? configValue;
513
- } else if (isSpringConfig(configValue) || isEasingConfig(configValue)) {
514
- result[key] = flatValues[path] ?? configValue;
515
- } else if (isActionConfig(configValue)) {
516
- result[key] = flatValues[path] ?? configValue;
517
- } else if (isSelectConfig(configValue)) {
518
- const defaultValue = configValue.default ?? getFirstOptionValue(configValue.options);
519
- result[key] = flatValues[path] ?? defaultValue;
520
- } else if (isColorConfig(configValue)) {
521
- result[key] = flatValues[path] ?? configValue.default ?? "#000000";
522
- } else if (isTextConfig(configValue)) {
523
- result[key] = flatValues[path] ?? configValue.default ?? "";
524
- } else if (typeof configValue === "object" && configValue !== null) {
525
- result[key] = buildResolvedValues(configValue, flatValues, path);
526
- }
527
- }
528
- return result;
529
- }
530
- function hasType(value, type) {
531
- return typeof value === "object" && value !== null && "type" in value && value.type === type;
532
- }
533
- function isSpringConfig(value) {
534
- return hasType(value, "spring");
535
- }
536
- function isEasingConfig(value) {
537
- return hasType(value, "easing");
538
- }
539
- function isActionConfig(value) {
540
- return hasType(value, "action");
541
- }
542
- function isSelectConfig(value) {
543
- return hasType(value, "select") && "options" in value && Array.isArray(value.options);
544
- }
545
- function isColorConfig(value) {
546
- return hasType(value, "color");
547
- }
548
- function isTextConfig(value) {
549
- return hasType(value, "text");
550
- }
551
- function getFirstOptionValue(options) {
552
- const first = options[0];
553
- return typeof first === "string" ? first : first.value;
554
779
  }
555
780
 
556
781
  // src/components/DialRoot.tsx
557
- import { useEffect as useEffect8, useState as useState10, useRef as useRef11, useCallback as useCallback6 } from "react";
782
+ import { useEffect as useEffect8, useState as useState10, useRef as useRef11, useCallback as useCallback10 } from "react";
558
783
  import { createPortal as createPortal3 } from "react-dom";
559
784
 
785
+ // src/components/Folder.tsx
786
+ import { useState, useRef as useRef2, useEffect as useEffect2 } from "react";
787
+ import { motion, AnimatePresence } from "motion/react";
788
+
789
+ // src/icons.ts
790
+ var ICON_CHEVRON = "M6 9.5L12 15.5L18 9.5";
791
+ var ICON_CHECK = "M5 12.75L10 19L19 5";
792
+ var ICON_CLIPBOARD = {
793
+ board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
794
+ sparkle: "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",
795
+ body: "M16 5H17C18.6569 5 20 6.34315 20 8V11M8 5H7C5.34315 5 4 6.34315 4 8V18C4 19.6569 5.34315 21 7 21H12"
796
+ };
797
+ var ICON_ADD_PRESET = [
798
+ "M4 6H20",
799
+ "M4 12H10",
800
+ "M15 15L21 15",
801
+ "M18 12V18",
802
+ "M4 18H10"
803
+ ];
804
+ var ICON_TRASH = [
805
+ "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",
806
+ "M10 11V16",
807
+ "M14 11V16",
808
+ "M3.5 6H20.5",
809
+ "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"
810
+ ];
811
+ var ICON_PANEL = {
812
+ path: "M6.84766 11.75C6.78583 11.9899 6.75 12.2408 6.75 12.5C6.75 12.7592 6.78583 13.0101 6.84766 13.25H2C1.58579 13.25 1.25 12.9142 1.25 12.5C1.25 12.0858 1.58579 11.75 2 11.75H6.84766ZM14 11.75C14.4142 11.75 14.75 12.0858 14.75 12.5C14.75 12.9142 14.4142 13.25 14 13.25H12.6523C12.7142 13.0101 12.75 12.7592 12.75 12.5C12.75 12.2408 12.7142 11.9899 12.6523 11.75H14ZM3.09766 7.25C3.03583 7.48994 3 7.74075 3 8C3 8.25925 3.03583 8.51006 3.09766 8.75H2C1.58579 8.75 1.25 8.41421 1.25 8C1.25 7.58579 1.58579 7.25 2 7.25H3.09766ZM14 7.25C14.4142 7.25 14.75 7.58579 14.75 8C14.75 8.41421 14.4142 8.75 14 8.75H8.90234C8.96417 8.51006 9 8.25925 9 8C9 7.74075 8.96417 7.48994 8.90234 7.25H14ZM7.59766 2.75C7.53583 2.98994 7.5 3.24075 7.5 3.5C7.5 3.75925 7.53583 4.01006 7.59766 4.25H2C1.58579 4.25 1.25 3.91421 1.25 3.5C1.25 3.08579 1.58579 2.75 2 2.75H7.59766ZM14 2.75C14.4142 2.75 14.75 3.08579 14.75 3.5C14.75 3.91421 14.4142 4.25 14 4.25H13.4023C13.4642 4.01006 13.5 3.75925 13.5 3.5C13.5 3.24075 13.4642 2.98994 13.4023 2.75H14Z",
813
+ circles: [
814
+ { cx: "6", cy: "8", r: "0.998596" },
815
+ { cx: "10.4999", cy: "3.5", r: "0.998657" },
816
+ { cx: "9.75015", cy: "12.5", r: "0.997986" }
817
+ ]
818
+ };
819
+
820
+ // src/components/Folder.tsx
821
+ import { jsx, jsxs } from "react/jsx-runtime";
822
+ function Folder({ title, children, defaultOpen = true, isRoot = false, inline = false, onOpenChange, toolbar, panelHeightOffset = 10 }) {
823
+ const [isOpen, setIsOpen] = useState(defaultOpen);
824
+ const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
825
+ const contentRef = useRef2(null);
826
+ const [contentHeight, setContentHeight] = useState(void 0);
827
+ const [windowHeight, setWindowHeight] = useState(typeof window !== "undefined" ? window.innerHeight : 800);
828
+ useEffect2(() => {
829
+ if (!isRoot) return;
830
+ const onResize = () => setWindowHeight(window.innerHeight);
831
+ window.addEventListener("resize", onResize);
832
+ return () => window.removeEventListener("resize", onResize);
833
+ }, [isRoot]);
834
+ useEffect2(() => {
835
+ const el = contentRef.current;
836
+ if (!el) return;
837
+ const ro = new ResizeObserver(() => {
838
+ if (isOpen) {
839
+ const h = el.offsetHeight;
840
+ setContentHeight((prev) => prev === h ? prev : h);
841
+ }
842
+ });
843
+ ro.observe(el);
844
+ return () => ro.disconnect();
845
+ }, [isOpen]);
846
+ const handleToggle = () => {
847
+ if (inline && isRoot) return;
848
+ const next = !isOpen;
849
+ setIsOpen(next);
850
+ if (next) {
851
+ setIsCollapsed(false);
852
+ } else {
853
+ setIsCollapsed(true);
854
+ }
855
+ onOpenChange?.(next);
856
+ };
857
+ const folderContent = /* @__PURE__ */ jsxs(
858
+ "div",
859
+ {
860
+ ref: isRoot ? contentRef : void 0,
861
+ className: `dialkit-folder ${isRoot ? "dialkit-folder-root" : ""}`,
862
+ "data-open": String(isOpen),
863
+ children: [
864
+ /* @__PURE__ */ jsxs("div", { className: `dialkit-folder-header ${isRoot ? "dialkit-panel-header" : ""}`, onClick: handleToggle, children: [
865
+ /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-header-top", children: [
866
+ isRoot ? isOpen && /* @__PURE__ */ jsx("div", { className: "dialkit-folder-title-row", children: /* @__PURE__ */ jsx("span", { className: "dialkit-folder-title dialkit-folder-title-root", children: title }) }) : /* @__PURE__ */ jsx("div", { className: "dialkit-folder-title-row", children: /* @__PURE__ */ jsx("span", { className: "dialkit-folder-title", children: title }) }),
867
+ isRoot && !inline && /* @__PURE__ */ jsxs(
868
+ "svg",
869
+ {
870
+ className: "dialkit-panel-icon",
871
+ viewBox: "0 0 16 16",
872
+ fill: "none",
873
+ children: [
874
+ /* @__PURE__ */ jsx("path", { opacity: "0.5", d: ICON_PANEL.path, fill: "currentColor" }),
875
+ ICON_PANEL.circles.map((c, i) => /* @__PURE__ */ jsx("circle", { cx: c.cx, cy: c.cy, r: c.r, fill: "currentColor", stroke: "currentColor", strokeWidth: "1.25" }, i))
876
+ ]
877
+ }
878
+ ),
879
+ !isRoot && /* @__PURE__ */ jsx(
880
+ motion.svg,
881
+ {
882
+ className: "dialkit-folder-icon",
883
+ viewBox: "0 0 24 24",
884
+ fill: "none",
885
+ stroke: "currentColor",
886
+ strokeWidth: "2.5",
887
+ strokeLinecap: "round",
888
+ strokeLinejoin: "round",
889
+ initial: false,
890
+ animate: { rotate: isOpen ? 0 : 180 },
891
+ transition: { type: "spring", visualDuration: 0.35, bounce: 0.15 },
892
+ children: /* @__PURE__ */ jsx("path", { d: ICON_CHEVRON })
893
+ }
894
+ )
895
+ ] }),
896
+ isRoot && toolbar && isOpen && /* @__PURE__ */ jsx("div", { className: "dialkit-panel-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar })
897
+ ] }),
898
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: isOpen && /* @__PURE__ */ jsx(
899
+ motion.div,
900
+ {
901
+ className: "dialkit-folder-content",
902
+ initial: isRoot ? void 0 : { height: 0, opacity: 0 },
903
+ animate: isRoot ? void 0 : { height: "auto", opacity: 1 },
904
+ exit: isRoot ? void 0 : { height: 0, opacity: 0 },
905
+ transition: isRoot ? void 0 : { type: "spring", visualDuration: 0.35, bounce: 0.1 },
906
+ style: isRoot ? void 0 : { clipPath: "inset(0 -20px)" },
907
+ children: /* @__PURE__ */ jsx("div", { className: "dialkit-folder-inner", children })
908
+ }
909
+ ) })
910
+ ]
911
+ }
912
+ );
913
+ if (isRoot) {
914
+ if (inline) {
915
+ return /* @__PURE__ */ jsx("div", { className: "dialkit-panel-inner dialkit-panel-inline", children: folderContent });
916
+ }
917
+ const panelStyle = isOpen ? { width: 280, height: contentHeight !== void 0 ? Math.min(contentHeight + panelHeightOffset, windowHeight - 32) : "auto", borderRadius: 14, boxShadow: "var(--dial-shadow)", cursor: void 0, overflowY: "auto" } : { width: 42, height: 42, borderRadius: "50%", boxSizing: "border-box", boxShadow: "var(--dial-shadow-collapsed)", overflow: "hidden", cursor: "pointer" };
918
+ return /* @__PURE__ */ jsx(
919
+ motion.div,
920
+ {
921
+ className: "dialkit-panel-inner",
922
+ style: panelStyle,
923
+ onClick: !isOpen ? handleToggle : void 0,
924
+ "data-collapsed": isCollapsed,
925
+ whileTap: !isOpen ? { scale: 0.9 } : void 0,
926
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
927
+ children: folderContent
928
+ }
929
+ );
930
+ }
931
+ return folderContent;
932
+ }
933
+
560
934
  // src/components/Panel.tsx
561
- import { useState as useState9, useContext, useSyncExternalStore as useSyncExternalStore4 } from "react";
935
+ import { useCallback as useCallback9, useContext, useState as useState9, useSyncExternalStore as useSyncExternalStore4 } from "react";
562
936
  import { motion as motion5, AnimatePresence as AnimatePresence4 } from "motion/react";
563
937
 
564
938
  // src/components/ShortcutListener.tsx
565
- import { createContext, useEffect as useEffect2, useRef as useRef2, useState, useCallback } from "react";
939
+ import { createContext, useEffect as useEffect3, useRef as useRef3, useState as useState2, useCallback as useCallback2 } from "react";
566
940
 
567
941
  // src/shortcut-utils.ts
568
942
  function decimalsForStep(step) {
@@ -650,15 +1024,15 @@ function formatModifier(modifier) {
650
1024
  }
651
1025
 
652
1026
  // src/components/ShortcutListener.tsx
653
- import { jsx } from "react/jsx-runtime";
1027
+ import { jsx as jsx2 } from "react/jsx-runtime";
654
1028
  var ShortcutContext = createContext({ activePanelId: null, activePath: null });
655
1029
  function ShortcutListener({ children }) {
656
- const [activeShortcut, setActiveShortcut] = useState({ activePanelId: null, activePath: null });
657
- const activeKeysRef = useRef2(/* @__PURE__ */ new Set());
658
- const isDraggingRef = useRef2(false);
659
- const lastMouseXRef = useRef2(null);
660
- const dragAccumulatorRef = useRef2(0);
661
- const resolveActiveTarget = useCallback((interaction) => {
1030
+ const [activeShortcut, setActiveShortcut] = useState2({ activePanelId: null, activePath: null });
1031
+ const activeKeysRef = useRef3(/* @__PURE__ */ new Set());
1032
+ const isDraggingRef = useRef3(false);
1033
+ const lastMouseXRef = useRef3(null);
1034
+ const dragAccumulatorRef = useRef3(0);
1035
+ const resolveActiveTarget = useCallback2((interaction) => {
662
1036
  for (const key of activeKeysRef.current) {
663
1037
  const panels = DialStore.getPanels();
664
1038
  for (const panel of panels) {
@@ -675,7 +1049,7 @@ function ShortcutListener({ children }) {
675
1049
  }
676
1050
  return null;
677
1051
  }, []);
678
- useEffect2(() => {
1052
+ useEffect3(() => {
679
1053
  const handleKeyDown = (e) => {
680
1054
  if (isInputFocused()) return;
681
1055
  const key = e.key.toLowerCase();
@@ -833,150 +1207,11 @@ function ShortcutListener({ children }) {
833
1207
  window.removeEventListener("blur", handleWindowBlur);
834
1208
  };
835
1209
  }, [resolveActiveTarget]);
836
- return /* @__PURE__ */ jsx(ShortcutContext.Provider, { value: activeShortcut, children });
837
- }
838
-
839
- // src/icons.ts
840
- var ICON_CHEVRON = "M6 9.5L12 15.5L18 9.5";
841
- var ICON_CHECK = "M5 12.75L10 19L19 5";
842
- var ICON_CLIPBOARD = {
843
- board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
844
- sparkle: "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",
845
- body: "M16 5H17C18.6569 5 20 6.34315 20 8V11M8 5H7C5.34315 5 4 6.34315 4 8V18C4 19.6569 5.34315 21 7 21H12"
846
- };
847
- var ICON_ADD_PRESET = [
848
- "M4 6H20",
849
- "M4 12H10",
850
- "M15 15L21 15",
851
- "M18 12V18",
852
- "M4 18H10"
853
- ];
854
- var ICON_TRASH = [
855
- "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",
856
- "M10 11V16",
857
- "M14 11V16",
858
- "M3.5 6H20.5",
859
- "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"
860
- ];
861
- var ICON_PANEL = {
862
- path: "M6.84766 11.75C6.78583 11.9899 6.75 12.2408 6.75 12.5C6.75 12.7592 6.78583 13.0101 6.84766 13.25H2C1.58579 13.25 1.25 12.9142 1.25 12.5C1.25 12.0858 1.58579 11.75 2 11.75H6.84766ZM14 11.75C14.4142 11.75 14.75 12.0858 14.75 12.5C14.75 12.9142 14.4142 13.25 14 13.25H12.6523C12.7142 13.0101 12.75 12.7592 12.75 12.5C12.75 12.2408 12.7142 11.9899 12.6523 11.75H14ZM3.09766 7.25C3.03583 7.48994 3 7.74075 3 8C3 8.25925 3.03583 8.51006 3.09766 8.75H2C1.58579 8.75 1.25 8.41421 1.25 8C1.25 7.58579 1.58579 7.25 2 7.25H3.09766ZM14 7.25C14.4142 7.25 14.75 7.58579 14.75 8C14.75 8.41421 14.4142 8.75 14 8.75H8.90234C8.96417 8.51006 9 8.25925 9 8C9 7.74075 8.96417 7.48994 8.90234 7.25H14ZM7.59766 2.75C7.53583 2.98994 7.5 3.24075 7.5 3.5C7.5 3.75925 7.53583 4.01006 7.59766 4.25H2C1.58579 4.25 1.25 3.91421 1.25 3.5C1.25 3.08579 1.58579 2.75 2 2.75H7.59766ZM14 2.75C14.4142 2.75 14.75 3.08579 14.75 3.5C14.75 3.91421 14.4142 4.25 14 4.25H13.4023C13.4642 4.01006 13.5 3.75925 13.5 3.5C13.5 3.24075 13.4642 2.98994 13.4023 2.75H14Z",
863
- circles: [
864
- { cx: "6", cy: "8", r: "0.998596" },
865
- { cx: "10.4999", cy: "3.5", r: "0.998657" },
866
- { cx: "9.75015", cy: "12.5", r: "0.997986" }
867
- ]
868
- };
869
-
870
- // src/components/Folder.tsx
871
- import { useState as useState2, useRef as useRef3, useEffect as useEffect3 } from "react";
872
- import { motion, AnimatePresence } from "motion/react";
873
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
874
- function Folder({ title, children, defaultOpen = true, isRoot = false, inline = false, onOpenChange, toolbar }) {
875
- const [isOpen, setIsOpen] = useState2(defaultOpen);
876
- const [isCollapsed, setIsCollapsed] = useState2(!defaultOpen);
877
- const contentRef = useRef3(null);
878
- const [contentHeight, setContentHeight] = useState2(void 0);
879
- const [windowHeight, setWindowHeight] = useState2(typeof window !== "undefined" ? window.innerHeight : 800);
880
- useEffect3(() => {
881
- if (!isRoot) return;
882
- const onResize = () => setWindowHeight(window.innerHeight);
883
- window.addEventListener("resize", onResize);
884
- return () => window.removeEventListener("resize", onResize);
885
- }, [isRoot]);
886
- useEffect3(() => {
887
- const el = contentRef.current;
888
- if (!el) return;
889
- const ro = new ResizeObserver(() => {
890
- if (isOpen) {
891
- const h = el.offsetHeight;
892
- setContentHeight((prev) => prev === h ? prev : h);
893
- }
894
- });
895
- ro.observe(el);
896
- return () => ro.disconnect();
897
- }, [isOpen]);
898
- const handleToggle = () => {
899
- if (inline && isRoot) return;
900
- const next = !isOpen;
901
- setIsOpen(next);
902
- if (next) {
903
- setIsCollapsed(false);
904
- } else {
905
- setIsCollapsed(true);
906
- }
907
- onOpenChange?.(next);
908
- };
909
- const folderContent = /* @__PURE__ */ jsxs("div", { ref: isRoot ? contentRef : void 0, className: `dialkit-folder ${isRoot ? "dialkit-folder-root" : ""}`, children: [
910
- /* @__PURE__ */ jsxs("div", { className: `dialkit-folder-header ${isRoot ? "dialkit-panel-header" : ""}`, onClick: handleToggle, children: [
911
- /* @__PURE__ */ jsxs("div", { className: "dialkit-folder-header-top", children: [
912
- isRoot ? isOpen && /* @__PURE__ */ jsx2("div", { className: "dialkit-folder-title-row", children: /* @__PURE__ */ jsx2("span", { className: "dialkit-folder-title dialkit-folder-title-root", children: title }) }) : /* @__PURE__ */ jsx2("div", { className: "dialkit-folder-title-row", children: /* @__PURE__ */ jsx2("span", { className: "dialkit-folder-title", children: title }) }),
913
- isRoot && !inline && /* @__PURE__ */ jsxs(
914
- "svg",
915
- {
916
- className: "dialkit-panel-icon",
917
- viewBox: "0 0 16 16",
918
- fill: "none",
919
- children: [
920
- /* @__PURE__ */ jsx2("path", { opacity: "0.5", d: ICON_PANEL.path, fill: "currentColor" }),
921
- ICON_PANEL.circles.map((c, i) => /* @__PURE__ */ jsx2("circle", { cx: c.cx, cy: c.cy, r: c.r, fill: "currentColor", stroke: "currentColor", strokeWidth: "1.25" }, i))
922
- ]
923
- }
924
- ),
925
- !isRoot && /* @__PURE__ */ jsx2(
926
- motion.svg,
927
- {
928
- className: "dialkit-folder-icon",
929
- viewBox: "0 0 24 24",
930
- fill: "none",
931
- stroke: "currentColor",
932
- strokeWidth: "2.5",
933
- strokeLinecap: "round",
934
- strokeLinejoin: "round",
935
- initial: false,
936
- animate: { rotate: isOpen ? 0 : 180 },
937
- transition: { type: "spring", visualDuration: 0.35, bounce: 0.15 },
938
- children: /* @__PURE__ */ jsx2("path", { d: ICON_CHEVRON })
939
- }
940
- )
941
- ] }),
942
- isRoot && toolbar && isOpen && /* @__PURE__ */ jsx2("div", { className: "dialkit-panel-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar })
943
- ] }),
944
- /* @__PURE__ */ jsx2(AnimatePresence, { initial: false, children: isOpen && /* @__PURE__ */ jsx2(
945
- motion.div,
946
- {
947
- className: "dialkit-folder-content",
948
- initial: isRoot ? void 0 : { height: 0, opacity: 0 },
949
- animate: isRoot ? void 0 : { height: "auto", opacity: 1 },
950
- exit: isRoot ? void 0 : { height: 0, opacity: 0 },
951
- transition: isRoot ? void 0 : { type: "spring", visualDuration: 0.35, bounce: 0.1 },
952
- style: isRoot ? void 0 : { clipPath: "inset(0 -20px)" },
953
- children: /* @__PURE__ */ jsx2("div", { className: "dialkit-folder-inner", children })
954
- }
955
- ) })
956
- ] });
957
- if (isRoot) {
958
- if (inline) {
959
- return /* @__PURE__ */ jsx2("div", { className: "dialkit-panel-inner dialkit-panel-inline", children: folderContent });
960
- }
961
- const panelStyle = isOpen ? { width: 280, height: contentHeight !== void 0 ? Math.min(contentHeight + 10, windowHeight - 32) : "auto", borderRadius: 14, boxShadow: "var(--dial-shadow)", cursor: void 0, overflowY: "auto" } : { width: 42, height: 42, borderRadius: "50%", boxSizing: "border-box", boxShadow: "var(--dial-shadow-collapsed)", overflow: "hidden", cursor: "pointer" };
962
- return /* @__PURE__ */ jsx2(
963
- motion.div,
964
- {
965
- className: "dialkit-panel-inner",
966
- style: panelStyle,
967
- onClick: !isOpen ? handleToggle : void 0,
968
- "data-collapsed": isCollapsed,
969
- whileTap: !isOpen ? { scale: 0.9 } : void 0,
970
- transition: { type: "spring", visualDuration: 0.15, bounce: 0.3 },
971
- children: folderContent
972
- }
973
- );
974
- }
975
- return folderContent;
1210
+ return /* @__PURE__ */ jsx2(ShortcutContext.Provider, { value: activeShortcut, children });
976
1211
  }
977
1212
 
978
1213
  // src/components/Slider.tsx
979
- import { useRef as useRef4, useState as useState3, useCallback as useCallback2, useEffect as useEffect4 } from "react";
1214
+ import { useRef as useRef4, useState as useState3, useCallback as useCallback3, useEffect as useEffect4 } from "react";
980
1215
  import { motion as motion2, useMotionValue, useTransform, animate } from "motion/react";
981
1216
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
982
1217
  var CLICK_THRESHOLD = 3;
@@ -1034,7 +1269,7 @@ function Slider({
1034
1269
  fillPercent.jump(percentage);
1035
1270
  }
1036
1271
  }, [percentage, isInteracting, fillPercent]);
1037
- const positionToValue = useCallback2(
1272
+ const positionToValue = useCallback3(
1038
1273
  (clientX) => {
1039
1274
  const rect = wrapperRectRef.current;
1040
1275
  if (!rect) return value;
@@ -1047,11 +1282,11 @@ function Slider({
1047
1282
  },
1048
1283
  [min, max, value]
1049
1284
  );
1050
- const percentFromValue = useCallback2(
1285
+ const percentFromValue = useCallback3(
1051
1286
  (v) => (v - min) / (max - min) * 100,
1052
1287
  [min, max]
1053
1288
  );
1054
- const computeRubberStretch = useCallback2(
1289
+ const computeRubberStretch = useCallback3(
1055
1290
  (clientX, sign) => {
1056
1291
  const rect = wrapperRectRef.current;
1057
1292
  if (!rect) return 0;
@@ -1061,7 +1296,7 @@ function Slider({
1061
1296
  },
1062
1297
  []
1063
1298
  );
1064
- const handlePointerDown = useCallback2(
1299
+ const handlePointerDown = useCallback3(
1065
1300
  (e) => {
1066
1301
  if (showInput) return;
1067
1302
  e.preventDefault();
@@ -1077,7 +1312,7 @@ function Slider({
1077
1312
  },
1078
1313
  [showInput]
1079
1314
  );
1080
- const handlePointerMove = useCallback2(
1315
+ const handlePointerMove = useCallback3(
1081
1316
  (e) => {
1082
1317
  if (!isInteracting || !pointerDownPos.current) return;
1083
1318
  const dx = e.clientX - pointerDownPos.current.x;
@@ -1118,7 +1353,7 @@ function Slider({
1118
1353
  computeRubberStretch
1119
1354
  ]
1120
1355
  );
1121
- const handlePointerUp = useCallback2(
1356
+ const handlePointerUp = useCallback3(
1122
1357
  (e) => {
1123
1358
  if (!isInteracting) return;
1124
1359
  if (isClickRef.current) {
@@ -1335,7 +1570,7 @@ function Slider({
1335
1570
  }
1336
1571
 
1337
1572
  // src/components/SegmentedControl.tsx
1338
- import { useRef as useRef5, useState as useState4, useLayoutEffect, useCallback as useCallback3 } from "react";
1573
+ import { useRef as useRef5, useState as useState4, useLayoutEffect, useCallback as useCallback4 } from "react";
1339
1574
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1340
1575
  function SegmentedControl({
1341
1576
  options,
@@ -1345,7 +1580,7 @@ function SegmentedControl({
1345
1580
  const containerRef = useRef5(null);
1346
1581
  const hasAnimated = useRef5(false);
1347
1582
  const [pillStyle, setPillStyle] = useState4(null);
1348
- const measure = useCallback3(() => {
1583
+ const measure = useCallback4(() => {
1349
1584
  const container = containerRef.current;
1350
1585
  if (!container) return;
1351
1586
  const activeButton = container.querySelector('[data-active="true"]');
@@ -1499,14 +1734,18 @@ function SpringVisualization({ spring, isSimpleMode }) {
1499
1734
  }
1500
1735
 
1501
1736
  // src/components/SpringControl.tsx
1502
- import { useSyncExternalStore as useSyncExternalStore2, useRef as useRef6 } from "react";
1737
+ import { useCallback as useCallback5, useRef as useRef6, useSyncExternalStore as useSyncExternalStore2 } from "react";
1503
1738
  import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1504
1739
  function SpringControl({ panelId, path, label, spring, onChange }) {
1505
- const mode = useSyncExternalStore2(
1506
- (cb) => DialStore.subscribe(panelId, cb),
1740
+ const subscribe = useCallback5(
1741
+ (callback) => DialStore.subscribe(panelId, callback),
1742
+ [panelId]
1743
+ );
1744
+ const getSnapshot = useCallback5(
1507
1745
  () => DialStore.getSpringMode(panelId, path),
1508
- () => DialStore.getSpringMode(panelId, path)
1746
+ [panelId, path]
1509
1747
  );
1748
+ const mode = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
1510
1749
  const isSimpleMode = mode === "simple";
1511
1750
  const cache = useRef6({
1512
1751
  simple: spring.visualDuration !== void 0 ? spring : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
@@ -1655,14 +1894,18 @@ function EasingVisualization({ easing }) {
1655
1894
  }
1656
1895
 
1657
1896
  // src/components/TransitionControl.tsx
1658
- import { useSyncExternalStore as useSyncExternalStore3, useRef as useRef7, useState as useState5 } from "react";
1897
+ import { useCallback as useCallback6, useRef as useRef7, useState as useState5, useSyncExternalStore as useSyncExternalStore3 } from "react";
1659
1898
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1660
1899
  function TransitionControl({ panelId, path, label, value, onChange }) {
1661
- const mode = useSyncExternalStore3(
1662
- (cb) => DialStore.subscribe(panelId, cb),
1900
+ const subscribe = useCallback6(
1901
+ (callback) => DialStore.subscribe(panelId, callback),
1902
+ [panelId]
1903
+ );
1904
+ const getSnapshot = useCallback6(
1663
1905
  () => DialStore.getTransitionMode(panelId, path),
1664
- () => DialStore.getTransitionMode(panelId, path)
1906
+ [panelId, path]
1665
1907
  );
1908
+ const mode = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
1666
1909
  const isEasing = mode === "easing";
1667
1910
  const isSimpleSpring = mode === "simple";
1668
1911
  const cache = useRef7({
@@ -1801,9 +2044,29 @@ function TextControl({ label, value, onChange, placeholder }) {
1801
2044
  }
1802
2045
 
1803
2046
  // src/components/SelectControl.tsx
1804
- import { useState as useState6, useRef as useRef8, useEffect as useEffect5, useCallback as useCallback4 } from "react";
2047
+ import { useState as useState6, useRef as useRef8, useEffect as useEffect5, useCallback as useCallback7 } from "react";
1805
2048
  import { createPortal } from "react-dom";
1806
2049
  import { motion as motion3, AnimatePresence as AnimatePresence2 } from "motion/react";
2050
+
2051
+ // src/dropdown-position.ts
2052
+ function getDropdownPosition(trigger, portalRoot, options = {}) {
2053
+ const { dropdownHeight = 0, gap = 4, allowAbove = true } = options;
2054
+ const triggerRect = trigger.getBoundingClientRect();
2055
+ const rootRect = portalRoot.getBoundingClientRect();
2056
+ const spaceBelow = window.innerHeight - triggerRect.bottom - gap;
2057
+ const above = allowAbove && spaceBelow < dropdownHeight && triggerRect.top > spaceBelow;
2058
+ return {
2059
+ top: above ? triggerRect.top - rootRect.top - dropdownHeight - gap : triggerRect.bottom - rootRect.top + gap,
2060
+ left: triggerRect.left - rootRect.left,
2061
+ width: triggerRect.width,
2062
+ above
2063
+ };
2064
+ }
2065
+ function getDialKitPortalRoot(trigger) {
2066
+ return trigger?.closest(".dialkit-root") ?? null;
2067
+ }
2068
+
2069
+ // src/components/SelectControl.tsx
1807
2070
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1808
2071
  function toTitleCase(s) {
1809
2072
  return s.replace(/\b\w/g, (c) => c.toUpperCase());
@@ -1821,23 +2084,14 @@ function SelectControl({ label, value, options, onChange }) {
1821
2084
  const [pos, setPos] = useState6(null);
1822
2085
  const normalized = normalizeOptions(options);
1823
2086
  const selectedOption = normalized.find((o) => o.value === value);
1824
- const updatePos = useCallback4(() => {
2087
+ const updatePos = useCallback7(() => {
1825
2088
  const el = triggerRef.current;
1826
- if (!el) return;
1827
- const rect = el.getBoundingClientRect();
2089
+ if (!el || !portalTarget) return;
1828
2090
  const dropdownHeight = 8 + normalized.length * 36;
1829
- const spaceBelow = window.innerHeight - rect.bottom - 4;
1830
- const above = spaceBelow < dropdownHeight && rect.top > spaceBelow;
1831
- setPos({
1832
- top: above ? rect.top - 4 : rect.bottom + 4,
1833
- left: rect.left,
1834
- width: rect.width,
1835
- above
1836
- });
1837
- }, [normalized.length]);
2091
+ setPos(getDropdownPosition(el, portalTarget, { dropdownHeight }));
2092
+ }, [normalized.length, portalTarget]);
1838
2093
  useEffect5(() => {
1839
- const root = triggerRef.current?.closest(".dialkit-root");
1840
- setPortalTarget(root ?? document.body);
2094
+ setPortalTarget(getDialKitPortalRoot(triggerRef.current) ?? document.body);
1841
2095
  }, []);
1842
2096
  useEffect5(() => {
1843
2097
  if (!isOpen) return;
@@ -1896,10 +2150,11 @@ function SelectControl({ label, value, options, onChange }) {
1896
2150
  exit: { opacity: 0, y: pos.above ? 8 : -8, scale: 0.95 },
1897
2151
  transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1898
2152
  style: {
1899
- position: "fixed",
2153
+ position: "absolute",
1900
2154
  left: pos.left,
2155
+ top: pos.top,
1901
2156
  width: pos.width,
1902
- ...pos.above ? { bottom: window.innerHeight - pos.top, transformOrigin: "bottom" } : { top: pos.top, transformOrigin: "top" }
2157
+ transformOrigin: pos.above ? "bottom" : "top"
1903
2158
  },
1904
2159
  children: normalized.map((option) => /* @__PURE__ */ jsx11(
1905
2160
  "button",
@@ -2000,7 +2255,7 @@ function expandShorthandHex(hex) {
2000
2255
  }
2001
2256
 
2002
2257
  // src/components/PresetManager.tsx
2003
- import { useState as useState8, useRef as useRef10, useEffect as useEffect7, useCallback as useCallback5 } from "react";
2258
+ import { useState as useState8, useRef as useRef10, useEffect as useEffect7, useCallback as useCallback8 } from "react";
2004
2259
  import { createPortal as createPortal2 } from "react-dom";
2005
2260
  import { motion as motion4, AnimatePresence as AnimatePresence3 } from "motion/react";
2006
2261
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
@@ -2011,7 +2266,7 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2011
2266
  const [pos, setPos] = useState8({ top: 0, left: 0, width: 0 });
2012
2267
  const hasPresets = presets.length > 0;
2013
2268
  const activePreset = presets.find((p) => p.id === activePresetId);
2014
- const open = useCallback5(() => {
2269
+ const open = useCallback8(() => {
2015
2270
  if (!hasPresets) return;
2016
2271
  const rect = triggerRef.current?.getBoundingClientRect();
2017
2272
  if (rect) {
@@ -2019,8 +2274,8 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2019
2274
  }
2020
2275
  setIsOpen(true);
2021
2276
  }, [hasPresets]);
2022
- const close = useCallback5(() => setIsOpen(false), []);
2023
- const toggle = useCallback5(() => {
2277
+ const close = useCallback8(() => setIsOpen(false), []);
2278
+ const toggle = useCallback8(() => {
2024
2279
  if (isOpen) close();
2025
2280
  else open();
2026
2281
  }, [isOpen, open, close]);
@@ -2128,16 +2383,20 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
2128
2383
 
2129
2384
  // src/components/Panel.tsx
2130
2385
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2131
- function Panel({ panel, defaultOpen = true, inline = false }) {
2386
+ function Panel({ panel, defaultOpen = true, inline = false, onOpenChange, variant = "root" }) {
2132
2387
  const [copied, setCopied] = useState9(false);
2133
2388
  const [isPanelOpen, setIsPanelOpen] = useState9(defaultOpen);
2134
2389
  const shortcutCtx = useContext(ShortcutContext);
2135
2390
  const hasShortcuts = Object.keys(panel.shortcuts).length > 0;
2136
- const values = useSyncExternalStore4(
2137
- (cb) => DialStore.subscribe(panel.id, cb),
2391
+ const subscribe = useCallback9(
2392
+ (callback) => DialStore.subscribe(panel.id, callback),
2393
+ [panel.id]
2394
+ );
2395
+ const getSnapshot = useCallback9(
2138
2396
  () => DialStore.getValues(panel.id),
2139
- () => DialStore.getValues(panel.id)
2397
+ [panel.id]
2140
2398
  );
2399
+ const values = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
2141
2400
  const presets = DialStore.getPresets(panel.id);
2142
2401
  const activePresetId = DialStore.getActivePresetId(panel.id);
2143
2402
  const handleAddPreset = () => {
@@ -2157,6 +2416,10 @@ Apply these values as the new defaults in the useDialKit call.`;
2157
2416
  setCopied(true);
2158
2417
  setTimeout(() => setCopied(false), 1500);
2159
2418
  };
2419
+ const handleOpenChange = useCallback9((open) => {
2420
+ setIsPanelOpen(open);
2421
+ onOpenChange?.(open);
2422
+ }, [onOpenChange]);
2160
2423
  const renderControl = (control) => {
2161
2424
  const value = values[control.path];
2162
2425
  switch (control.type) {
@@ -2330,13 +2593,83 @@ Apply these values as the new defaults in the useDialKit call.`;
2330
2593
  }
2331
2594
  )
2332
2595
  ] });
2333
- return /* @__PURE__ */ jsx14("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx14(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: setIsPanelOpen, toolbar, children: renderControls() }) });
2596
+ if (variant === "section") {
2597
+ return /* @__PURE__ */ jsxs13(Folder, { title: panel.name, defaultOpen, onOpenChange: handleOpenChange, children: [
2598
+ /* @__PURE__ */ jsx14("div", { className: "dialkit-panel-section-toolbar", onClick: (e) => e.stopPropagation(), children: toolbar }),
2599
+ renderControls()
2600
+ ] });
2601
+ }
2602
+ return /* @__PURE__ */ jsx14("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx14(Folder, { title: panel.name, defaultOpen, isRoot: true, inline, onOpenChange: handleOpenChange, toolbar, children: renderControls() }) });
2603
+ }
2604
+
2605
+ // src/panel-drag.ts
2606
+ var PANEL_DRAG_THRESHOLD = 8;
2607
+ var COLLAPSED_PANEL_SIZE = 42;
2608
+ var DRAG_EXCLUSION_SELECTOR = [
2609
+ ".dialkit-panel-icon",
2610
+ ".dialkit-panel-toolbar",
2611
+ "button",
2612
+ "input",
2613
+ "select",
2614
+ "textarea",
2615
+ "a",
2616
+ '[role="button"]',
2617
+ '[contenteditable="true"]'
2618
+ ].join(",");
2619
+ function getPanelDragHandle(target, panel) {
2620
+ if (!(target instanceof Element) || !panel) return null;
2621
+ const inner = target.closest(".dialkit-panel-inner");
2622
+ if (!inner || !panel.contains(inner)) return null;
2623
+ if (inner.getAttribute("data-collapsed") === "true") {
2624
+ return inner;
2625
+ }
2626
+ const header = target.closest(".dialkit-panel-header");
2627
+ if (!header || !inner.contains(header)) return null;
2628
+ if (target.closest(DRAG_EXCLUSION_SELECTOR)) return null;
2629
+ return header;
2630
+ }
2631
+ function getPanelDragStart(pointerX, pointerY, panel) {
2632
+ const rect = panel.getBoundingClientRect();
2633
+ return {
2634
+ pointerX,
2635
+ pointerY,
2636
+ elX: rect.left,
2637
+ elY: rect.top
2638
+ };
2639
+ }
2640
+ function getPanelDragOffset(start, pointerX, pointerY) {
2641
+ return {
2642
+ x: start.elX + pointerX - start.pointerX,
2643
+ y: start.elY + pointerY - start.pointerY
2644
+ };
2645
+ }
2646
+ function hasPanelDragMoved(start, pointerX, pointerY) {
2647
+ const dx = pointerX - start.pointerX;
2648
+ const dy = pointerY - start.pointerY;
2649
+ return Math.hypot(dx, dy) >= PANEL_DRAG_THRESHOLD;
2650
+ }
2651
+ function getPanelOriginX(position, offset, viewportWidth = typeof window !== "undefined" ? window.innerWidth : void 0) {
2652
+ if (offset && viewportWidth) {
2653
+ return offset.x + COLLAPSED_PANEL_SIZE / 2 < viewportWidth / 2 ? "left" : "right";
2654
+ }
2655
+ return position.endsWith("left") ? "left" : "right";
2656
+ }
2657
+ function blockPanelDragClick(handle) {
2658
+ const blocker = (event) => {
2659
+ event.preventDefault();
2660
+ event.stopImmediatePropagation();
2661
+ event.stopPropagation();
2662
+ };
2663
+ handle.addEventListener("click", blocker, { capture: true, once: true });
2664
+ window.setTimeout(() => {
2665
+ handle.removeEventListener("click", blocker, true);
2666
+ }, 0);
2334
2667
  }
2335
2668
 
2336
2669
  // src/components/DialRoot.tsx
2337
2670
  import { jsx as jsx15 } from "react/jsx-runtime";
2338
2671
  var isDevDefault = typeof process !== "undefined" && process?.env?.NODE_ENV ? process.env.NODE_ENV !== "production" : typeof import.meta !== "undefined" && import.meta.env?.MODE ? import.meta.env.MODE !== "production" : true;
2339
- function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover", theme = "system", productionEnabled = isDevDefault }) {
2672
+ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover", theme = "system", productionEnabled = isDevDefault, onOpenChange }) {
2340
2673
  if (!productionEnabled) return null;
2341
2674
  const [panels, setPanels] = useState10([]);
2342
2675
  const [mounted, setMounted] = useState10(false);
@@ -2348,6 +2681,9 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2348
2681
  const draggingRef = useRef11(false);
2349
2682
  const dragStartRef = useRef11(null);
2350
2683
  const didDragRef = useRef11(false);
2684
+ const dragTargetRef = useRef11(null);
2685
+ const panelOpenStatesRef = useRef11(/* @__PURE__ */ new Map());
2686
+ const rootOpenRef = useRef11(null);
2351
2687
  useEffect8(() => {
2352
2688
  setMounted(true);
2353
2689
  setPanels(DialStore.getPanels());
@@ -2356,22 +2692,36 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2356
2692
  });
2357
2693
  return unsubscribe;
2358
2694
  }, []);
2695
+ useEffect8(() => {
2696
+ const fallbackOpen = inline || defaultOpen;
2697
+ const nextStates = /* @__PURE__ */ new Map();
2698
+ for (const panel of panels) {
2699
+ nextStates.set(panel.id, panelOpenStatesRef.current.get(panel.id) ?? fallbackOpen);
2700
+ }
2701
+ panelOpenStatesRef.current = nextStates;
2702
+ rootOpenRef.current = Array.from(nextStates.values()).some(Boolean);
2703
+ }, [defaultOpen, inline, panels]);
2359
2704
  useEffect8(() => {
2360
2705
  if (!panelRef.current || inline) return;
2361
2706
  const observer = new MutationObserver(() => {
2362
- const inner = panelRef.current?.querySelector(".dialkit-panel-inner");
2363
- if (!inner) return;
2364
- const collapsed = inner.getAttribute("data-collapsed") === "true";
2707
+ const inners = panelRef.current?.querySelectorAll(".dialkit-panel-inner");
2708
+ if (!inners || inners.length === 0) return;
2709
+ const collapsed = Array.from(inners).every(
2710
+ (el) => el.getAttribute("data-collapsed") === "true"
2711
+ );
2712
+ const currentDragOffset = dragOffset;
2365
2713
  if (!collapsed) {
2366
- if (dragOffset) {
2367
- lastDragOffset.current = dragOffset;
2368
- const bubbleCenterX = dragOffset.x + 21;
2714
+ if (currentDragOffset) {
2715
+ lastDragOffset.current = currentDragOffset;
2716
+ const bubbleCenterX = currentDragOffset.x + 21;
2369
2717
  const midX = window.innerWidth / 2;
2370
2718
  setActivePosition(bubbleCenterX < midX ? "top-left" : "top-right");
2371
2719
  } else {
2372
2720
  setActivePosition(position);
2373
2721
  }
2374
2722
  setDragOffset(null);
2723
+ } else if (currentDragOffset) {
2724
+ lastDragOffset.current = currentDragOffset;
2375
2725
  } else if (lastDragOffset.current) {
2376
2726
  setDragOffset(lastDragOffset.current);
2377
2727
  }
@@ -2379,46 +2729,51 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2379
2729
  observer.observe(panelRef.current, { subtree: true, attributes: true, attributeFilter: ["data-collapsed"] });
2380
2730
  return () => observer.disconnect();
2381
2731
  }, [inline, dragOffset, position]);
2382
- const handlePointerDown = useCallback6((e) => {
2383
- const inner = panelRef.current?.querySelector(".dialkit-panel-inner");
2384
- if (!inner || inner.getAttribute("data-collapsed") !== "true") return;
2385
- const rect = panelRef.current.getBoundingClientRect();
2386
- dragStartRef.current = {
2387
- pointerX: e.clientX,
2388
- pointerY: e.clientY,
2389
- elX: rect.left,
2390
- elY: rect.top
2391
- };
2732
+ const handlePointerDown = useCallback10((e) => {
2733
+ const panel = panelRef.current;
2734
+ const handle = getPanelDragHandle(e.target, panel);
2735
+ if (!panel || !handle) return;
2736
+ dragTargetRef.current = handle;
2737
+ dragStartRef.current = getPanelDragStart(e.clientX, e.clientY, panel);
2392
2738
  didDragRef.current = false;
2393
2739
  draggingRef.current = true;
2394
- e.target.setPointerCapture(e.pointerId);
2740
+ handle.setPointerCapture(e.pointerId);
2395
2741
  }, []);
2396
- const handlePointerMove = useCallback6((e) => {
2742
+ const handlePointerMove = useCallback10((e) => {
2397
2743
  if (!draggingRef.current || !dragStartRef.current) return;
2398
- const dx = e.clientX - dragStartRef.current.pointerX;
2399
- const dy = e.clientY - dragStartRef.current.pointerY;
2400
- if (!didDragRef.current && Math.abs(dx) + Math.abs(dy) < 4) return;
2744
+ if (!didDragRef.current && !hasPanelDragMoved(dragStartRef.current, e.clientX, e.clientY)) return;
2401
2745
  didDragRef.current = true;
2402
- setDragOffset({
2403
- x: dragStartRef.current.elX + dx,
2404
- y: dragStartRef.current.elY + dy
2405
- });
2746
+ setDragOffset(getPanelDragOffset(dragStartRef.current, e.clientX, e.clientY));
2406
2747
  }, []);
2407
- const handlePointerUp = useCallback6((e) => {
2748
+ const handlePointerUp = useCallback10((e) => {
2408
2749
  if (!draggingRef.current) return;
2409
2750
  draggingRef.current = false;
2410
2751
  dragStartRef.current = null;
2752
+ const dragTarget = dragTargetRef.current;
2753
+ if (dragTarget?.hasPointerCapture(e.pointerId)) {
2754
+ dragTarget.releasePointerCapture(e.pointerId);
2755
+ }
2411
2756
  if (didDragRef.current) {
2412
2757
  e.stopPropagation();
2413
- const inner = panelRef.current?.querySelector(".dialkit-panel-inner");
2414
- if (inner) {
2415
- const blocker = (ev) => {
2416
- ev.stopPropagation();
2417
- };
2418
- inner.addEventListener("click", blocker, { capture: true, once: true });
2758
+ if (dragTarget) {
2759
+ blockPanelDragClick(dragTarget);
2419
2760
  }
2420
2761
  }
2762
+ dragTargetRef.current = null;
2421
2763
  }, []);
2764
+ const handlePanelOpenChange = useCallback10((panelId, open) => {
2765
+ panelOpenStatesRef.current.set(panelId, open);
2766
+ const fallbackOpen = inline || defaultOpen;
2767
+ const nextRootOpen = panels.some((panel) => panelOpenStatesRef.current.get(panel.id) ?? fallbackOpen);
2768
+ if (rootOpenRef.current === nextRootOpen) return;
2769
+ rootOpenRef.current = nextRootOpen;
2770
+ onOpenChange?.(nextRootOpen);
2771
+ }, [defaultOpen, inline, onOpenChange, panels]);
2772
+ const handleRootOpenChange = useCallback10((open) => {
2773
+ if (rootOpenRef.current === open) return;
2774
+ rootOpenRef.current = open;
2775
+ onOpenChange?.(open);
2776
+ }, [onOpenChange]);
2422
2777
  if (!mounted || typeof window === "undefined") {
2423
2778
  return null;
2424
2779
  }
@@ -2431,18 +2786,51 @@ function DialRoot({ position = "top-right", defaultOpen = true, mode = "popover"
2431
2786
  right: "auto",
2432
2787
  bottom: "auto"
2433
2788
  } : void 0;
2789
+ const originX = getPanelOriginX(activePosition, dragOffset);
2790
+ const hasMultiplePanels = panels.length > 1;
2434
2791
  const content = /* @__PURE__ */ jsx15(ShortcutListener, { children: /* @__PURE__ */ jsx15("div", { className: "dialkit-root", "data-mode": mode, "data-theme": theme, children: /* @__PURE__ */ jsx15(
2435
2792
  "div",
2436
2793
  {
2437
2794
  ref: panelRef,
2438
2795
  className: "dialkit-panel",
2439
2796
  "data-position": inline ? void 0 : dragOffset ? void 0 : activePosition,
2797
+ "data-origin-x": inline ? void 0 : originX,
2440
2798
  "data-mode": mode,
2799
+ "data-multiple": hasMultiplePanels ? "true" : void 0,
2441
2800
  style: dragStyle,
2442
2801
  onPointerDown: !inline ? handlePointerDown : void 0,
2443
2802
  onPointerMove: !inline ? handlePointerMove : void 0,
2444
2803
  onPointerUp: !inline ? handlePointerUp : void 0,
2445
- children: panels.map((panel) => /* @__PURE__ */ jsx15(Panel, { panel, defaultOpen: inline || defaultOpen, inline }, panel.id))
2804
+ onPointerCancel: !inline ? handlePointerUp : void 0,
2805
+ children: hasMultiplePanels ? /* @__PURE__ */ jsx15("div", { className: "dialkit-panel-wrapper", children: /* @__PURE__ */ jsx15(
2806
+ Folder,
2807
+ {
2808
+ title: "DialKit",
2809
+ defaultOpen: inline || defaultOpen,
2810
+ isRoot: true,
2811
+ inline,
2812
+ onOpenChange: handleRootOpenChange,
2813
+ panelHeightOffset: 2,
2814
+ children: panels.map((panel) => /* @__PURE__ */ jsx15(
2815
+ Panel,
2816
+ {
2817
+ panel,
2818
+ defaultOpen: true,
2819
+ variant: "section"
2820
+ },
2821
+ panel.id
2822
+ ))
2823
+ }
2824
+ ) }) : panels.map((panel) => /* @__PURE__ */ jsx15(
2825
+ Panel,
2826
+ {
2827
+ panel,
2828
+ defaultOpen: inline || defaultOpen,
2829
+ inline,
2830
+ onOpenChange: (open) => handlePanelOpenChange(panel.id, open)
2831
+ },
2832
+ panel.id
2833
+ ))
2446
2834
  }
2447
2835
  ) }) });
2448
2836
  if (inline) {
@@ -2466,7 +2854,7 @@ function ButtonGroup({ buttons }) {
2466
2854
  }
2467
2855
 
2468
2856
  // src/components/ShortcutsMenu.tsx
2469
- import { useState as useState11, useRef as useRef12, useEffect as useEffect9, useCallback as useCallback7 } from "react";
2857
+ import { useState as useState11, useRef as useRef12, useEffect as useEffect9, useCallback as useCallback11 } from "react";
2470
2858
  import { createPortal as createPortal4 } from "react-dom";
2471
2859
  import { motion as motion6, AnimatePresence as AnimatePresence5 } from "motion/react";
2472
2860
  import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
@@ -2493,15 +2881,15 @@ function ShortcutsMenu({ panelId }) {
2493
2881
  const triggerRef = useRef12(null);
2494
2882
  const dropdownRef = useRef12(null);
2495
2883
  const [pos, setPos] = useState11({ top: 0, right: 0 });
2496
- const open = useCallback7(() => {
2884
+ const open = useCallback11(() => {
2497
2885
  const rect = triggerRef.current?.getBoundingClientRect();
2498
2886
  if (rect) {
2499
2887
  setPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
2500
2888
  }
2501
2889
  setIsOpen(true);
2502
2890
  }, []);
2503
- const close = useCallback7(() => setIsOpen(false), []);
2504
- const toggle = useCallback7(() => {
2891
+ const close = useCallback11(() => setIsOpen(false), []);
2892
+ const toggle = useCallback11(() => {
2505
2893
  if (isOpen) close();
2506
2894
  else open();
2507
2895
  }, [isOpen, open, close]);
@@ -2598,6 +2986,7 @@ export {
2598
2986
  TextControl,
2599
2987
  Toggle,
2600
2988
  TransitionControl,
2601
- useDialKit
2989
+ useDialKit,
2990
+ useDialKitController
2602
2991
  };
2603
2992
  //# sourceMappingURL=index.js.map