dialkit 1.2.1 → 1.4.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 (88) hide show
  1. package/README.md +653 -6
  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/icons.d.ts +4 -1
  6. package/dist/icons.js +13 -0
  7. package/dist/icons.js.map +1 -1
  8. package/dist/index.cjs +3796 -768
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +315 -10
  11. package/dist/index.d.ts +315 -10
  12. package/dist/index.js +3733 -711
  13. package/dist/index.js.map +1 -1
  14. package/dist/panel-drag.d.ts +19 -0
  15. package/dist/panel-drag.js +72 -0
  16. package/dist/panel-drag.js.map +1 -0
  17. package/dist/solid/index.d.ts +264 -5
  18. package/dist/solid/index.js +5277 -1632
  19. package/dist/solid/index.js.map +1 -1
  20. package/dist/store/index.cjs +329 -57
  21. package/dist/store/index.cjs.map +1 -1
  22. package/dist/store/index.d.cts +43 -4
  23. package/dist/store/index.d.ts +43 -4
  24. package/dist/store/index.js +321 -56
  25. package/dist/store/index.js.map +1 -1
  26. package/dist/styles.css +809 -7
  27. package/dist/svelte/components/ControlRenderer.svelte +5 -2
  28. package/dist/svelte/components/ControlRenderer.svelte.d.ts +2 -0
  29. package/dist/svelte/components/ControlRenderer.svelte.d.ts.map +1 -1
  30. package/dist/svelte/components/DialRoot.svelte +218 -16
  31. package/dist/svelte/components/DialRoot.svelte.d.ts +1 -0
  32. package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -1
  33. package/dist/svelte/components/Folder.svelte +24 -13
  34. package/dist/svelte/components/Folder.svelte.d.ts +1 -0
  35. package/dist/svelte/components/Folder.svelte.d.ts.map +1 -1
  36. package/dist/svelte/components/Panel.svelte +104 -71
  37. package/dist/svelte/components/Panel.svelte.d.ts +4 -0
  38. package/dist/svelte/components/Panel.svelte.d.ts.map +1 -1
  39. package/dist/svelte/components/PresetManager.svelte +5 -5
  40. package/dist/svelte/components/PresetManager.svelte.d.ts.map +1 -1
  41. package/dist/svelte/components/SelectControl.svelte +6 -14
  42. package/dist/svelte/components/SelectControl.svelte.d.ts.map +1 -1
  43. package/dist/svelte/components/Timeline/ClipPopover.svelte +206 -0
  44. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts +26 -0
  45. package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts.map +1 -0
  46. package/dist/svelte/components/Timeline/DialTimeline.svelte +76 -0
  47. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts +13 -0
  48. package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts.map +1 -0
  49. package/dist/svelte/components/Timeline/TimelineClip.svelte +233 -0
  50. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts +24 -0
  51. package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts.map +1 -0
  52. package/dist/svelte/components/Timeline/TimelineSection.svelte +756 -0
  53. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts +12 -0
  54. package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts.map +1 -0
  55. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte +25 -0
  56. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts +4 -0
  57. package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts.map +1 -0
  58. package/dist/svelte/components/TransitionControl.svelte +26 -11
  59. package/dist/svelte/components/TransitionControl.svelte.d.ts +9 -0
  60. package/dist/svelte/components/TransitionControl.svelte.d.ts.map +1 -1
  61. package/dist/svelte/createDialKit.svelte.d.ts +11 -1
  62. package/dist/svelte/createDialKit.svelte.d.ts.map +1 -1
  63. package/dist/svelte/createDialKit.svelte.js +61 -34
  64. package/dist/svelte/createDialTimeline.svelte.d.ts +4 -0
  65. package/dist/svelte/createDialTimeline.svelte.d.ts.map +1 -0
  66. package/dist/svelte/createDialTimeline.svelte.js +73 -0
  67. package/dist/svelte/index.d.ts +7 -3
  68. package/dist/svelte/index.d.ts.map +1 -1
  69. package/dist/svelte/index.js +4 -1
  70. package/dist/svelte/theme-css.d.ts +1 -1
  71. package/dist/svelte/theme-css.d.ts.map +1 -1
  72. package/dist/svelte/theme-css.js +809 -7
  73. package/dist/timeline/index.cjs +1288 -0
  74. package/dist/timeline/index.cjs.map +1 -0
  75. package/dist/timeline/index.d.cts +443 -0
  76. package/dist/timeline/index.d.ts +443 -0
  77. package/dist/timeline/index.js +1233 -0
  78. package/dist/timeline/index.js.map +1 -0
  79. package/dist/vue/index.d.ts +325 -12
  80. package/dist/vue/index.js +3226 -280
  81. package/dist/vue/index.js.map +1 -1
  82. package/package.json +23 -13
  83. package/dist/solid/index.cjs +0 -3079
  84. package/dist/solid/index.cjs.map +0 -1
  85. package/dist/solid/index.d.cts +0 -258
  86. package/dist/vue/index.cjs +0 -3056
  87. package/dist/vue/index.cjs.map +0 -1
  88. package/dist/vue/index.d.cts +0 -675
package/dist/vue/index.js CHANGED
@@ -3,9 +3,108 @@ import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
3
3
 
4
4
  // src/store/DialStore.ts
5
5
  var EMPTY_VALUES = Object.freeze({});
6
+ function resolveDialValues(config, flatValues) {
7
+ return resolveConfigValues(config, flatValues, "");
8
+ }
9
+ function flattenDialValueUpdates(config, updates) {
10
+ const values = {};
11
+ if (typeof updates === "object" && updates !== null) {
12
+ flattenConfigUpdates(config, updates, "", values);
13
+ }
14
+ return values;
15
+ }
16
+ function resolveConfigValues(config, flatValues, prefix) {
17
+ const result = {};
18
+ for (const [key, configValue] of Object.entries(config)) {
19
+ if (key === "_collapsed") continue;
20
+ const path = prefix ? `${prefix}.${key}` : key;
21
+ if (Array.isArray(configValue) && configValue.length <= 4 && typeof configValue[0] === "number") {
22
+ result[key] = flatValues[path] ?? configValue[0];
23
+ } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
24
+ result[key] = flatValues[path] ?? configValue;
25
+ } else if (isSpringConfigValue(configValue) || isEasingConfigValue(configValue)) {
26
+ result[key] = flatValues[path] ?? configValue;
27
+ } else if (isActionConfigValue(configValue)) {
28
+ result[key] = flatValues[path] ?? configValue;
29
+ } else if (isSelectConfigValue(configValue)) {
30
+ const defaultValue = configValue.default ?? getFirstOptionValue(configValue.options);
31
+ result[key] = flatValues[path] ?? defaultValue;
32
+ } else if (isColorConfigValue(configValue)) {
33
+ result[key] = flatValues[path] ?? configValue.default ?? "#000000";
34
+ } else if (isTextConfigValue(configValue)) {
35
+ result[key] = flatValues[path] ?? configValue.default ?? "";
36
+ } else if (typeof configValue === "object" && configValue !== null) {
37
+ result[key] = resolveConfigValues(configValue, flatValues, path);
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ function flattenConfigUpdates(config, updates, prefix, values) {
43
+ for (const [key, configValue] of Object.entries(config)) {
44
+ if (key === "_collapsed" || !(key in updates)) continue;
45
+ const nextValue = updates[key];
46
+ if (nextValue === void 0) continue;
47
+ const path = prefix ? `${prefix}.${key}` : key;
48
+ if (isActionConfigValue(configValue)) {
49
+ continue;
50
+ }
51
+ if (isLeafConfigValue(configValue)) {
52
+ values[path] = nextValue;
53
+ continue;
54
+ }
55
+ if (typeof configValue === "object" && configValue !== null && typeof nextValue === "object" && nextValue !== null && !Array.isArray(nextValue)) {
56
+ flattenConfigUpdates(configValue, nextValue, path, values);
57
+ }
58
+ }
59
+ }
60
+ function isLeafConfigValue(value) {
61
+ 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);
62
+ }
63
+ function hasType(value, type) {
64
+ return typeof value === "object" && value !== null && "type" in value && value.type === type;
65
+ }
66
+ function isSpringConfigValue(value) {
67
+ return hasType(value, "spring");
68
+ }
69
+ function isEasingConfigValue(value) {
70
+ return hasType(value, "easing");
71
+ }
72
+ function isHexColor(value) {
73
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
74
+ }
75
+ function formatLabel(key) {
76
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
77
+ }
78
+ function inferStep(min, max) {
79
+ const range = max - min;
80
+ if (range <= 1) return 0.01;
81
+ if (range <= 10) return 0.1;
82
+ if (range <= 100) return 1;
83
+ return 10;
84
+ }
85
+ function isActionConfigValue(value) {
86
+ return hasType(value, "action");
87
+ }
88
+ function isSelectConfigValue(value) {
89
+ return hasType(value, "select") && "options" in value && Array.isArray(value.options);
90
+ }
91
+ function isColorConfigValue(value) {
92
+ return hasType(value, "color");
93
+ }
94
+ function isTextConfigValue(value) {
95
+ return hasType(value, "text");
96
+ }
97
+ function getFirstOptionValue(options) {
98
+ const first = options[0];
99
+ if (first === void 0) return "";
100
+ return typeof first === "string" ? first : first.value;
101
+ }
6
102
  var DialStoreClass = class {
7
103
  constructor() {
8
104
  this.panels = /* @__PURE__ */ new Map();
105
+ this.panelsSnapshot = [];
106
+ this.standardPanelsSnapshot = [];
107
+ this.timelinePanelsSnapshot = [];
9
108
  this.listeners = /* @__PURE__ */ new Map();
10
109
  this.globalListeners = /* @__PURE__ */ new Set();
11
110
  this.snapshots = /* @__PURE__ */ new Map();
@@ -13,87 +112,144 @@ var DialStoreClass = class {
13
112
  this.presets = /* @__PURE__ */ new Map();
14
113
  this.activePreset = /* @__PURE__ */ new Map();
15
114
  this.baseValues = /* @__PURE__ */ new Map();
115
+ this.defaultValues = /* @__PURE__ */ new Map();
116
+ this.registrationCounts = /* @__PURE__ */ new Map();
117
+ this.retainedPanels = /* @__PURE__ */ new Set();
118
+ this.persistConfigs = /* @__PURE__ */ new Map();
16
119
  }
17
- registerPanel(id, name, config, shortcuts) {
120
+ registerPanel(id, name, config, shortcuts, options = {}) {
121
+ const existingPanel = this.panels.get(id);
122
+ if (existingPanel && existingPanel.kind !== options.kind) {
123
+ console.warn(
124
+ `[dialkit] Panel id "${id}" cannot be shared by a timeline and a standard panel; the most recent registration controls where it renders.`
125
+ );
126
+ }
127
+ this.configurePanelRetention(id, options);
128
+ this.registrationCounts.set(id, (this.registrationCounts.get(id) ?? 0) + 1);
18
129
  const controls = this.parseConfig(config, "", shortcuts);
19
- const values = this.flattenValues(config, "");
20
- this.initTransitionModes(config, "", values);
21
- this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {} });
130
+ const controlsByPath = this.mapControlsByPath(controls);
131
+ const defaultValues = this.flattenValues(config, "");
132
+ this.initTransitionModes(config, "", defaultValues);
133
+ const persisted = this.loadPersistedPanel(id);
134
+ const previousValues = this.panels.get(id)?.values ?? this.snapshots.get(id) ?? persisted?.values ?? {};
135
+ const values = this.reconcileValues(defaultValues, previousValues, controlsByPath);
136
+ const previousBaseValues = this.baseValues.get(id) ?? persisted?.baseValues ?? persisted?.values ?? {};
137
+ const baseValues = this.reconcileValues(defaultValues, previousBaseValues, controlsByPath);
138
+ this.panels.set(id, { id, name, controls, values, shortcuts: shortcuts ?? {}, kind: options.kind });
22
139
  this.snapshots.set(id, { ...values });
23
- this.baseValues.set(id, { ...values });
140
+ this.baseValues.set(id, baseValues);
141
+ this.defaultValues.set(id, { ...defaultValues });
142
+ const existingPresets = this.presets.get(id) ?? persisted?.presets;
143
+ if (existingPresets) {
144
+ this.presets.set(id, this.reconcilePresets(existingPresets, defaultValues, controlsByPath));
145
+ }
146
+ if (!this.activePreset.has(id) && persisted?.activePresetId !== void 0) {
147
+ this.activePreset.set(id, persisted.activePresetId);
148
+ }
149
+ this.persistPanel(id);
150
+ this.notify(id);
24
151
  this.notifyGlobal();
25
152
  }
26
- updatePanel(id, name, config, shortcuts) {
153
+ updatePanel(id, name, config, shortcuts, options = {}) {
154
+ this.configurePanelRetention(id, options);
27
155
  const existing = this.panels.get(id);
28
156
  if (!existing) {
29
- this.registerPanel(id, name, config, shortcuts);
157
+ this.registerPanel(id, name, config, shortcuts, options);
30
158
  return;
31
159
  }
32
160
  const controls = this.parseConfig(config, "", shortcuts);
33
161
  const controlsByPath = this.mapControlsByPath(controls);
34
162
  const defaultValues = this.flattenValues(config, "");
35
- const nextValues = {};
36
- for (const [path, defaultValue] of Object.entries(defaultValues)) {
37
- nextValues[path] = this.normalizePreservedValue(
38
- existing.values[path],
39
- defaultValue,
40
- controlsByPath.get(path)
41
- );
42
- }
43
- this.initTransitionModes(config, "", nextValues);
44
- for (const [path, mode] of Object.entries(existing.values)) {
45
- if (!path.endsWith(".__mode")) {
46
- continue;
47
- }
48
- const transitionPath = path.slice(0, -"__mode".length - 1);
49
- const transitionControl = controlsByPath.get(transitionPath);
50
- if (transitionControl?.type === "transition") {
51
- nextValues[path] = mode;
52
- }
53
- }
54
- const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts };
163
+ this.initTransitionModes(config, "", defaultValues);
164
+ const nextValues = this.reconcileValues(defaultValues, existing.values, controlsByPath);
165
+ const nextPanel = { id, name, controls, values: nextValues, shortcuts: shortcuts ?? existing.shortcuts, kind: options.kind ?? existing.kind };
55
166
  this.panels.set(id, nextPanel);
56
167
  this.snapshots.set(id, { ...nextValues });
57
168
  const previousBaseValues = this.baseValues.get(id) ?? {};
58
- const nextBaseValues = {};
59
- for (const [path, defaultValue] of Object.entries(defaultValues)) {
60
- nextBaseValues[path] = this.normalizePreservedValue(
61
- previousBaseValues[path],
62
- defaultValue,
63
- controlsByPath.get(path)
64
- );
65
- }
169
+ const nextBaseValues = this.reconcileValues(defaultValues, previousBaseValues, controlsByPath);
66
170
  for (const [path, value] of Object.entries(nextValues)) {
67
171
  if (path.endsWith(".__mode")) {
68
172
  nextBaseValues[path] = value;
69
173
  }
70
174
  }
71
175
  this.baseValues.set(id, nextBaseValues);
176
+ this.defaultValues.set(id, { ...defaultValues });
177
+ this.presets.set(id, this.reconcilePresets(this.presets.get(id) ?? [], defaultValues, controlsByPath));
178
+ this.persistPanel(id);
72
179
  this.notify(id);
73
180
  this.notifyGlobal();
74
181
  }
75
182
  unregisterPanel(id) {
183
+ const nextCount = (this.registrationCounts.get(id) ?? 1) - 1;
184
+ if (nextCount > 0) {
185
+ this.registrationCounts.set(id, nextCount);
186
+ return;
187
+ }
188
+ this.registrationCounts.delete(id);
76
189
  this.panels.delete(id);
77
- this.listeners.delete(id);
78
- this.snapshots.delete(id);
79
- this.actionListeners.delete(id);
80
- this.baseValues.delete(id);
190
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
191
+ if (this.actionListeners.get(id)?.size === 0) this.actionListeners.delete(id);
192
+ if (!this.retainedPanels.has(id)) {
193
+ this.snapshots.delete(id);
194
+ this.baseValues.delete(id);
195
+ this.defaultValues.delete(id);
196
+ this.presets.delete(id);
197
+ this.activePreset.delete(id);
198
+ this.persistConfigs.delete(id);
199
+ }
81
200
  this.notifyGlobal();
82
201
  }
83
202
  updateValue(panelId, path, value) {
203
+ this.updateValues(panelId, { [path]: value });
204
+ }
205
+ updateValues(panelId, updates) {
84
206
  const panel = this.panels.get(panelId);
85
207
  if (!panel) return;
86
- panel.values[path] = value;
208
+ const validUpdates = {};
209
+ for (const [path, value] of Object.entries(updates)) {
210
+ if (!Object.prototype.hasOwnProperty.call(panel.values, path)) {
211
+ continue;
212
+ }
213
+ const control = this.findControlByPath(panel.controls, path);
214
+ if (control?.type === "action") {
215
+ continue;
216
+ }
217
+ panel.values[path] = value;
218
+ validUpdates[path] = value;
219
+ }
220
+ if (Object.keys(validUpdates).length === 0) {
221
+ return;
222
+ }
87
223
  const activeId = this.activePreset.get(panelId);
88
224
  if (activeId) {
89
225
  const presets = this.presets.get(panelId) ?? [];
90
226
  const preset = presets.find((p) => p.id === activeId);
91
- if (preset) preset.values[path] = value;
227
+ if (preset) {
228
+ for (const [path, value] of Object.entries(validUpdates)) {
229
+ preset.values[path] = value;
230
+ }
231
+ }
92
232
  } else {
93
233
  const base = this.baseValues.get(panelId);
94
- if (base) base[path] = value;
234
+ if (base) {
235
+ for (const [path, value] of Object.entries(validUpdates)) {
236
+ base[path] = value;
237
+ }
238
+ }
95
239
  }
96
240
  this.snapshots.set(panelId, { ...panel.values });
241
+ this.persistPanel(panelId);
242
+ this.notify(panelId);
243
+ }
244
+ resetValues(panelId) {
245
+ const panel = this.panels.get(panelId);
246
+ const defaults = this.defaultValues.get(panelId);
247
+ if (!panel || !defaults) return;
248
+ panel.values = { ...defaults };
249
+ this.snapshots.set(panelId, { ...panel.values });
250
+ this.baseValues.set(panelId, { ...defaults });
251
+ this.activePreset.set(panelId, null);
252
+ this.persistPanel(panelId);
97
253
  this.notify(panelId);
98
254
  }
99
255
  updateSpringMode(panelId, path, mode) {
@@ -109,6 +265,7 @@ var DialStoreClass = class {
109
265
  if (!panel) return;
110
266
  panel.values[`${path}.__mode`] = mode;
111
267
  this.snapshots.set(panelId, { ...panel.values });
268
+ this.persistPanel(panelId);
112
269
  this.notify(panelId);
113
270
  }
114
271
  getTransitionMode(panelId, path) {
@@ -123,8 +280,10 @@ var DialStoreClass = class {
123
280
  getValues(panelId) {
124
281
  return this.snapshots.get(panelId) ?? EMPTY_VALUES;
125
282
  }
126
- getPanels() {
127
- return Array.from(this.panels.values());
283
+ getPanels(kind) {
284
+ if (kind === "panel") return this.standardPanelsSnapshot;
285
+ if (kind === "timeline") return this.timelinePanelsSnapshot;
286
+ return this.panelsSnapshot;
128
287
  }
129
288
  getPanel(id) {
130
289
  return this.panels.get(id);
@@ -135,7 +294,11 @@ var DialStoreClass = class {
135
294
  }
136
295
  this.listeners.get(panelId).add(listener);
137
296
  return () => {
138
- this.listeners.get(panelId)?.delete(listener);
297
+ const listeners = this.listeners.get(panelId);
298
+ listeners?.delete(listener);
299
+ if (listeners?.size === 0 && !this.panels.has(panelId)) {
300
+ this.listeners.delete(panelId);
301
+ }
139
302
  };
140
303
  }
141
304
  subscribeGlobal(listener) {
@@ -148,7 +311,11 @@ var DialStoreClass = class {
148
311
  }
149
312
  this.actionListeners.get(panelId).add(listener);
150
313
  return () => {
151
- this.actionListeners.get(panelId)?.delete(listener);
314
+ const listeners = this.actionListeners.get(panelId);
315
+ listeners?.delete(listener);
316
+ if (listeners?.size === 0 && !this.panels.has(panelId)) {
317
+ this.actionListeners.delete(panelId);
318
+ }
152
319
  };
153
320
  }
154
321
  triggerAction(panelId, path) {
@@ -167,6 +334,7 @@ var DialStoreClass = class {
167
334
  this.presets.set(panelId, [...existing, preset]);
168
335
  this.activePreset.set(panelId, id);
169
336
  this.snapshots.set(panelId, { ...panel.values });
337
+ this.persistPanel(panelId);
170
338
  this.notify(panelId);
171
339
  return id;
172
340
  }
@@ -179,6 +347,7 @@ var DialStoreClass = class {
179
347
  panel.values = { ...preset.values };
180
348
  this.snapshots.set(panelId, { ...panel.values });
181
349
  this.activePreset.set(panelId, presetId);
350
+ this.persistPanel(panelId);
182
351
  this.notify(panelId);
183
352
  }
184
353
  deletePreset(panelId, presetId) {
@@ -191,6 +360,7 @@ var DialStoreClass = class {
191
360
  if (panel) {
192
361
  this.snapshots.set(panelId, { ...panel.values });
193
362
  }
363
+ this.persistPanel(panelId);
194
364
  this.notify(panelId);
195
365
  }
196
366
  getPresets(panelId) {
@@ -207,6 +377,7 @@ var DialStoreClass = class {
207
377
  this.snapshots.set(panelId, { ...panel.values });
208
378
  }
209
379
  this.activePreset.set(panelId, null);
380
+ this.persistPanel(panelId);
210
381
  this.notify(panelId);
211
382
  }
212
383
  resolveShortcutTarget(key, modifier) {
@@ -237,6 +408,94 @@ var DialStoreClass = class {
237
408
  }
238
409
  return results;
239
410
  }
411
+ configurePanelRetention(id, options) {
412
+ if (options.retainOnUnmount) {
413
+ this.retainedPanels.add(id);
414
+ }
415
+ const persistConfig = this.normalizePersistConfig(id, options.persist);
416
+ if (persistConfig) {
417
+ this.persistConfigs.set(id, persistConfig);
418
+ this.retainedPanels.add(id);
419
+ }
420
+ }
421
+ reconcileValues(defaultValues, previousValues, controlsByPath) {
422
+ const nextValues = {};
423
+ for (const [path, defaultValue] of Object.entries(defaultValues)) {
424
+ if (path.endsWith(".__mode")) {
425
+ const transitionPath = path.slice(0, -".__mode".length);
426
+ const transitionControl = controlsByPath.get(transitionPath);
427
+ nextValues[path] = transitionControl?.type === "transition" && previousValues[path] !== void 0 ? previousValues[path] : defaultValue;
428
+ continue;
429
+ }
430
+ nextValues[path] = this.normalizePreservedValue(
431
+ previousValues[path],
432
+ defaultValue,
433
+ controlsByPath.get(path)
434
+ );
435
+ }
436
+ return nextValues;
437
+ }
438
+ reconcilePresets(presets, defaultValues, controlsByPath) {
439
+ return presets.map((preset) => ({
440
+ ...preset,
441
+ values: this.reconcileValues(defaultValues, preset.values, controlsByPath)
442
+ }));
443
+ }
444
+ normalizePersistConfig(id, persist) {
445
+ if (!persist) return null;
446
+ const options = typeof persist === "object" ? persist : {};
447
+ return {
448
+ key: options.key ?? `dialkit:${id}`,
449
+ storage: options.storage ?? "localStorage",
450
+ presets: options.presets ?? true
451
+ };
452
+ }
453
+ loadPersistedPanel(id) {
454
+ const config = this.persistConfigs.get(id);
455
+ if (!config) return null;
456
+ const storage = this.getStorage(config.storage);
457
+ if (!storage) return null;
458
+ try {
459
+ const raw = storage.getItem(config.key);
460
+ if (!raw) return null;
461
+ const parsed = JSON.parse(raw);
462
+ if (parsed?.version !== 1 || typeof parsed !== "object") return null;
463
+ return parsed;
464
+ } catch {
465
+ return null;
466
+ }
467
+ }
468
+ persistPanel(id) {
469
+ const config = this.persistConfigs.get(id);
470
+ if (!config) return;
471
+ const storage = this.getStorage(config.storage);
472
+ if (!storage) return;
473
+ const values = this.snapshots.get(id) ?? this.panels.get(id)?.values;
474
+ if (!values) return;
475
+ const state = {
476
+ version: 1,
477
+ values,
478
+ baseValues: this.baseValues.get(id) ?? values,
479
+ activePresetId: this.activePreset.get(id) ?? null
480
+ };
481
+ if (config.presets) {
482
+ state.presets = this.presets.get(id) ?? [];
483
+ }
484
+ try {
485
+ storage.setItem(config.key, JSON.stringify(state));
486
+ } catch {
487
+ }
488
+ }
489
+ getStorage(kind) {
490
+ if (typeof globalThis === "undefined" || !("window" in globalThis)) {
491
+ return null;
492
+ }
493
+ try {
494
+ return kind === "sessionStorage" ? globalThis.window?.sessionStorage ?? null : globalThis.window?.localStorage ?? null;
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
240
499
  findControlByPath(controls, path) {
241
500
  for (const control of controls) {
242
501
  if (control.path === path) return control;
@@ -251,6 +510,9 @@ var DialStoreClass = class {
251
510
  this.listeners.get(panelId)?.forEach((fn) => fn());
252
511
  }
253
512
  notifyGlobal() {
513
+ this.panelsSnapshot = Array.from(this.panels.values());
514
+ this.standardPanelsSnapshot = this.panelsSnapshot.filter((panel) => panel.kind !== "timeline");
515
+ this.timelinePanelsSnapshot = this.panelsSnapshot.filter((panel) => panel.kind === "timeline");
254
516
  this.globalListeners.forEach((fn) => fn());
255
517
  }
256
518
  initTransitionModes(config, prefix, values) {
@@ -366,10 +628,10 @@ var DialStoreClass = class {
366
628
  return typeof value === "object" && value !== null && "type" in value && value.type === "text";
367
629
  }
368
630
  isHexColor(value) {
369
- return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(value);
631
+ return isHexColor(value);
370
632
  }
371
633
  formatLabel(key) {
372
- return key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
634
+ return formatLabel(key);
373
635
  }
374
636
  inferRange(value) {
375
637
  if (value >= 0 && value <= 1) {
@@ -385,11 +647,7 @@ var DialStoreClass = class {
385
647
  }
386
648
  }
387
649
  inferStep(min, max) {
388
- const range = max - min;
389
- if (range <= 1) return 0.01;
390
- if (range <= 10) return 0.1;
391
- if (range <= 100) return 1;
392
- return 10;
650
+ return inferStep(min, max);
393
651
  }
394
652
  normalizePreservedValue(existingValue, defaultValue, control) {
395
653
  if (existingValue === void 0 || !control) {
@@ -461,26 +719,35 @@ var DialStoreClass = class {
461
719
  return map;
462
720
  }
463
721
  };
464
- var DialStore = new DialStoreClass();
722
+ var DialStore = /* @__PURE__ */ new DialStoreClass();
465
723
 
466
724
  // src/vue/useDialKit.ts
467
725
  var dialKitInstance = 0;
468
726
  function useDialKit(name, config, options) {
469
- const panelId = `${name}-${++dialKitInstance}`;
727
+ return useDialKitController(name, config, options).values;
728
+ }
729
+ function useDialKitController(name, config, options) {
730
+ const hasStableId = options?.id !== void 0;
731
+ const panelId = options?.id ?? `${name}-${++dialKitInstance}`;
470
732
  const configRef = shallowRef(config);
471
733
  const onActionRef = ref(options?.onAction);
472
734
  const shortcutsRef = shallowRef(options?.shortcuts);
473
- const values = ref(DialStore.getValues(panelId));
735
+ const persistRef = shallowRef(options?.persist);
736
+ const flatValues = ref(DialStore.getValues(panelId));
474
737
  const mounted = ref(false);
475
738
  const serializedConfig = computed(() => JSON.stringify(config));
476
739
  const serializedShortcuts = computed(() => JSON.stringify(options?.shortcuts));
740
+ const serializedPersist = computed(() => JSON.stringify(options?.persist));
477
741
  let unsubscribeValues;
478
742
  let unsubscribeActions;
479
743
  const register = () => {
480
- DialStore.registerPanel(panelId, name, configRef.value, shortcutsRef.value);
481
- values.value = DialStore.getValues(panelId);
744
+ DialStore.registerPanel(panelId, name, configRef.value, shortcutsRef.value, {
745
+ retainOnUnmount: hasStableId,
746
+ persist: persistRef.value
747
+ });
748
+ flatValues.value = DialStore.getValues(panelId);
482
749
  unsubscribeValues = DialStore.subscribe(panelId, () => {
483
- values.value = DialStore.getValues(panelId);
750
+ flatValues.value = DialStore.getValues(panelId);
484
751
  });
485
752
  unsubscribeActions = DialStore.subscribeActions(panelId, (action) => {
486
753
  onActionRef.value?.(action);
@@ -492,12 +759,19 @@ function useDialKit(name, config, options) {
492
759
  watch(() => options?.shortcuts, (next) => {
493
760
  shortcutsRef.value = next;
494
761
  });
495
- watch([serializedConfig, serializedShortcuts], () => {
762
+ watch(() => options?.persist, (next) => {
763
+ persistRef.value = next;
764
+ });
765
+ watch([serializedConfig, serializedShortcuts, serializedPersist], () => {
496
766
  configRef.value = config;
497
767
  shortcutsRef.value = options?.shortcuts;
768
+ persistRef.value = options?.persist;
498
769
  if (mounted.value) {
499
- DialStore.updatePanel(panelId, name, configRef.value, shortcutsRef.value);
500
- values.value = DialStore.getValues(panelId);
770
+ DialStore.updatePanel(panelId, name, configRef.value, shortcutsRef.value, {
771
+ retainOnUnmount: hasStableId,
772
+ persist: persistRef.value
773
+ });
774
+ flatValues.value = DialStore.getValues(panelId);
501
775
  }
502
776
  });
503
777
  onMounted(register);
@@ -509,88 +783,1344 @@ function useDialKit(name, config, options) {
509
783
  unsubscribeActions?.();
510
784
  DialStore.unregisterPanel(panelId);
511
785
  });
512
- return computed(() => buildResolvedValues(configRef.value, values.value, ""));
786
+ const values = computed(() => resolveDialValues(configRef.value, flatValues.value));
787
+ return {
788
+ values,
789
+ setValue(path, value) {
790
+ DialStore.updateValue(panelId, path, value);
791
+ },
792
+ setValues(nextValues) {
793
+ DialStore.updateValues(panelId, flattenDialValueUpdates(configRef.value, nextValues));
794
+ },
795
+ resetValues() {
796
+ DialStore.resetValues(panelId);
797
+ },
798
+ getValues() {
799
+ return resolveDialValues(configRef.value, DialStore.getValues(panelId));
800
+ }
801
+ };
513
802
  }
514
- function buildResolvedValues(config, flatValues, prefix) {
515
- const result = {};
516
- for (const [key, configValue] of Object.entries(config)) {
517
- if (key === "_collapsed") continue;
518
- const path = prefix ? `${prefix}.${key}` : key;
519
- if (Array.isArray(configValue) && configValue.length <= 4 && typeof configValue[0] === "number") {
520
- result[key] = flatValues[path] ?? configValue[0];
521
- } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
522
- result[key] = flatValues[path] ?? configValue;
523
- } else if (isSpringConfig(configValue) || isEasingConfig(configValue)) {
524
- result[key] = flatValues[path] ?? configValue;
525
- } else if (isActionConfig(configValue)) {
526
- result[key] = flatValues[path] ?? configValue;
527
- } else if (isSelectConfig(configValue)) {
528
- const defaultValue = configValue.default ?? getFirstOptionValue(configValue.options);
529
- result[key] = flatValues[path] ?? defaultValue;
530
- } else if (isColorConfig(configValue)) {
531
- result[key] = flatValues[path] ?? configValue.default ?? "#000000";
532
- } else if (isTextConfig(configValue)) {
533
- result[key] = flatValues[path] ?? configValue.default ?? "";
534
- } else if (typeof configValue === "object" && configValue !== null) {
535
- result[key] = buildResolvedValues(configValue, flatValues, path);
803
+
804
+ // src/vue/useDialTimeline.ts
805
+ import { computed as computed2, onMounted as onMounted2, onUnmounted as onUnmounted2, shallowRef as shallowRef2, watch as watch2 } from "vue";
806
+
807
+ // src/store/TimelineStore.ts
808
+ function loopSpan(duration, loopStart) {
809
+ if (!Number.isFinite(duration) || duration <= 0) return 0;
810
+ if (!Number.isFinite(loopStart)) loopStart = 0;
811
+ const start = Math.min(Math.max(0, loopStart), duration);
812
+ return duration - start > 0 ? duration - start : duration;
813
+ }
814
+ function foldLoopTime(time, duration, loopStart = 0) {
815
+ if (!Number.isFinite(time) || !Number.isFinite(duration) || duration <= 0) {
816
+ return { time: 0, wraps: 0 };
817
+ }
818
+ if (time < duration) return { time, wraps: 0 };
819
+ const span = loopSpan(duration, loopStart);
820
+ const base = duration - span;
821
+ const over = time - base;
822
+ return { time: base + over % span, wraps: Math.floor(over / span) };
823
+ }
824
+ var TIMELINE_CLIP_COLORS = [
825
+ "#E8E8E8"
826
+ // neutral white — slightly off-white so the pure-white selection ring still reads
827
+ ];
828
+ var EMPTY_TRANSPORT = Object.freeze({ time: 0, playing: false, duration: 0, wraps: 0 });
829
+ var TimelineStoreClass = class {
830
+ constructor() {
831
+ this.timelines = /* @__PURE__ */ new Map();
832
+ this.transports = /* @__PURE__ */ new Map();
833
+ this.listeners = /* @__PURE__ */ new Map();
834
+ this.globalListeners = /* @__PURE__ */ new Set();
835
+ this.registrationCounts = /* @__PURE__ */ new Map();
836
+ this.listCache = null;
837
+ this.rafId = null;
838
+ this.lastTick = 0;
839
+ this.tick = (now) => {
840
+ const dt = Math.max(0, (now - this.lastTick) / 1e3);
841
+ this.lastTick = now;
842
+ let anyPlaying = false;
843
+ for (const [id, transport] of this.transports) {
844
+ if (!transport.playing) continue;
845
+ const meta = this.timelines.get(id);
846
+ const duration = meta?.duration ?? transport.duration;
847
+ if (!Number.isFinite(duration) || duration <= 0) {
848
+ this.transports.set(id, { time: 0, playing: false, duration: 0, wraps: 0 });
849
+ this.notify(id);
850
+ continue;
851
+ }
852
+ let time = transport.time + dt;
853
+ let playing = true;
854
+ let wraps = transport.wraps;
855
+ if (time >= duration) {
856
+ if (meta?.loop) {
857
+ const folded = foldLoopTime(time, duration, meta.loopStart);
858
+ time = folded.time;
859
+ wraps += folded.wraps;
860
+ } else {
861
+ time = duration;
862
+ playing = false;
863
+ }
864
+ }
865
+ this.transports.set(id, { time, playing, duration, wraps });
866
+ if (playing) anyPlaying = true;
867
+ this.notify(id);
868
+ }
869
+ this.rafId = anyPlaying ? window.requestAnimationFrame(this.tick) : null;
870
+ };
871
+ }
872
+ register(meta, options) {
873
+ const existing = this.timelines.get(meta.id);
874
+ if (existing && existing.name !== meta.name) {
875
+ console.warn(
876
+ `[dialkit] Timeline id "${meta.id}" is already registered by "${existing.name}"; "${meta.name}" will share and overwrite that transport.`
877
+ );
536
878
  }
879
+ this.registrationCounts.set(meta.id, (this.registrationCounts.get(meta.id) ?? 0) + 1);
880
+ this.applyMeta(meta, options.autoplay);
537
881
  }
538
- return result;
882
+ update(meta) {
883
+ if (!this.timelines.has(meta.id)) return;
884
+ this.applyMeta(meta, false);
885
+ }
886
+ unregister(id) {
887
+ const nextCount = (this.registrationCounts.get(id) ?? 1) - 1;
888
+ if (nextCount > 0) {
889
+ this.registrationCounts.set(id, nextCount);
890
+ return;
891
+ }
892
+ this.registrationCounts.delete(id);
893
+ this.timelines.delete(id);
894
+ this.transports.delete(id);
895
+ if (this.listeners.get(id)?.size === 0) this.listeners.delete(id);
896
+ this.listCache = null;
897
+ this.notifyGlobal();
898
+ }
899
+ play(id) {
900
+ const transport = this.transports.get(id);
901
+ if (!transport || transport.duration <= 0 || transport.playing) return;
902
+ const restart = transport.time >= transport.duration;
903
+ this.transports.set(id, {
904
+ ...transport,
905
+ time: restart ? 0 : transport.time,
906
+ wraps: restart ? 0 : transport.wraps,
907
+ playing: true
908
+ });
909
+ this.notify(id);
910
+ this.ensureLoop();
911
+ }
912
+ pause(id) {
913
+ const transport = this.transports.get(id);
914
+ if (!transport || !transport.playing) return;
915
+ this.transports.set(id, { ...transport, playing: false });
916
+ this.notify(id);
917
+ }
918
+ replay(id) {
919
+ const transport = this.transports.get(id);
920
+ if (!transport || transport.duration <= 0) return;
921
+ this.transports.set(id, { ...transport, time: 0, wraps: 0, playing: true });
922
+ this.notify(id);
923
+ this.ensureLoop();
924
+ }
925
+ seek(id, time) {
926
+ const transport = this.transports.get(id);
927
+ if (!transport || !Number.isFinite(time)) return;
928
+ const clamped = Math.min(transport.duration, Math.max(0, time));
929
+ this.transports.set(id, { ...transport, time: clamped, wraps: 0 });
930
+ this.notify(id);
931
+ }
932
+ getTransport(id) {
933
+ return this.transports.get(id) ?? EMPTY_TRANSPORT;
934
+ }
935
+ getTimeline(id) {
936
+ return this.timelines.get(id);
937
+ }
938
+ getTimelines() {
939
+ if (!this.listCache) {
940
+ this.listCache = Array.from(this.timelines.values());
941
+ }
942
+ return this.listCache;
943
+ }
944
+ subscribe(id, listener) {
945
+ if (!this.listeners.has(id)) {
946
+ this.listeners.set(id, /* @__PURE__ */ new Set());
947
+ }
948
+ this.listeners.get(id).add(listener);
949
+ return () => {
950
+ const listeners = this.listeners.get(id);
951
+ listeners?.delete(listener);
952
+ if (listeners?.size === 0 && !this.timelines.has(id)) {
953
+ this.listeners.delete(id);
954
+ }
955
+ };
956
+ }
957
+ subscribeGlobal(listener) {
958
+ this.globalListeners.add(listener);
959
+ return () => {
960
+ this.globalListeners.delete(listener);
961
+ };
962
+ }
963
+ applyMeta(meta, autoplay) {
964
+ const duration = Number.isFinite(meta.duration) ? Math.max(0, meta.duration) : 0;
965
+ const loopStart = Number.isFinite(meta.loopStart) ? Math.min(duration, Math.max(0, meta.loopStart)) : 0;
966
+ const safeMeta = { ...meta, duration, loopStart };
967
+ this.timelines.set(meta.id, safeMeta);
968
+ const existing = this.transports.get(meta.id);
969
+ if (existing) {
970
+ this.transports.set(meta.id, {
971
+ time: Math.min(existing.time, duration),
972
+ playing: duration > 0 && existing.playing,
973
+ duration,
974
+ wraps: existing.wraps
975
+ });
976
+ } else {
977
+ const playing = duration > 0 && autoplay;
978
+ this.transports.set(meta.id, { time: 0, playing, duration, wraps: 0 });
979
+ if (playing) this.ensureLoop();
980
+ }
981
+ this.listCache = null;
982
+ this.notify(meta.id);
983
+ this.notifyGlobal();
984
+ }
985
+ ensureLoop() {
986
+ if (this.rafId !== null || typeof window === "undefined") return;
987
+ this.lastTick = performance.now();
988
+ this.rafId = window.requestAnimationFrame(this.tick);
989
+ }
990
+ notify(id) {
991
+ this.listeners.get(id)?.forEach((fn) => fn());
992
+ }
993
+ notifyGlobal() {
994
+ this.globalListeners.forEach((fn) => fn());
995
+ }
996
+ };
997
+ var TimelineStore = /* @__PURE__ */ new TimelineStoreClass();
998
+
999
+ // src/transition-math.ts
1000
+ function round2(value) {
1001
+ return Math.round(value * 100) / 100;
539
1002
  }
540
- function hasType(value, type) {
541
- return typeof value === "object" && value !== null && "type" in value && value.type === type;
1003
+ function clamp(value, min, max) {
1004
+ return Math.min(max, Math.max(min, value));
542
1005
  }
543
- function isSpringConfig(value) {
544
- return hasType(value, "spring");
1006
+ function isTransitionConfig(value) {
1007
+ return isSpringConfigValue(value) || isEasingConfigValue(value);
545
1008
  }
546
- function isEasingConfig(value) {
547
- return hasType(value, "easing");
1009
+ function isPhysicsSpring(transition) {
1010
+ return transition.type === "spring" && (transition.stiffness !== void 0 || transition.damping !== void 0 || transition.mass !== void 0);
548
1011
  }
549
- function isActionConfig(value) {
550
- return hasType(value, "action");
1012
+ function springParams(spring) {
1013
+ if (isPhysicsSpring(spring)) {
1014
+ return { stiffness: spring.stiffness ?? 200, damping: spring.damping ?? 25, mass: spring.mass ?? 1 };
1015
+ }
1016
+ const visualDuration = Math.max(0.05, spring.visualDuration ?? 0.3);
1017
+ const bounce = spring.bounce ?? 0.3;
1018
+ const root = 2 * Math.PI / (visualDuration * 1.2);
1019
+ const stiffness = root * root;
1020
+ const damping = 2 * Math.min(1, Math.max(0.05, 1 - bounce)) * Math.sqrt(stiffness);
1021
+ return { stiffness, damping, mass: 1 };
551
1022
  }
552
- function isSelectConfig(value) {
553
- return hasType(value, "select") && "options" in value && Array.isArray(value.options);
1023
+ function springProgress(t, { stiffness, damping, mass }) {
1024
+ if (t <= 0) return 0;
1025
+ const w0 = Math.sqrt(stiffness / mass);
1026
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
1027
+ if (zeta < 0.9999) {
1028
+ const wd2 = w0 * Math.sqrt(1 - zeta * zeta);
1029
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd2 * t) + zeta * w0 / wd2 * Math.sin(wd2 * t));
1030
+ }
1031
+ if (zeta < 1.0001) {
1032
+ return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
1033
+ }
1034
+ const wd = w0 * Math.sqrt(zeta * zeta - 1);
1035
+ const r1 = -zeta * w0 + wd;
1036
+ const r2 = -zeta * w0 - wd;
1037
+ return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
554
1038
  }
555
- function isColorConfig(value) {
556
- return hasType(value, "color");
1039
+ function springSettleDuration(params) {
1040
+ const w0 = Math.sqrt(params.stiffness / params.mass);
1041
+ const zeta = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
1042
+ const decay = zeta >= 1 ? zeta * w0 - w0 * Math.sqrt(Math.max(0, zeta * zeta - 1)) : zeta * w0;
1043
+ const duration = Math.log(200) / Math.max(decay, 1e-6);
1044
+ return round2(clamp(duration, 0.05, 10));
557
1045
  }
558
- function isTextConfig(value) {
559
- return hasType(value, "text");
1046
+ function cubicBezierProgress(p, [x1, y1, x2, y2]) {
1047
+ if (p <= 0) return 0;
1048
+ if (p >= 1) return 1;
1049
+ const sampleX = (t2) => bezierAxis(t2, x1, x2);
1050
+ const sampleY = (t2) => bezierAxis(t2, y1, y2);
1051
+ let t = p;
1052
+ for (let i = 0; i < 8; i++) {
1053
+ const x = sampleX(t) - p;
1054
+ if (Math.abs(x) < 1e-5) return sampleY(t);
1055
+ const dx = bezierAxisDerivative(t, x1, x2);
1056
+ if (Math.abs(dx) < 1e-6) break;
1057
+ t -= x / dx;
1058
+ }
1059
+ let lo = 0;
1060
+ let hi = 1;
1061
+ t = p;
1062
+ while (hi - lo > 1e-5) {
1063
+ if (sampleX(t) < p) lo = t;
1064
+ else hi = t;
1065
+ t = (lo + hi) / 2;
1066
+ }
1067
+ return sampleY(t);
560
1068
  }
561
- function getFirstOptionValue(options) {
562
- const first = options[0];
563
- return typeof first === "string" ? first : first.value;
1069
+ function bezierAxis(t, a1, a2) {
1070
+ return (1 - 3 * a2 + 3 * a1) * t * t * t + (3 * a2 - 6 * a1) * t * t + 3 * a1 * t;
1071
+ }
1072
+ function bezierAxisDerivative(t, a1, a2) {
1073
+ return 3 * (1 - 3 * a2 + 3 * a1) * t * t + 2 * (3 * a2 - 6 * a1) * t + 3 * a1;
1074
+ }
1075
+ function resolveClipTransition(raw, clipDuration) {
1076
+ const safeDuration = Math.max(0.05, clipDuration);
1077
+ if (raw.type === "easing") {
1078
+ return {
1079
+ transition: { ...raw, duration: safeDuration },
1080
+ duration: safeDuration,
1081
+ isPhysics: false
1082
+ };
1083
+ }
1084
+ if (isPhysicsSpring(raw)) {
1085
+ return {
1086
+ transition: raw,
1087
+ duration: springSettleDuration(springParams(raw)),
1088
+ isPhysics: true
1089
+ };
1090
+ }
1091
+ return {
1092
+ transition: { type: "spring", bounce: raw.bounce ?? 0.2, visualDuration: safeDuration },
1093
+ duration: safeDuration,
1094
+ isPhysics: false
1095
+ };
564
1096
  }
565
1097
 
566
- // src/vue/directives/dialkit.ts
567
- import {
568
- createApp,
569
- defineComponent as defineComponent16,
570
- h as h16,
571
- shallowRef as shallowRef2
572
- } from "vue";
573
-
574
- // src/vue/components/DialRoot.ts
575
- import { defineComponent as defineComponent15, h as h15, onMounted as onMounted10, onUnmounted as onUnmounted9, ref as ref13, Teleport as Teleport3 } from "vue";
576
-
577
- // src/vue/components/Panel.ts
578
- import { Fragment, defineComponent as defineComponent14, h as h14, onMounted as onMounted9, onUnmounted as onUnmounted8, ref as ref12 } from "vue";
579
- import { AnimatePresence as AnimatePresence4, motion as motion4 } from "motion-v";
580
-
581
- // src/icons.ts
582
- var ICON_CHEVRON = "M6 9.5L12 15.5L18 9.5";
583
- var ICON_CHECK = "M5 12.75L10 19L19 5";
584
- var ICON_CLIPBOARD = {
585
- board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
586
- 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",
587
- 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"
588
- };
589
- var ICON_ADD_PRESET = [
590
- "M4 6H20",
591
- "M4 12H10",
592
- "M15 15L21 15",
593
- "M18 12V18",
1098
+ // src/timeline-core.ts
1099
+ var CLIP_VALUE_STEP = 0.01;
1100
+ var TIMELINE_MIN_CLIP_DURATION = 0.05;
1101
+ var DEFAULT_STEP_DURATION = 0.3;
1102
+ var DEFAULT_CLIP_TRANSITION = { type: "spring", bounce: 0.2 };
1103
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["time", "playing", "duration", "play", "pause", "replay", "seek"]);
1104
+ function isClipConfig(value) {
1105
+ return isPlainObject(value) && Number.isFinite(value.at);
1106
+ }
1107
+ function isGroupConfig(value) {
1108
+ if (!isPlainObject(value) || "at" in value) return false;
1109
+ const entries = Object.values(value);
1110
+ return entries.length > 0 && entries.some(isClipConfig);
1111
+ }
1112
+ function isPlainObject(value) {
1113
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1114
+ }
1115
+ function nonNegativeFinite(value, fallback = 0) {
1116
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : fallback;
1117
+ }
1118
+ function animatedDuration(value, fallback = DEFAULT_STEP_DURATION) {
1119
+ return Math.max(TIMELINE_MIN_CLIP_DURATION, nonNegativeFinite(value, fallback));
1120
+ }
1121
+ function transitionDefaultDuration(transition) {
1122
+ if (transition.type === "easing") return animatedDuration(transition.duration);
1123
+ if (isPhysicsSpring(transition)) {
1124
+ return animatedDuration(springSettleDuration(springParams(transition)));
1125
+ }
1126
+ if (transition.visualDuration !== void 0) return animatedDuration(transition.visualDuration);
1127
+ return animatedDuration(springSettleDuration(springParams(transition)));
1128
+ }
1129
+ function defaultStepDuration(step, inheritedTransition) {
1130
+ const curve = step.transition ?? inheritedTransition;
1131
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
1132
+ if (step.duration !== void 0) return animatedDuration(step.duration);
1133
+ if (step.transition) return transitionDefaultDuration(step.transition);
1134
+ return DEFAULT_STEP_DURATION;
1135
+ }
1136
+ function defaultTrackDuration(track, inheritedTransition) {
1137
+ const curve = track.transition ?? inheritedTransition;
1138
+ if (track.steps?.length) {
1139
+ return track.steps.reduce((sum, step) => sum + defaultStepDuration(step, curve), 0);
1140
+ }
1141
+ if (curve && isPhysicsSpring(curve)) return transitionDefaultDuration(curve);
1142
+ if (track.duration !== void 0) return animatedDuration(track.duration);
1143
+ if (track.transition) return transitionDefaultDuration(track.transition);
1144
+ return DEFAULT_STEP_DURATION;
1145
+ }
1146
+ function defaultClipDuration(clip) {
1147
+ const defaultCurve = isTransitionConfig(clip.transition) ? clip.transition : DEFAULT_CLIP_TRANSITION;
1148
+ if (clip.props) {
1149
+ return Object.values(clip.props).reduce(
1150
+ (max, track) => Math.max(
1151
+ max,
1152
+ nonNegativeFinite(track.delay) + defaultTrackDuration(track, defaultCurve)
1153
+ ),
1154
+ 0
1155
+ );
1156
+ }
1157
+ if (clip.steps?.length) {
1158
+ return clip.steps.reduce((sum, step) => sum + defaultStepDuration(step, defaultCurve), 0);
1159
+ }
1160
+ const animating = Boolean(clip.transition || clip.from || clip.to);
1161
+ if (!animating) return nonNegativeFinite(clip.duration);
1162
+ if (isPhysicsSpring(defaultCurve)) return transitionDefaultDuration(defaultCurve);
1163
+ if (clip.duration !== void 0) return animatedDuration(clip.duration);
1164
+ if (isTransitionConfig(clip.transition)) return transitionDefaultDuration(clip.transition);
1165
+ return clip.from || clip.to ? transitionDefaultDuration(DEFAULT_CLIP_TRANSITION) : 0;
1166
+ }
1167
+ function normalizeLoopMode(value) {
1168
+ if (value === true || value === "mirror" || value === "repeat") return "repeat";
1169
+ return "off";
1170
+ }
1171
+ function normalizeStoredTransition(transition, clipDuration) {
1172
+ if (transition.type === "easing") {
1173
+ return { ...transition, duration: clipDuration };
1174
+ }
1175
+ if (isPhysicsSpring(transition)) {
1176
+ return transition;
1177
+ }
1178
+ return { type: "spring", bounce: transition.bounce ?? 0.2 };
1179
+ }
1180
+ function collectClipEntries(config) {
1181
+ const entries = [];
1182
+ for (const [key, value] of Object.entries(config)) {
1183
+ if (key === "duration") continue;
1184
+ if (RESERVED_KEYS.has(key)) {
1185
+ console.warn(`[dialkit] Timeline key "${key}" collides with a reserved key and was skipped.`);
1186
+ continue;
1187
+ }
1188
+ if (isClipConfig(value)) {
1189
+ entries.push({ path: key, childKey: key, clip: value });
1190
+ } else if (isGroupConfig(value)) {
1191
+ for (const [childKey, childClip] of Object.entries(value)) {
1192
+ if (isClipConfig(childClip)) {
1193
+ entries.push({ path: `${key}.${childKey}`, childKey, group: key, clip: childClip });
1194
+ } else {
1195
+ console.warn(
1196
+ `[dialkit] Timeline clip "${key}.${childKey}" is missing a numeric "at" and was skipped.`
1197
+ );
1198
+ }
1199
+ }
1200
+ } else {
1201
+ console.warn(
1202
+ `[dialkit] Timeline entry "${key}" is neither a clip (needs a numeric "at") nor a group of clips and was skipped.`
1203
+ );
1204
+ }
1205
+ }
1206
+ return entries;
1207
+ }
1208
+ function definedValues(values) {
1209
+ if (!values) return void 0;
1210
+ const result = {};
1211
+ for (const [key, value] of Object.entries(values)) {
1212
+ if (value !== void 0) result[key] = value;
1213
+ }
1214
+ return result;
1215
+ }
1216
+ function setDialPath(dialConfig, path, value) {
1217
+ const segments = path.split(".");
1218
+ let node = dialConfig;
1219
+ for (const segment of segments.slice(0, -1)) {
1220
+ node = node[segment] ?? (node[segment] = {});
1221
+ }
1222
+ node[segments[segments.length - 1]] = value;
1223
+ }
1224
+ function parseTimelineConfig(config) {
1225
+ const entries = collectClipEntries(config);
1226
+ let maxEnd = 0;
1227
+ for (const { clip } of entries) {
1228
+ maxEnd = Math.max(maxEnd, nonNegativeFinite(clip.at) + defaultClipDuration(clip));
1229
+ }
1230
+ const duration = typeof config.duration === "number" && Number.isFinite(config.duration) && config.duration > 0 ? config.duration : maxEnd > 0 ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : 1;
1231
+ const dialConfig = {};
1232
+ const clips = [];
1233
+ entries.forEach(({ path, childKey, group, clip }, index) => {
1234
+ const raw = clip;
1235
+ if (raw.props && (raw.steps?.length || raw.from || raw.to)) {
1236
+ console.warn(
1237
+ `[dialkit] Timeline clip "${path}": "props" is mutually exclusive with from/to/steps \u2014 using "props".`
1238
+ );
1239
+ } else if (raw.steps?.length && raw.to) {
1240
+ console.warn(
1241
+ `[dialkit] Timeline clip "${path}": "to" is ignored when "steps" is present \u2014 each leg's "to" defines its targets.`
1242
+ );
1243
+ }
1244
+ const hasSteps = Boolean(clip.steps?.length) && !clip.props;
1245
+ const hasProps = Boolean(clip.props);
1246
+ const single = isTransitionConfig(clip.transition) ? clip.transition : void 0;
1247
+ const total = defaultClipDuration(clip);
1248
+ const defaultCurve = single ?? DEFAULT_CLIP_TRANSITION;
1249
+ const clipAt = nonNegativeFinite(clip.at);
1250
+ const clipDial = {
1251
+ at: [clipAt, 0, duration, CLIP_VALUE_STEP]
1252
+ };
1253
+ if (!hasSteps && !hasProps) {
1254
+ clipDial.duration = [total, 0, duration, CLIP_VALUE_STEP];
1255
+ }
1256
+ if (!hasSteps && !hasProps && (clip.transition || clip.from || clip.to)) {
1257
+ clipDial.transition = normalizeStoredTransition(defaultCurve, total);
1258
+ }
1259
+ let tracks;
1260
+ if (clip.props) {
1261
+ tracks = [];
1262
+ for (const [prop, track] of Object.entries(clip.props)) {
1263
+ if (TRACK_RESERVED.has(prop) || /^step\d+$/.test(prop)) {
1264
+ console.warn(`[dialkit] Timeline property "${prop}" collides with a clip field and was skipped.`);
1265
+ continue;
1266
+ }
1267
+ const trackDuration = defaultTrackDuration(track, defaultCurve);
1268
+ const trackCurve = track.transition ?? defaultCurve;
1269
+ const hasTrackSteps = Boolean(track.steps?.length);
1270
+ const trackDial = {
1271
+ delay: [nonNegativeFinite(track.delay), 0, duration, CLIP_VALUE_STEP]
1272
+ };
1273
+ if (!hasTrackSteps) {
1274
+ trackDial.duration = [trackDuration, 0, duration, CLIP_VALUE_STEP];
1275
+ trackDial.transition = normalizeStoredTransition(trackCurve, trackDuration);
1276
+ }
1277
+ const fromValue = track.from ?? (hasTrackSteps ? void 0 : track.to);
1278
+ if (hasTrackSteps && fromValue === void 0) {
1279
+ console.warn(
1280
+ `[dialkit] Timeline clip "${path}": track "${prop}" has steps but no "from" \u2014 declare its starting value.`
1281
+ );
1282
+ }
1283
+ if (fromValue !== void 0) {
1284
+ trackDial.from = scalarDial(prop, fromValue, hasTrackSteps ? track.steps[0]?.to : track.to);
1285
+ }
1286
+ if (!hasTrackSteps && track.to !== void 0) {
1287
+ trackDial.to = scalarDial(prop, track.to, fromValue);
1288
+ }
1289
+ let trackStepKeys;
1290
+ if (hasTrackSteps) {
1291
+ trackStepKeys = [];
1292
+ let previous = fromValue;
1293
+ track.steps.forEach((step, stepIndex) => {
1294
+ const stepKey = `step${stepIndex + 1}`;
1295
+ trackStepKeys.push(stepKey);
1296
+ const stepDuration = defaultStepDuration(step, trackCurve);
1297
+ const stepDial = {
1298
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
1299
+ transition: normalizeStoredTransition(step.transition ?? trackCurve, stepDuration)
1300
+ };
1301
+ if (step.to !== void 0) {
1302
+ stepDial.to = scalarDial(prop, step.to, previous);
1303
+ previous = step.to;
1304
+ }
1305
+ trackDial[stepKey] = stepDial;
1306
+ });
1307
+ }
1308
+ clipDial[prop] = trackDial;
1309
+ tracks.push({ prop, stepKeys: trackStepKeys });
1310
+ }
1311
+ }
1312
+ if (clip.from && !hasProps) {
1313
+ clipDial.from = withFromToRanges(
1314
+ clip.from,
1315
+ hasSteps ? definedValues(clip.steps[0]?.to) : clip.to
1316
+ );
1317
+ }
1318
+ if (!hasSteps && !hasProps && clip.to) {
1319
+ clipDial.to = withFromToRanges(clip.to, clip.from);
1320
+ }
1321
+ let stepKeys;
1322
+ if (hasSteps) {
1323
+ stepKeys = [];
1324
+ let running = clip.from;
1325
+ clip.steps.forEach((step, stepIndex) => {
1326
+ const stepKey = `step${stepIndex + 1}`;
1327
+ stepKeys.push(stepKey);
1328
+ const stepDuration = defaultStepDuration(step, defaultCurve);
1329
+ const stepDial = {
1330
+ duration: [stepDuration, 0, duration, CLIP_VALUE_STEP],
1331
+ transition: normalizeStoredTransition(step.transition ?? defaultCurve, stepDuration)
1332
+ };
1333
+ const stepTo = definedValues(step.to);
1334
+ if (stepTo) {
1335
+ for (const prop of Object.keys(stepTo)) {
1336
+ if (!running || !(prop in running)) {
1337
+ console.warn(
1338
+ `[dialkit] Timeline clip "${path}": property "${prop}" first animates in step ${stepIndex + 1} with no starting value \u2014 declare it in "from".`
1339
+ );
1340
+ }
1341
+ }
1342
+ stepDial.to = withFromToRanges(stepTo, running);
1343
+ }
1344
+ clipDial[stepKey] = stepDial;
1345
+ running = { ...running ?? {}, ...stepTo ?? {} };
1346
+ });
1347
+ }
1348
+ setDialPath(dialConfig, path, clipDial);
1349
+ clips.push({
1350
+ key: path,
1351
+ label: formatLabel(childKey),
1352
+ color: TIMELINE_CLIP_COLORS[index % TIMELINE_CLIP_COLORS.length],
1353
+ loop: normalizeLoopMode(clip.loop),
1354
+ group,
1355
+ stepKeys,
1356
+ tracks
1357
+ });
1358
+ });
1359
+ return { duration, dialConfig, clips };
1360
+ }
1361
+ var TRACK_RESERVED = /* @__PURE__ */ new Set(["at", "duration", "loop", "from", "to", "transition", "delay"]);
1362
+ function scalarDial(prop, value, counterpart) {
1363
+ const record = withFromToRanges(
1364
+ { [prop]: value },
1365
+ counterpart === void 0 ? void 0 : { [prop]: counterpart }
1366
+ );
1367
+ return record[prop];
1368
+ }
1369
+ var FROM_TO_RANGE_PRESETS = [
1370
+ [/^(x|y|z|tx|ty|offsetx|offsety|translatex|translatey)$/i, { min: -100, max: 100, step: 1 }],
1371
+ [/rotat|angle|skew/i, { min: -180, max: 180, step: 1 }],
1372
+ [/^scale/i, { min: 0, max: 2, step: 0.01 }],
1373
+ [/opacity|alpha/i, { min: 0, max: 1, step: 0.01 }],
1374
+ [/blur|radius|spread/i, { min: 0, max: 100, step: 1 }]
1375
+ ];
1376
+ function inferFromToRange(key, value, counterpart) {
1377
+ const lo = Math.min(value, counterpart ?? value);
1378
+ const hi = Math.max(value, counterpart ?? value);
1379
+ const preset = FROM_TO_RANGE_PRESETS.find(([pattern]) => pattern.test(key))?.[1];
1380
+ if (preset) {
1381
+ return [value, Math.min(preset.min, lo), Math.max(preset.max, hi), preset.step];
1382
+ }
1383
+ if (lo >= 0 && hi <= 1) {
1384
+ return [value, 0, 1, 0.01];
1385
+ }
1386
+ const extent = Math.max(Math.abs(lo), Math.abs(hi), 1);
1387
+ const min = lo < 0 ? -extent * 2 : 0;
1388
+ const max = Math.max(extent * 2, hi);
1389
+ return [value, min, max, inferStep(min, max)];
1390
+ }
1391
+ function withFromToRanges(config, counterpart) {
1392
+ const result = {};
1393
+ for (const [key, value] of Object.entries(config)) {
1394
+ const other = counterpart?.[key];
1395
+ if (typeof value === "number") {
1396
+ result[key] = inferFromToRange(key, value, typeof other === "number" ? other : void 0);
1397
+ } else if (isPlainObject(value) && !("type" in value)) {
1398
+ result[key] = withFromToRanges(
1399
+ value,
1400
+ isPlainObject(other) && !("type" in other) ? other : void 0
1401
+ );
1402
+ } else {
1403
+ result[key] = value;
1404
+ }
1405
+ }
1406
+ return result;
1407
+ }
1408
+ function curveStatic(transition, duration) {
1409
+ if (!transition) return { duration };
1410
+ if (transition.type === "easing") return { duration, ease: transition.ease };
1411
+ const spring = springParams(transition);
1412
+ return { duration, spring, settle: springSettleDuration(spring) };
1413
+ }
1414
+ function sampleCurve(curve, elapsed) {
1415
+ if (elapsed <= 0) return 0;
1416
+ if (curve.spring) {
1417
+ if (curve.settle !== void 0 && elapsed >= curve.settle) return 1;
1418
+ return springProgress(elapsed, curve.spring);
1419
+ }
1420
+ if (curve.ease) {
1421
+ return cubicBezierProgress(clamp(curve.duration > 0 ? elapsed / curve.duration : 1, 0, 1), curve.ease);
1422
+ }
1423
+ return curve.duration > 0 ? Math.min(1, elapsed / curve.duration) : 1;
1424
+ }
1425
+ function resolvedAtPath(resolved, path) {
1426
+ let node = resolved;
1427
+ for (const segment of path.split(".")) {
1428
+ node = isPlainObject(node) ? node[segment] : void 0;
1429
+ }
1430
+ return isPlainObject(node) ? node : {};
1431
+ }
1432
+ function computeStaticClips(parsed, flatValues) {
1433
+ const resolved = resolveDialValues(parsed.dialConfig, flatValues);
1434
+ return parsed.clips.map(
1435
+ (clip) => buildClipStatic(resolvedAtPath(resolved, clip.key), clip, parsed.duration)
1436
+ );
1437
+ }
1438
+ function computeStaticTimeline(parsed, flatValues) {
1439
+ let clips = computeStaticClips(parsed, flatValues);
1440
+ const maxEnd = clips.reduce(
1441
+ (end, clip) => Math.max(end, clip.at + clip.duration),
1442
+ parsed.duration
1443
+ );
1444
+ const duration = maxEnd > parsed.duration ? Math.ceil(maxEnd * 100 - 1e-4) / 100 : parsed.duration;
1445
+ if (duration !== parsed.duration) {
1446
+ clips = clips.map(
1447
+ (clip) => clip.loop === "repeat" ? { ...clip, end: duration } : clip
1448
+ );
1449
+ }
1450
+ return { duration, clips };
1451
+ }
1452
+ function computeClipStaticFromValues(values, clip, timelineDuration) {
1453
+ return buildClipStatic(unflattenClipValues(values, clip.key), clip, timelineDuration);
1454
+ }
1455
+ function buildClipStatic(clipResolved, clip, timelineDuration) {
1456
+ {
1457
+ const at = typeof clipResolved.at === "number" ? clipResolved.at : 0;
1458
+ const from = isPlainObject(clipResolved.from) ? clipResolved.from : void 0;
1459
+ const single = isTransitionConfig(clipResolved.transition) ? clipResolved.transition : void 0;
1460
+ const staticClip = {
1461
+ key: clip.key,
1462
+ childKey: clip.group ? clip.key.slice(clip.group.length + 1) : clip.key,
1463
+ group: clip.group,
1464
+ at,
1465
+ duration: 0,
1466
+ loop: "off",
1467
+ end: 0,
1468
+ isPhysics: false,
1469
+ from,
1470
+ tracks: [],
1471
+ explicitSteps: Boolean(clip.stepKeys?.length)
1472
+ };
1473
+ if (clip.tracks?.length) {
1474
+ const tracks = clip.tracks.map(({ prop, stepKeys }) => {
1475
+ const trackResolved = isPlainObject(clipResolved[prop]) ? clipResolved[prop] : {};
1476
+ const delay = typeof trackResolved.delay === "number" ? trackResolved.delay : 0;
1477
+ const fromValue = trackResolved.from;
1478
+ let steps;
1479
+ let trackDuration = 0;
1480
+ if (stepKeys?.length) {
1481
+ let running = fromValue;
1482
+ steps = stepKeys.map((stepKey) => {
1483
+ const stepResolved = isPlainObject(trackResolved[stepKey]) ? trackResolved[stepKey] : {};
1484
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
1485
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
1486
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
1487
+ const toValue = stepResolved.to;
1488
+ const step = {
1489
+ key: stepKey,
1490
+ offset: trackDuration,
1491
+ duration: effective.duration,
1492
+ isPhysics: effective.isPhysics,
1493
+ start: running === void 0 ? {} : { [prop]: running },
1494
+ to: toValue === void 0 ? {} : { [prop]: toValue },
1495
+ curve: curveStatic(effective.transition, effective.duration)
1496
+ };
1497
+ if (toValue !== void 0) running = toValue;
1498
+ trackDuration += effective.duration;
1499
+ return step;
1500
+ });
1501
+ } else {
1502
+ const storedDuration = typeof trackResolved.duration === "number" ? trackResolved.duration : 0;
1503
+ const raw = isTransitionConfig(trackResolved.transition) ? trackResolved.transition : void 0;
1504
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
1505
+ const toValue = trackResolved.to;
1506
+ trackDuration = effective.duration;
1507
+ steps = [
1508
+ {
1509
+ key: null,
1510
+ offset: 0,
1511
+ duration: effective.duration,
1512
+ isPhysics: effective.isPhysics,
1513
+ start: fromValue === void 0 ? {} : { [prop]: fromValue },
1514
+ to: toValue === void 0 ? {} : { [prop]: toValue },
1515
+ curve: curveStatic(effective.transition, effective.duration)
1516
+ }
1517
+ ];
1518
+ }
1519
+ return { prop, delay, duration: trackDuration, steps };
1520
+ });
1521
+ staticClip.tracks = tracks;
1522
+ staticClip.props = tracks.map((track) => track.prop);
1523
+ staticClip.duration = tracks.reduce((max, track) => Math.max(max, track.delay + track.duration), 0);
1524
+ staticClip.from = Object.fromEntries(
1525
+ tracks.map((track) => [track.prop, track.steps[0].start[track.prop]])
1526
+ );
1527
+ staticClip.to = Object.fromEntries(
1528
+ tracks.map((track) => {
1529
+ const last = track.steps[track.steps.length - 1];
1530
+ return [track.prop, last.to[track.prop] ?? last.start[track.prop]];
1531
+ })
1532
+ );
1533
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
1534
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
1535
+ return staticClip;
1536
+ }
1537
+ if (clip.stepKeys?.length) {
1538
+ let running = { ...from ?? {} };
1539
+ let offset = 0;
1540
+ const steps = clip.stepKeys.map((stepKey) => {
1541
+ const stepResolved = isPlainObject(clipResolved[stepKey]) ? clipResolved[stepKey] : {};
1542
+ const storedDuration = typeof stepResolved.duration === "number" ? stepResolved.duration : 0;
1543
+ const raw = isTransitionConfig(stepResolved.transition) ? stepResolved.transition : void 0;
1544
+ const effective = raw ? resolveClipTransition(raw, storedDuration) : { transition: void 0, duration: storedDuration, isPhysics: false };
1545
+ const to = isPlainObject(stepResolved.to) ? stepResolved.to : {};
1546
+ const step = {
1547
+ key: stepKey,
1548
+ offset,
1549
+ duration: effective.duration,
1550
+ isPhysics: effective.isPhysics,
1551
+ start: running,
1552
+ to,
1553
+ curve: curveStatic(effective.transition, effective.duration)
1554
+ };
1555
+ running = { ...running, ...to };
1556
+ offset += effective.duration;
1557
+ return step;
1558
+ });
1559
+ staticClip.tracks = [{ delay: 0, duration: offset, steps }];
1560
+ staticClip.duration = offset;
1561
+ staticClip.to = running;
1562
+ } else {
1563
+ const storedDuration = typeof clipResolved.duration === "number" ? clipResolved.duration : 0;
1564
+ const to = isPlainObject(clipResolved.to) ? clipResolved.to : void 0;
1565
+ if (single) {
1566
+ const effective = resolveClipTransition(single, storedDuration);
1567
+ staticClip.duration = effective.duration;
1568
+ staticClip.isPhysics = effective.isPhysics;
1569
+ staticClip.transition = effective.transition;
1570
+ staticClip.css = transitionToCss(effective.transition);
1571
+ staticClip.to = to;
1572
+ if (from && to) {
1573
+ staticClip.tracks = [
1574
+ {
1575
+ delay: 0,
1576
+ duration: effective.duration,
1577
+ steps: [
1578
+ {
1579
+ key: null,
1580
+ offset: 0,
1581
+ duration: effective.duration,
1582
+ isPhysics: effective.isPhysics,
1583
+ start: from,
1584
+ to,
1585
+ curve: curveStatic(effective.transition, effective.duration)
1586
+ }
1587
+ ]
1588
+ }
1589
+ ];
1590
+ }
1591
+ } else {
1592
+ staticClip.duration = storedDuration;
1593
+ staticClip.to = to;
1594
+ if (from && to) {
1595
+ const base = resolveClipTransition(DEFAULT_CLIP_TRANSITION, storedDuration);
1596
+ staticClip.duration = base.duration;
1597
+ staticClip.tracks = [
1598
+ {
1599
+ delay: 0,
1600
+ duration: base.duration,
1601
+ steps: [
1602
+ {
1603
+ key: null,
1604
+ offset: 0,
1605
+ duration: base.duration,
1606
+ isPhysics: false,
1607
+ start: from,
1608
+ to,
1609
+ curve: curveStatic(base.transition, base.duration)
1610
+ }
1611
+ ]
1612
+ }
1613
+ ];
1614
+ }
1615
+ }
1616
+ }
1617
+ if (staticClip.tracks.length) {
1618
+ const props = new Set(Object.keys(from ?? {}));
1619
+ for (const track of staticClip.tracks) {
1620
+ for (const step of track.steps) {
1621
+ for (const prop of Object.keys(step.to)) props.add(prop);
1622
+ }
1623
+ }
1624
+ staticClip.props = Array.from(props);
1625
+ }
1626
+ staticClip.loop = staticClip.duration > 0 ? clip.loop : "off";
1627
+ staticClip.end = staticClip.loop === "off" ? staticClip.at + staticClip.duration : timelineDuration;
1628
+ return staticClip;
1629
+ }
1630
+ }
1631
+ function stepAtPosition(steps, pos) {
1632
+ for (const step of steps) {
1633
+ if (pos < step.offset + step.duration) return step;
1634
+ }
1635
+ return steps[steps.length - 1];
1636
+ }
1637
+ function evalPropAtPos(steps, prop, pos) {
1638
+ const step = stepAtPosition(steps, pos);
1639
+ const within = Math.max(0, pos - step.offset);
1640
+ if (prop in step.to) {
1641
+ const eased = sampleCurve(step.curve, within);
1642
+ return interpolateResolved(step.start[prop], step.to[prop], eased);
1643
+ }
1644
+ return step.start[prop];
1645
+ }
1646
+ function computeClipState(clip, time, cycleTime = time) {
1647
+ const total = clip.duration;
1648
+ const looping = clip.loop === "repeat" && total > 0;
1649
+ const started = time >= clip.at || looping && cycleTime > time;
1650
+ const done = time >= clip.end;
1651
+ const elapsed = time - clip.at;
1652
+ const phaseElapsed = looping ? cycleTime - clip.at : elapsed;
1653
+ const fold = (e) => looping ? e % total : e;
1654
+ const basePos = started ? fold(Math.max(0, phaseElapsed)) : 0;
1655
+ const progress = total > 0 ? clamp(basePos / total, 0, 1) : started ? 1 : 0;
1656
+ let current;
1657
+ let stepIndex = 0;
1658
+ if (clip.tracks.length && clip.props?.length) {
1659
+ current = {};
1660
+ for (const track of clip.tracks) {
1661
+ const props = track.prop !== void 0 ? [track.prop] : clip.props;
1662
+ for (const prop of props) {
1663
+ const startValue = track.steps[0]?.start[prop];
1664
+ if (!started) {
1665
+ if (startValue !== void 0) current[prop] = startValue;
1666
+ continue;
1667
+ }
1668
+ const phase = phaseElapsed - track.delay;
1669
+ if (phase <= 0) {
1670
+ if (startValue !== void 0) current[prop] = startValue;
1671
+ continue;
1672
+ }
1673
+ const pos = looping && track.duration > 0 ? phase % track.duration : phase;
1674
+ const value = evalPropAtPos(track.steps, prop, pos);
1675
+ if (value !== void 0) current[prop] = value;
1676
+ }
1677
+ }
1678
+ const shared = clip.tracks[0];
1679
+ if (started && clip.explicitSteps && shared.prop === void 0) {
1680
+ stepIndex = shared.steps.indexOf(stepAtPosition(shared.steps, basePos));
1681
+ }
1682
+ }
1683
+ return {
1684
+ at: clip.at,
1685
+ duration: clip.duration,
1686
+ loop: clip.loop,
1687
+ started,
1688
+ active: started && !done,
1689
+ done,
1690
+ progress,
1691
+ step: clip.explicitSteps ? stepIndex : void 0,
1692
+ from: clip.from,
1693
+ to: clip.to,
1694
+ animate: started ? clip.to : clip.from,
1695
+ transition: clip.transition,
1696
+ css: clip.css,
1697
+ current
1698
+ };
1699
+ }
1700
+ function interpolateResolved(from, to, p) {
1701
+ if (typeof from === "number" && typeof to === "number") {
1702
+ return from + (to - from) * p;
1703
+ }
1704
+ if (typeof from === "string" && typeof to === "string") {
1705
+ const mixed = mixHexColors(from, to, p);
1706
+ if (mixed) return mixed;
1707
+ }
1708
+ if (isPlainObject(from) && isPlainObject(to)) {
1709
+ const result = {};
1710
+ for (const key of Object.keys(from)) {
1711
+ result[key] = key in to ? interpolateResolved(from[key], to[key], p) : from[key];
1712
+ }
1713
+ for (const key of Object.keys(to)) {
1714
+ if (!(key in from)) result[key] = to[key];
1715
+ }
1716
+ return result;
1717
+ }
1718
+ return p < 0.5 ? from : to;
1719
+ }
1720
+ function parseHex(hex) {
1721
+ if (!isHexColor(hex)) return null;
1722
+ let h22 = hex.slice(1);
1723
+ if (h22.length === 3) h22 = h22.split("").map((c) => c + c).join("");
1724
+ return [
1725
+ parseInt(h22.slice(0, 2), 16),
1726
+ parseInt(h22.slice(2, 4), 16),
1727
+ parseInt(h22.slice(4, 6), 16),
1728
+ h22.length === 8 ? parseInt(h22.slice(6, 8), 16) : 255
1729
+ ];
1730
+ }
1731
+ function mixHexColors(a, b, p) {
1732
+ const ca = parseHex(a);
1733
+ const cb = parseHex(b);
1734
+ if (!ca || !cb) return null;
1735
+ const t = clamp(p, 0, 1);
1736
+ const mixed = ca.map((v, i) => Math.round(v + (cb[i] - v) * t));
1737
+ const hex = (n) => n.toString(16).padStart(2, "0");
1738
+ const rgb = `#${hex(mixed[0])}${hex(mixed[1])}${hex(mixed[2])}`;
1739
+ return mixed[3] === 255 ? rgb : `${rgb}${hex(mixed[3])}`;
1740
+ }
1741
+ function transitionToCss(transition) {
1742
+ if (!transition) return void 0;
1743
+ if (transition.type === "easing") {
1744
+ return {
1745
+ transitionDuration: `${round2(transition.duration)}s`,
1746
+ transitionTimingFunction: `cubic-bezier(${transition.ease.map((v) => round2(v)).join(", ")})`
1747
+ };
1748
+ }
1749
+ const params = springParams(transition);
1750
+ const dampingRatio = params.damping / (2 * Math.sqrt(params.stiffness * params.mass));
1751
+ const duration = transition.visualDuration ?? springSettleDuration(params);
1752
+ const bounce = transition.bounce ?? Math.max(0, round2(1 - dampingRatio));
1753
+ return {
1754
+ transitionDuration: `${round2(duration)}s`,
1755
+ transitionTimingFunction: bounce > 0.05 ? `cubic-bezier(0.34, ${round2(1.2 + bounce)}, 0.64, 1)` : "cubic-bezier(0.25, 0.6, 0.35, 1)"
1756
+ };
1757
+ }
1758
+ function timelinePopoverDisplayValues(values, clipKey, stepKeys, stepKey) {
1759
+ const display = { ...values };
1760
+ const swap = (path, duration) => {
1761
+ const raw = display[path];
1762
+ if (isTransitionConfig(raw)) display[path] = resolveClipTransition(raw, duration).transition;
1763
+ };
1764
+ if (stepKey) {
1765
+ swap(`${clipKey}.${stepKey}.transition`, numberValue(values[`${clipKey}.${stepKey}.duration`]));
1766
+ return display;
1767
+ }
1768
+ const cycle = stepKeys?.length ? stepKeys.reduce((sum, sk) => sum + numberValue(values[`${clipKey}.${sk}.duration`]), 0) : numberValue(values[`${clipKey}.duration`]);
1769
+ swap(`${clipKey}.transition`, cycle);
1770
+ return display;
1771
+ }
1772
+ function numberValue(value) {
1773
+ return typeof value === "number" ? value : 0;
1774
+ }
1775
+ function unflattenClipValues(values, clipKey) {
1776
+ const prefix = `${clipKey}.`;
1777
+ const result = {};
1778
+ const entries = Object.entries(values).filter(([path]) => path.startsWith(prefix)).map(([path, value]) => ({ segments: path.slice(prefix.length).split("."), value })).sort((a, b) => a.segments.length - b.segments.length);
1779
+ for (const { segments, value } of entries) {
1780
+ let node = result;
1781
+ for (let i = 0; i < segments.length - 1; i++) {
1782
+ const existing = node[segments[i]];
1783
+ node = isPlainObject(existing) ? existing : node[segments[i]] = {};
1784
+ }
1785
+ node[segments[segments.length - 1]] = cloneTimelineValue(value);
1786
+ }
1787
+ return result;
1788
+ }
1789
+ function cloneTimelineValue(value) {
1790
+ if (Array.isArray(value)) return value.map(cloneTimelineValue);
1791
+ if (!isPlainObject(value)) return value;
1792
+ return Object.fromEntries(
1793
+ Object.entries(value).map(([key, nested]) => [key, cloneTimelineValue(nested)])
1794
+ );
1795
+ }
1796
+ function clampTrackDelay(delay, at, trackDuration, timelineDuration) {
1797
+ return clamp(round2(delay), 0, Math.max(0, round2(timelineDuration - at - trackDuration)));
1798
+ }
1799
+ function clampClipMove(at, duration, timelineDuration) {
1800
+ return clamp(round2(at), 0, Math.max(0, timelineDuration - duration));
1801
+ }
1802
+ function clampClipResizeEnd(duration, at, timelineDuration) {
1803
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, timelineDuration - at);
1804
+ }
1805
+ function clampClipResizeStart(newAt, at, duration) {
1806
+ const clampedAt = clamp(round2(newAt), 0, at + duration - TIMELINE_MIN_CLIP_DURATION);
1807
+ return { at: clampedAt, duration: round2(at + duration - clampedAt) };
1808
+ }
1809
+ function clampStepResize(duration, at, otherStepsTotal, timelineDuration) {
1810
+ const max = Math.max(TIMELINE_MIN_CLIP_DURATION, timelineDuration - at - otherStepsTotal);
1811
+ return clamp(round2(duration), TIMELINE_MIN_CLIP_DURATION, max);
1812
+ }
1813
+ function normalizeTimelineValuesForCopy(values, clips) {
1814
+ const normalized = { ...values };
1815
+ for (const path of Object.keys(normalized)) {
1816
+ if (path.endsWith(".__mode")) delete normalized[path];
1817
+ }
1818
+ const normalizeTransitionAt = (transitionPath, durationPath) => {
1819
+ const raw = normalized[transitionPath];
1820
+ if (!isTransitionConfig(raw)) return;
1821
+ if (isPhysicsSpring(raw)) {
1822
+ normalized[durationPath] = transitionDefaultDuration(raw);
1823
+ }
1824
+ normalized[transitionPath] = normalizeStoredTransition(
1825
+ raw,
1826
+ numberValue(normalized[durationPath])
1827
+ );
1828
+ };
1829
+ for (const clip of clips) {
1830
+ for (const stepKey of clip.stepKeys ?? []) {
1831
+ normalizeTransitionAt(
1832
+ `${clip.key}.${stepKey}.transition`,
1833
+ `${clip.key}.${stepKey}.duration`
1834
+ );
1835
+ }
1836
+ normalizeTransitionAt(`${clip.key}.transition`, `${clip.key}.duration`);
1837
+ for (const track of clip.tracks ?? []) {
1838
+ const trackKey = `${clip.key}.${track.prop}`;
1839
+ for (const stepKey of track.stepKeys ?? []) {
1840
+ normalizeTransitionAt(
1841
+ `${trackKey}.${stepKey}.transition`,
1842
+ `${trackKey}.${stepKey}.duration`
1843
+ );
1844
+ }
1845
+ normalizeTransitionAt(`${trackKey}.transition`, `${trackKey}.duration`);
1846
+ if (normalized[`${trackKey}.delay`] === 0) {
1847
+ delete normalized[`${trackKey}.delay`];
1848
+ }
1849
+ }
1850
+ delete normalized[`${clip.key}.loop`];
1851
+ }
1852
+ return normalized;
1853
+ }
1854
+ function formatClock(time, tenths = false) {
1855
+ const safe = Math.max(0, time);
1856
+ const minutes = Math.floor(safe / 60);
1857
+ const seconds = safe - minutes * 60;
1858
+ const secondsText = tenths ? seconds.toFixed(1).padStart(4, "0") : String(Math.floor(seconds)).padStart(2, "0");
1859
+ return `${String(minutes).padStart(2, "0")}:${secondsText}`;
1860
+ }
1861
+ function formatSeconds(value) {
1862
+ return `${round2(value)}s`;
1863
+ }
1864
+ function formatStepLabel(stepKey) {
1865
+ const match = /^step(\d+)$/.exec(stepKey);
1866
+ return match ? `Step ${match[1]}` : formatLabel(stepKey);
1867
+ }
1868
+
1869
+ // src/timeline/adapter.ts
1870
+ function resolveTimelineLoop(loop) {
1871
+ if (typeof loop === "object" && loop !== null) {
1872
+ return {
1873
+ enabled: true,
1874
+ start: Number.isFinite(loop.from) ? Math.max(0, loop.from) : 0
1875
+ };
1876
+ }
1877
+ return { enabled: Boolean(loop), start: 0 };
1878
+ }
1879
+ function buildTimelineMeta(id, name, duration, parsed, loop) {
1880
+ const resolvedLoop = resolveTimelineLoop(loop);
1881
+ return {
1882
+ id,
1883
+ name,
1884
+ duration,
1885
+ loop: resolvedLoop.enabled,
1886
+ loopStart: resolvedLoop.start,
1887
+ clips: parsed.clips
1888
+ };
1889
+ }
1890
+ function buildTimelineValues(staticClips, transport, timelineDuration, loopStart, actions) {
1891
+ var _a;
1892
+ const result = {
1893
+ time: transport.time,
1894
+ playing: transport.playing,
1895
+ duration: timelineDuration,
1896
+ ...actions
1897
+ };
1898
+ const span = loopSpan(transport.duration, loopStart);
1899
+ const cycleTime = (span > 0 ? transport.wraps * span : 0) + transport.time;
1900
+ for (const clip of staticClips) {
1901
+ const state = computeClipState(clip, transport.time, cycleTime);
1902
+ if (clip.group) {
1903
+ const bucket = result[_a = clip.group] ?? (result[_a] = {});
1904
+ bucket[clip.childKey] = state;
1905
+ } else {
1906
+ result[clip.key] = state;
1907
+ }
1908
+ }
1909
+ return result;
1910
+ }
1911
+
1912
+ // src/store/TimelineUiStore.ts
1913
+ var TimelineUiStoreClass = class {
1914
+ constructor() {
1915
+ this.visible = true;
1916
+ this.initialized = false;
1917
+ this.controllers = /* @__PURE__ */ new Map();
1918
+ this.listeners = /* @__PURE__ */ new Set();
1919
+ }
1920
+ getVisible() {
1921
+ for (const controller of this.controllers.values()) {
1922
+ if (controller.visible !== void 0) return controller.visible;
1923
+ }
1924
+ return this.visible;
1925
+ }
1926
+ registerController(id, controller) {
1927
+ const previous = this.getVisible();
1928
+ if (!this.initialized) {
1929
+ this.visible = controller.defaultVisible;
1930
+ this.initialized = true;
1931
+ }
1932
+ this.controllers.set(id, controller);
1933
+ if (previous !== this.getVisible()) this.notify();
1934
+ return () => {
1935
+ const before = this.getVisible();
1936
+ this.controllers.delete(id);
1937
+ if (this.controllers.size === 0) this.initialized = false;
1938
+ if (before !== this.getVisible()) this.notify();
1939
+ };
1940
+ }
1941
+ updateController(id, controller) {
1942
+ if (!this.controllers.has(id)) return;
1943
+ const previous = this.getVisible();
1944
+ this.controllers.set(id, controller);
1945
+ if (previous !== this.getVisible()) this.notify();
1946
+ }
1947
+ requestVisible(visible) {
1948
+ const current = this.getVisible();
1949
+ if (current === visible) return;
1950
+ const controlled = Array.from(this.controllers.values()).filter(
1951
+ (controller) => controller.visible !== void 0
1952
+ );
1953
+ if (controlled.length > 0) {
1954
+ controlled.forEach((controller) => controller.onVisibilityChange?.(visible));
1955
+ return;
1956
+ }
1957
+ this.visible = visible;
1958
+ this.controllers.forEach((controller) => controller.onVisibilityChange?.(visible));
1959
+ this.notify();
1960
+ }
1961
+ toggle() {
1962
+ this.requestVisible(!this.getVisible());
1963
+ }
1964
+ subscribe(listener) {
1965
+ this.listeners.add(listener);
1966
+ return () => this.listeners.delete(listener);
1967
+ }
1968
+ notify() {
1969
+ this.listeners.forEach((listener) => listener());
1970
+ }
1971
+ };
1972
+ var TimelineUiStore = /* @__PURE__ */ new TimelineUiStoreClass();
1973
+
1974
+ // src/copy-instruction.ts
1975
+ function buildCopyInstruction(hookName, panelName, values) {
1976
+ const jsonStr = JSON.stringify(values, null, 2);
1977
+ if (hookName === "useDialTimeline" || hookName === "createDialTimeline") {
1978
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1979
+
1980
+ \`\`\`json
1981
+ ${jsonStr}
1982
+ \`\`\`
1983
+
1984
+ Apply these values as the new defaults in the ${hookName} call. Keep the existing \`clip.current\` bindings while this timeline is being authored; do not convert the animation or remove DialKit yet.
1985
+
1986
+ Add this comment immediately above the ${hookName} call as a production handoff note:
1987
+
1988
+ \`\`\`tsx
1989
+ // TODO(production): DialKit's clip.current values are the scrubbable authoring preview.
1990
+ // Replace them with equivalent real Motion animations using the tuned timeline
1991
+ // timings and transitions, then remove ${hookName} and <DialTimeline />.
1992
+ \`\`\``;
1993
+ }
1994
+ return `Update the ${hookName} configuration for "${panelName}" with these values:
1995
+
1996
+ \`\`\`json
1997
+ ${jsonStr}
1998
+ \`\`\`
1999
+
2000
+ Apply these values as the new defaults in the ${hookName} call.`;
2001
+ }
2002
+
2003
+ // src/env.ts
2004
+ 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;
2005
+
2006
+ // src/vue/useDialTimeline.ts
2007
+ var timelineInstance = 0;
2008
+ function useDialTimeline(name, config, options) {
2009
+ const hasStableId = options?.id !== void 0;
2010
+ const panelId = options?.id ?? `${name}-${++timelineInstance}`;
2011
+ const serializedConfig = computed2(() => JSON.stringify(config));
2012
+ const serializedPersist = computed2(() => JSON.stringify(options?.persist));
2013
+ const serializedLoop = computed2(() => JSON.stringify(options?.loop));
2014
+ const parsed = computed2(() => {
2015
+ serializedConfig.value;
2016
+ return parseTimelineConfig(config);
2017
+ });
2018
+ const flatValues = shallowRef2(DialStore.getValues(panelId));
2019
+ const transport = shallowRef2(TimelineStore.getTransport(panelId));
2020
+ const staticTimeline = computed2(() => computeStaticTimeline(parsed.value, flatValues.value));
2021
+ const meta = computed2(() => {
2022
+ serializedLoop.value;
2023
+ return buildTimelineMeta(
2024
+ panelId,
2025
+ name,
2026
+ staticTimeline.value.duration,
2027
+ parsed.value,
2028
+ options?.loop
2029
+ );
2030
+ });
2031
+ let mounted = false;
2032
+ let unsubscribeValues;
2033
+ let unsubscribeTransport;
2034
+ const play = () => TimelineStore.play(panelId);
2035
+ const pause = () => TimelineStore.pause(panelId);
2036
+ const replay = () => TimelineStore.replay(panelId);
2037
+ const seek = (time) => TimelineStore.seek(panelId, time);
2038
+ watch2([serializedConfig, serializedPersist], () => {
2039
+ if (!mounted) return;
2040
+ DialStore.updatePanel(panelId, name, parsed.value.dialConfig, void 0, {
2041
+ retainOnUnmount: hasStableId,
2042
+ persist: options?.persist,
2043
+ kind: "timeline"
2044
+ });
2045
+ flatValues.value = DialStore.getValues(panelId);
2046
+ });
2047
+ watch2(meta, (nextMeta) => {
2048
+ if (mounted) TimelineStore.update(nextMeta);
2049
+ });
2050
+ onMounted2(() => {
2051
+ unsubscribeValues = DialStore.subscribe(panelId, () => {
2052
+ flatValues.value = DialStore.getValues(panelId);
2053
+ });
2054
+ unsubscribeTransport = TimelineStore.subscribe(panelId, () => {
2055
+ transport.value = TimelineStore.getTransport(panelId);
2056
+ });
2057
+ DialStore.registerPanel(panelId, name, parsed.value.dialConfig, void 0, {
2058
+ retainOnUnmount: hasStableId,
2059
+ persist: options?.persist,
2060
+ kind: "timeline"
2061
+ });
2062
+ flatValues.value = DialStore.getValues(panelId);
2063
+ TimelineStore.register(meta.value, { autoplay: options?.autoplay ?? true });
2064
+ transport.value = TimelineStore.getTransport(panelId);
2065
+ mounted = true;
2066
+ });
2067
+ onUnmounted2(() => {
2068
+ mounted = false;
2069
+ unsubscribeValues?.();
2070
+ unsubscribeTransport?.();
2071
+ TimelineStore.unregister(panelId);
2072
+ DialStore.unregisterPanel(panelId);
2073
+ });
2074
+ return computed2(() => {
2075
+ const currentStatic = staticTimeline.value;
2076
+ return buildTimelineValues(
2077
+ currentStatic.clips,
2078
+ transport.value,
2079
+ currentStatic.duration,
2080
+ resolveTimelineLoop(options?.loop).start,
2081
+ { play, pause, replay, seek }
2082
+ );
2083
+ });
2084
+ }
2085
+
2086
+ // src/vue/directives/dialkit.ts
2087
+ import {
2088
+ createApp,
2089
+ defineComponent as defineComponent17,
2090
+ h as h17,
2091
+ shallowRef as shallowRef3
2092
+ } from "vue";
2093
+
2094
+ // src/vue/components/DialRoot.ts
2095
+ import { computed as computed6, defineComponent as defineComponent16, h as h16, nextTick as nextTick3, onMounted as onMounted12, onUnmounted as onUnmounted11, ref as ref14, Teleport as Teleport3 } from "vue";
2096
+
2097
+ // src/vue/components/Folder.ts
2098
+ import { defineComponent, h, onMounted as onMounted3, onUnmounted as onUnmounted3, ref as ref2 } from "vue";
2099
+ import { AnimatePresence, motion } from "motion-v";
2100
+
2101
+ // src/icons.ts
2102
+ var ICON_CHEVRON = "M6 9.5L12 15.5L18 9.5";
2103
+ var ICON_CHECK = "M5 12.75L10 19L19 5";
2104
+ var ICON_PAUSE = [
2105
+ "M6.75 3C5.23122 3 4 4.23122 4 5.75V18.25C4 19.7688 5.23122 21 6.75 21H7.25C8.76878 21 10 19.7688 10 18.25V5.75C10 4.23122 8.76878 3 7.25 3H6.75Z",
2106
+ "M16.75 3C15.2312 3 14 4.23122 14 5.75V18.25C14 19.7688 15.2312 21 16.75 21H17.25C18.7688 21 20 19.7688 20 18.25V5.75C20 4.23122 18.7688 3 17.25 3H16.75Z"
2107
+ ];
2108
+ var ICON_PLAY = "M9.24394 2.36758C7.41419 1.18362 5 2.49701 5 4.67639V19.3238C5 21.5032 7.41419 22.8166 9.24394 21.6326L20.5624 14.3089C22.2371 13.2253 22.2372 10.775 20.5624 9.69129L9.24394 2.36758Z";
2109
+ var ICON_TIMELINE = [
2110
+ "M18.868 10C20.8517 10.0003 22.2886 11.8914 21.7577 13.8027L20.369 18.8027C20.0083 20.1012 18.826 20.9999 17.4784 21H6.51941C5.17179 21 3.98948 20.1012 3.62878 18.8027L2.24011 13.8027C1.7092 11.8913 3.14603 10.0003 5.12976 10H18.868Z",
2111
+ "M18.9989 6.5C19.5511 6.50007 19.9989 6.94776 19.9989 7.5C19.9989 8.05224 19.5511 8.49993 18.9989 8.5H4.9989C4.44661 8.5 3.9989 8.05228 3.9989 7.5C3.9989 6.94772 4.44661 6.5 4.9989 6.5H18.9989Z",
2112
+ "M16.9989 3C17.5511 3.00007 17.9989 3.44776 17.9989 4C17.9989 4.55224 17.5511 4.99993 16.9989 5H6.9989C6.44661 5 5.9989 4.55228 5.9989 4C5.9989 3.44772 6.44661 3 6.9989 3H16.9989Z"
2113
+ ];
2114
+ var ICON_CLIPBOARD = {
2115
+ board: "M8 6C8 4.34315 9.34315 3 11 3H13C14.6569 3 16 4.34315 16 6V7H8V6Z",
2116
+ 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",
2117
+ 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"
2118
+ };
2119
+ var ICON_ADD_PRESET = [
2120
+ "M4 6H20",
2121
+ "M4 12H10",
2122
+ "M15 15L21 15",
2123
+ "M18 12V18",
594
2124
  "M4 18H10"
595
2125
  ];
596
2126
  var ICON_TRASH = [
@@ -610,8 +2140,6 @@ var ICON_PANEL = {
610
2140
  };
611
2141
 
612
2142
  // src/vue/components/Folder.ts
613
- import { defineComponent, h, onMounted as onMounted2, onUnmounted as onUnmounted2, ref as ref2 } from "vue";
614
- import { AnimatePresence, motion } from "motion-v";
615
2143
  var Folder = defineComponent({
616
2144
  name: "DialKitFolder",
617
2145
  props: {
@@ -623,6 +2151,10 @@ var Folder = defineComponent({
623
2151
  type: null,
624
2152
  required: false,
625
2153
  default: null
2154
+ },
2155
+ panelHeightOffset: {
2156
+ type: Number,
2157
+ default: 10
626
2158
  }
627
2159
  },
628
2160
  emits: ["openChange"],
@@ -639,7 +2171,7 @@ var Folder = defineComponent({
639
2171
  };
640
2172
  window.addEventListener("resize", resizeHandler);
641
2173
  }
642
- onUnmounted2(() => {
2174
+ onUnmounted3(() => {
643
2175
  if (resizeHandler) window.removeEventListener("resize", resizeHandler);
644
2176
  });
645
2177
  const handleToggle = () => {
@@ -650,7 +2182,7 @@ var Folder = defineComponent({
650
2182
  emit("openChange", next);
651
2183
  };
652
2184
  let ro = null;
653
- onMounted2(() => {
2185
+ onMounted3(() => {
654
2186
  if (!props.isRoot || typeof ResizeObserver === "undefined") return;
655
2187
  const el = contentRef.value;
656
2188
  if (!el) return;
@@ -667,7 +2199,7 @@ var Folder = defineComponent({
667
2199
  contentHeight.value = el.offsetHeight;
668
2200
  }
669
2201
  });
670
- onUnmounted2(() => {
2202
+ onUnmounted3(() => {
671
2203
  ro?.disconnect();
672
2204
  });
673
2205
  const renderHeader = () => h("div", {
@@ -722,7 +2254,8 @@ var Folder = defineComponent({
722
2254
  };
723
2255
  const folderContent = () => h("div", {
724
2256
  ref: props.isRoot ? contentRef : void 0,
725
- class: `dialkit-folder ${props.isRoot ? "dialkit-folder-root" : ""}`
2257
+ class: `dialkit-folder ${props.isRoot ? "dialkit-folder-root" : ""}`,
2258
+ "data-open": String(isOpen.value)
726
2259
  }, [
727
2260
  renderHeader(),
728
2261
  renderContent()
@@ -734,7 +2267,7 @@ var Folder = defineComponent({
734
2267
  }
735
2268
  const panelStyle = isOpen.value ? {
736
2269
  width: 280,
737
- height: contentHeight.value !== void 0 ? Math.min(contentHeight.value + 10, windowHeight.value - 32) : "auto",
2270
+ height: contentHeight.value !== void 0 ? Math.min(contentHeight.value + props.panelHeightOffset, windowHeight.value - 32) : "auto",
738
2271
  borderRadius: 14,
739
2272
  boxShadow: "var(--dial-shadow)",
740
2273
  cursor: void 0,
@@ -761,8 +2294,12 @@ var Folder = defineComponent({
761
2294
  }
762
2295
  });
763
2296
 
2297
+ // src/vue/components/Panel.ts
2298
+ import { Fragment, defineComponent as defineComponent14, h as h14, onMounted as onMounted10, onUnmounted as onUnmounted9, ref as ref12 } from "vue";
2299
+ import { AnimatePresence as AnimatePresence4, motion as motion4 } from "motion-v";
2300
+
764
2301
  // src/vue/components/Slider.ts
765
- import { defineComponent as defineComponent2, h as h2, computed as computed2, nextTick, onMounted as onMounted3, onUnmounted as onUnmounted3, ref as ref3, watch as watch2 } from "vue";
2302
+ import { defineComponent as defineComponent2, h as h2, computed as computed3, nextTick, onMounted as onMounted4, onUnmounted as onUnmounted4, ref as ref3, watch as watch3 } from "vue";
766
2303
  import { animate, motionValue } from "motion-v";
767
2304
 
768
2305
  // src/shortcut-utils.ts
@@ -869,9 +2406,9 @@ var Slider = defineComponent2({
869
2406
  },
870
2407
  emits: ["change"],
871
2408
  setup(props, { emit }) {
872
- const min = computed2(() => props.min ?? 0);
873
- const max = computed2(() => props.max ?? 1);
874
- const step = computed2(() => props.step ?? 0.01);
2409
+ const min = computed3(() => props.min ?? 0);
2410
+ const max = computed3(() => props.max ?? 1);
2411
+ const step = computed3(() => props.step ?? 0.01);
875
2412
  const wrapperRef = ref3(null);
876
2413
  const trackRef = ref3(null);
877
2414
  const fillRef = ref3(null);
@@ -891,9 +2428,9 @@ var Slider = defineComponent2({
891
2428
  const handleOpacityMv = motionValue(0);
892
2429
  const handleScaleXMv = motionValue(0.25);
893
2430
  const handleScaleYMv = motionValue(1);
894
- const percentage = computed2(() => (props.value - min.value) / (max.value - min.value) * 100);
895
- const isActive = computed2(() => isInteracting.value || isHovered.value);
896
- const displayValue = computed2(() => props.value.toFixed(decimalsForStep(step.value)));
2431
+ const percentage = computed3(() => (props.value - min.value) / (max.value - min.value) * 100);
2432
+ const isActive = computed3(() => isInteracting.value || isHovered.value);
2433
+ const displayValue = computed3(() => props.value.toFixed(decimalsForStep(step.value)));
897
2434
  let pointerDownPos = null;
898
2435
  let isClickFlag = true;
899
2436
  let wrapperRect = null;
@@ -1082,15 +2619,15 @@ var Slider = defineComponent2({
1082
2619
  isValueHovered.value = false;
1083
2620
  }
1084
2621
  };
1085
- watch2(() => props.value, () => {
2622
+ watch3(() => props.value, () => {
1086
2623
  if (!isInteracting.value && !snapAnim) {
1087
2624
  fillPercent.jump(percentage.value);
1088
2625
  }
1089
2626
  });
1090
- watch2([isInteracting, isHovered, isDragging, () => props.value], () => {
2627
+ watch3([isInteracting, isHovered, isDragging, () => props.value], () => {
1091
2628
  animateHandleState();
1092
2629
  });
1093
- watch2([isValueHovered, showInput, isValueEditable], () => {
2630
+ watch3([isValueHovered, showInput, isValueEditable], () => {
1094
2631
  if (hoverTimeout) {
1095
2632
  clearTimeout(hoverTimeout);
1096
2633
  hoverTimeout = null;
@@ -1104,14 +2641,14 @@ var Slider = defineComponent2({
1104
2641
  isValueEditable.value = false;
1105
2642
  }
1106
2643
  });
1107
- watch2(showInput, async (visible) => {
2644
+ watch3(showInput, async (visible) => {
1108
2645
  if (!visible) return;
1109
2646
  await nextTick();
1110
2647
  inputRef.value?.focus();
1111
2648
  inputRef.value?.select();
1112
2649
  });
1113
- const discreteSteps = computed2(() => (max.value - min.value) / step.value);
1114
- const hashMarks = computed2(() => {
2650
+ const discreteSteps = computed3(() => (max.value - min.value) / step.value);
2651
+ const hashMarks = computed3(() => {
1115
2652
  const marks = [];
1116
2653
  if (discreteSteps.value <= 10) {
1117
2654
  const count = Math.max(0, Math.floor(discreteSteps.value) - 1);
@@ -1132,7 +2669,7 @@ var Slider = defineComponent2({
1132
2669
  let unsubHandleOpacity = null;
1133
2670
  let unsubHandleScaleX = null;
1134
2671
  let unsubHandleScaleY = null;
1135
- onMounted3(() => {
2672
+ onMounted4(() => {
1136
2673
  unsubFill = fillPercent.on("change", applyFillStyles);
1137
2674
  unsubRubber = rubberStretchPx.on("change", applyRubberStyles);
1138
2675
  unsubHandleOpacity = handleOpacityMv.on("change", applyHandleVisualStyles);
@@ -1143,7 +2680,7 @@ var Slider = defineComponent2({
1143
2680
  applyHandleVisualStyles();
1144
2681
  animateHandleState();
1145
2682
  });
1146
- onUnmounted3(() => {
2683
+ onUnmounted4(() => {
1147
2684
  if (hoverTimeout) clearTimeout(hoverTimeout);
1148
2685
  snapAnim?.stop();
1149
2686
  rubberAnim?.stop();
@@ -1232,7 +2769,7 @@ var Slider = defineComponent2({
1232
2769
  import { defineComponent as defineComponent4, h as h4 } from "vue";
1233
2770
 
1234
2771
  // src/vue/components/SegmentedControl.ts
1235
- import { defineComponent as defineComponent3, h as h3, nextTick as nextTick2, onMounted as onMounted4, onUnmounted as onUnmounted4, ref as ref4, watch as watch3 } from "vue";
2772
+ import { defineComponent as defineComponent3, h as h3, nextTick as nextTick2, onMounted as onMounted5, onUnmounted as onUnmounted5, ref as ref4, watch as watch4 } from "vue";
1236
2773
  import { animate as animate2 } from "motion";
1237
2774
  var SegmentedControl = defineComponent3({
1238
2775
  name: "DialKitSegmentedControl",
@@ -1303,7 +2840,7 @@ var SegmentedControl = defineComponent3({
1303
2840
  );
1304
2841
  };
1305
2842
  let ro;
1306
- onMounted4(() => {
2843
+ onMounted5(() => {
1307
2844
  nextTick2(() => {
1308
2845
  updatePill(false);
1309
2846
  hasAnimated = true;
@@ -1313,11 +2850,11 @@ var SegmentedControl = defineComponent3({
1313
2850
  ro.observe(containerRef.value);
1314
2851
  }
1315
2852
  });
1316
- onUnmounted4(() => {
2853
+ onUnmounted5(() => {
1317
2854
  pillAnim?.stop();
1318
2855
  ro?.disconnect();
1319
2856
  });
1320
- watch3(
2857
+ watch4(
1321
2858
  () => props.value,
1322
2859
  () => {
1323
2860
  updatePill(true);
@@ -1381,10 +2918,10 @@ var Toggle = defineComponent4({
1381
2918
  });
1382
2919
 
1383
2920
  // src/vue/components/SpringControl.ts
1384
- import { defineComponent as defineComponent6, h as h6, onMounted as onMounted5, onUnmounted as onUnmounted5, ref as ref5 } from "vue";
2921
+ import { defineComponent as defineComponent6, h as h6, onMounted as onMounted6, onUnmounted as onUnmounted6, ref as ref5 } from "vue";
1385
2922
 
1386
2923
  // src/vue/components/SpringVisualization.ts
1387
- import { defineComponent as defineComponent5, h as h5, computed as computed3 } from "vue";
2924
+ import { defineComponent as defineComponent5, h as h5, computed as computed4 } from "vue";
1388
2925
  function generateSpringCurve(stiffness, damping, mass, duration) {
1389
2926
  const points = [];
1390
2927
  const steps = 100;
@@ -1418,7 +2955,7 @@ var SpringVisualization = defineComponent5({
1418
2955
  setup(props) {
1419
2956
  const width = 256;
1420
2957
  const height = 140;
1421
- const pathData = computed3(() => {
2958
+ const pathData = computed4(() => {
1422
2959
  let stiffness;
1423
2960
  let damping;
1424
2961
  let mass;
@@ -1494,12 +3031,12 @@ var SpringControl = defineComponent6({
1494
3031
  setup(props, { emit }) {
1495
3032
  const mode = ref5(DialStore.getSpringMode(props.panelId, props.path));
1496
3033
  let unsub;
1497
- onMounted5(() => {
3034
+ onMounted6(() => {
1498
3035
  unsub = DialStore.subscribe(props.panelId, () => {
1499
3036
  mode.value = DialStore.getSpringMode(props.panelId, props.path);
1500
3037
  });
1501
3038
  });
1502
- onUnmounted5(() => {
3039
+ onUnmounted6(() => {
1503
3040
  unsub?.();
1504
3041
  });
1505
3042
  const isSimpleMode = () => mode.value === "simple";
@@ -1595,10 +3132,10 @@ var SpringControl = defineComponent6({
1595
3132
  });
1596
3133
 
1597
3134
  // src/vue/components/TransitionControl.ts
1598
- import { defineComponent as defineComponent8, h as h8, onMounted as onMounted6, onUnmounted as onUnmounted6, ref as ref6 } from "vue";
3135
+ import { defineComponent as defineComponent8, h as h8, onMounted as onMounted7, onUnmounted as onUnmounted7, ref as ref6 } from "vue";
1599
3136
 
1600
3137
  // src/vue/components/EasingVisualization.ts
1601
- import { defineComponent as defineComponent7, h as h7, computed as computed4 } from "vue";
3138
+ import { defineComponent as defineComponent7, h as h7, computed as computed5 } from "vue";
1602
3139
  var EasingVisualization = defineComponent7({
1603
3140
  name: "DialKitEasingVisualization",
1604
3141
  props: {
@@ -1612,7 +3149,7 @@ var EasingVisualization = defineComponent7({
1612
3149
  const pad = 10;
1613
3150
  const inner = size - pad * 2;
1614
3151
  const unit = inner / 2;
1615
- const curve = computed4(() => {
3152
+ const curve = computed5(() => {
1616
3153
  const [x1, y1, x2, y2] = props.easing.ease;
1617
3154
  const toSvg = (nx, ny) => ({
1618
3155
  x: pad + (nx + 0.5) * unit,
@@ -1715,18 +3252,20 @@ var TransitionControl = defineComponent8({
1715
3252
  value: {
1716
3253
  type: Object,
1717
3254
  required: true
1718
- }
3255
+ },
3256
+ hideDuration: { type: Boolean, default: false },
3257
+ durationControl: Object
1719
3258
  },
1720
3259
  emits: ["change"],
1721
3260
  setup(props, { emit }) {
1722
3261
  const mode = ref6(DialStore.getTransitionMode(props.panelId, props.path));
1723
3262
  let unsub;
1724
- onMounted6(() => {
3263
+ onMounted7(() => {
1725
3264
  unsub = DialStore.subscribe(props.panelId, () => {
1726
3265
  mode.value = DialStore.getTransitionMode(props.panelId, props.path);
1727
3266
  });
1728
3267
  });
1729
- onUnmounted6(() => unsub?.());
3268
+ onUnmounted7(() => unsub?.());
1730
3269
  const cache = {
1731
3270
  easing: props.value.type === "easing" ? { ...props.value } : { type: "easing", duration: 0.3, ease: [1, -0.4, 0.5, 1] },
1732
3271
  simple: props.value.type === "spring" && props.value.visualDuration !== void 0 ? { ...props.value } : { type: "spring", visualDuration: 0.3, bounce: 0.2 },
@@ -1778,6 +3317,18 @@ var TransitionControl = defineComponent8({
1778
3317
  const isSimpleSpring = mode.value === "simple";
1779
3318
  const currentSpring = spring();
1780
3319
  const currentEasing = easing();
3320
+ const durationSlider = !props.hideDuration && (isEasing || isSimpleSpring) ? h8(Slider, {
3321
+ label: "Duration",
3322
+ value: props.durationControl?.value ?? (isEasing ? currentEasing.duration : currentSpring.visualDuration ?? 0.3),
3323
+ min: props.durationControl?.min ?? 0.1,
3324
+ max: props.durationControl?.max ?? (isEasing ? 2 : 1),
3325
+ step: props.durationControl?.step ?? 0.05,
3326
+ unit: "s",
3327
+ onChange: props.durationControl?.onChange ?? ((next) => {
3328
+ if (isEasing) emit("change", { ...currentEasing, duration: next });
3329
+ else handleSpringUpdate("visualDuration", next);
3330
+ })
3331
+ }) : null;
1781
3332
  return h8(Folder, { title: props.label, defaultOpen: true }, {
1782
3333
  default: () => [
1783
3334
  h8("div", { style: { display: "flex", flexDirection: "column", gap: "6px" } }, [
@@ -1799,29 +3350,11 @@ var TransitionControl = defineComponent8({
1799
3350
  h8(Slider, { label: "y1", value: currentEasing.ease[1], min: -1, max: 2, step: 0.01, onChange: (next) => updateEase(1, next) }),
1800
3351
  h8(Slider, { label: "x2", value: currentEasing.ease[2], min: 0, max: 1, step: 0.01, onChange: (next) => updateEase(2, next) }),
1801
3352
  h8(Slider, { label: "y2", value: currentEasing.ease[3], min: -1, max: 2, step: 0.01, onChange: (next) => updateEase(3, next) }),
1802
- h8(Slider, {
1803
- label: "Duration",
1804
- value: currentEasing.duration,
1805
- min: 0.1,
1806
- max: 2,
1807
- step: 0.05,
1808
- unit: "s",
1809
- onChange: (next) => emit("change", { ...currentEasing, duration: next })
1810
- }),
1811
3353
  h8(EaseTextInput, {
1812
3354
  ease: currentEasing.ease,
1813
3355
  onChange: (next) => emit("change", { ...currentEasing, ease: next })
1814
3356
  })
1815
3357
  ] : isSimpleSpring ? [
1816
- h8(Slider, {
1817
- label: "Duration",
1818
- value: currentSpring.visualDuration ?? 0.3,
1819
- min: 0.1,
1820
- max: 1,
1821
- step: 0.05,
1822
- unit: "s",
1823
- onChange: (next) => handleSpringUpdate("visualDuration", next)
1824
- }),
1825
3358
  h8(Slider, {
1826
3359
  label: "Bounce",
1827
3360
  value: currentSpring.bounce ?? 0.2,
@@ -1855,7 +3388,8 @@ var TransitionControl = defineComponent8({
1855
3388
  step: 0.1,
1856
3389
  onChange: (next) => handleSpringUpdate("mass", next)
1857
3390
  })
1858
- ]
3391
+ ],
3392
+ durationSlider
1859
3393
  ])
1860
3394
  ]
1861
3395
  });
@@ -1891,8 +3425,28 @@ var TextControl = defineComponent9({
1891
3425
  });
1892
3426
 
1893
3427
  // src/vue/components/SelectControl.ts
1894
- import { Teleport, defineComponent as defineComponent10, h as h10, onMounted as onMounted7, ref as ref8, watch as watch4 } from "vue";
3428
+ import { Teleport, defineComponent as defineComponent10, h as h10, onMounted as onMounted8, ref as ref8, watch as watch5 } from "vue";
1895
3429
  import { AnimatePresence as AnimatePresence2, motion as motion2 } from "motion-v";
3430
+
3431
+ // src/dropdown-position.ts
3432
+ function getDropdownPosition(trigger, portalRoot, options = {}) {
3433
+ const { dropdownHeight = 0, gap = 4, allowAbove = true } = options;
3434
+ const triggerRect = trigger.getBoundingClientRect();
3435
+ const rootRect = portalRoot.getBoundingClientRect();
3436
+ const spaceBelow = window.innerHeight - triggerRect.bottom - gap;
3437
+ const above = allowAbove && spaceBelow < dropdownHeight && triggerRect.top > spaceBelow;
3438
+ return {
3439
+ top: above ? triggerRect.top - rootRect.top - dropdownHeight - gap : triggerRect.bottom - rootRect.top + gap,
3440
+ left: triggerRect.left - rootRect.left,
3441
+ width: triggerRect.width,
3442
+ above
3443
+ };
3444
+ }
3445
+ function getDialKitPortalRoot(trigger) {
3446
+ return trigger?.closest(".dialkit-root") ?? null;
3447
+ }
3448
+
3449
+ // src/vue/components/SelectControl.ts
1896
3450
  function toTitleCase(value) {
1897
3451
  return value.replace(/\b\w/g, (char) => char.toUpperCase());
1898
3452
  }
@@ -1921,17 +3475,9 @@ var SelectControl = defineComponent10({
1921
3475
  const normalizedOptions = () => normalizeOptions(props.options);
1922
3476
  const selectedLabel = () => normalizedOptions().find((option) => option.value === props.value)?.label ?? props.value;
1923
3477
  const updatePos = () => {
1924
- if (!triggerRef.value) return;
1925
- const rect = triggerRef.value.getBoundingClientRect();
3478
+ if (!triggerRef.value || !portalTarget.value) return;
1926
3479
  const dropdownHeight = 8 + normalizedOptions().length * 36;
1927
- const spaceBelow = window.innerHeight - rect.bottom - 4;
1928
- const above = spaceBelow < dropdownHeight && rect.top > spaceBelow;
1929
- pos.value = {
1930
- top: above ? rect.top - 4 : rect.bottom + 4,
1931
- left: rect.left,
1932
- width: rect.width,
1933
- above
1934
- };
3480
+ pos.value = getDropdownPosition(triggerRef.value, portalTarget.value, { dropdownHeight });
1935
3481
  };
1936
3482
  const openDropdown = () => {
1937
3483
  updatePos();
@@ -1956,7 +3502,7 @@ var SelectControl = defineComponent10({
1956
3502
  if (isOpen.value) closeDropdown();
1957
3503
  else openDropdown();
1958
3504
  };
1959
- watch4(isOpen, (open, _, onCleanup) => {
3505
+ watch5(isOpen, (open, _, onCleanup) => {
1960
3506
  if (!open) return;
1961
3507
  const handleViewportChange = () => updatePos();
1962
3508
  const handleDocumentClick = (event) => {
@@ -1974,9 +3520,8 @@ var SelectControl = defineComponent10({
1974
3520
  window.removeEventListener("scroll", handleViewportChange, true);
1975
3521
  });
1976
3522
  });
1977
- onMounted7(() => {
1978
- const root = triggerRef.value?.closest(".dialkit-root");
1979
- portalTarget.value = root ?? document.body;
3523
+ onMounted8(() => {
3524
+ portalTarget.value = getDialKitPortalRoot(triggerRef.value) ?? document.body;
1980
3525
  });
1981
3526
  return () => h10("div", { class: "dialkit-select-row" }, [
1982
3527
  h10("button", {
@@ -2012,16 +3557,11 @@ var SelectControl = defineComponent10({
2012
3557
  exit: { opacity: 0, y: pos.value.above ? 8 : -8, scale: 0.95 },
2013
3558
  transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
2014
3559
  style: {
2015
- position: "fixed",
3560
+ position: "absolute",
2016
3561
  left: `${pos.value.left}px`,
3562
+ top: `${pos.value.top}px`,
2017
3563
  width: `${pos.value.width}px`,
2018
- ...pos.value.above ? {
2019
- bottom: `${window.innerHeight - pos.value.top}px`,
2020
- transformOrigin: "bottom"
2021
- } : {
2022
- top: `${pos.value.top}px`,
2023
- transformOrigin: "top"
2024
- }
3564
+ transformOrigin: pos.value.above ? "bottom" : "top"
2025
3565
  }
2026
3566
  }, normalizedOptions().map((option) => h10("button", {
2027
3567
  key: option.value,
@@ -2039,7 +3579,7 @@ var SelectControl = defineComponent10({
2039
3579
  });
2040
3580
 
2041
3581
  // src/vue/components/ColorControl.ts
2042
- import { defineComponent as defineComponent11, h as h11, ref as ref9, watch as watch5 } from "vue";
3582
+ import { defineComponent as defineComponent11, h as h11, ref as ref9, watch as watch6 } from "vue";
2043
3583
  var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
2044
3584
  function expandShorthandHex(hex) {
2045
3585
  if (hex.length !== 4) return hex;
@@ -2058,7 +3598,7 @@ var ColorControl = defineComponent11({
2058
3598
  const isEditing = ref9(false);
2059
3599
  const editValue = ref9(props.value);
2060
3600
  const colorInputRef = ref9(null);
2061
- watch5(() => props.value, (value) => {
3601
+ watch6(() => props.value, (value) => {
2062
3602
  if (!isEditing.value) editValue.value = value;
2063
3603
  });
2064
3604
  const submit = () => {
@@ -2113,7 +3653,7 @@ var ColorControl = defineComponent11({
2113
3653
  });
2114
3654
 
2115
3655
  // src/vue/components/PresetManager.ts
2116
- import { Teleport as Teleport2, defineComponent as defineComponent12, h as h12, ref as ref10, watch as watch6 } from "vue";
3656
+ import { Teleport as Teleport2, defineComponent as defineComponent12, h as h12, ref as ref10, watch as watch7 } from "vue";
2117
3657
  import { AnimatePresence as AnimatePresence3, motion as motion3 } from "motion-v";
2118
3658
  var PresetManager = defineComponent12({
2119
3659
  name: "DialKitPresetManager",
@@ -2163,7 +3703,7 @@ var PresetManager = defineComponent12({
2163
3703
  if (isOpen.value) close();
2164
3704
  else open();
2165
3705
  };
2166
- watch6(isOpen, (open2, _, onCleanup) => {
3706
+ watch7(isOpen, (open2, _, onCleanup) => {
2167
3707
  if (!open2) return;
2168
3708
  const handler = (event) => {
2169
3709
  const target = event.target;
@@ -2261,7 +3801,7 @@ var PresetManager = defineComponent12({
2261
3801
  });
2262
3802
 
2263
3803
  // src/vue/components/ShortcutListener.ts
2264
- import { defineComponent as defineComponent13, inject, onMounted as onMounted8, onUnmounted as onUnmounted7, provide, ref as ref11 } from "vue";
3804
+ import { defineComponent as defineComponent13, inject, onMounted as onMounted9, onUnmounted as onUnmounted8, provide, ref as ref11 } from "vue";
2265
3805
  var ShortcutKey = /* @__PURE__ */ Symbol("DialKitShortcut");
2266
3806
  function useShortcutContext() {
2267
3807
  return inject(ShortcutKey, {
@@ -2441,7 +3981,7 @@ var ShortcutListener = defineComponent13({
2441
3981
  activePanelId.value = null;
2442
3982
  activePath.value = null;
2443
3983
  };
2444
- onMounted8(() => {
3984
+ onMounted9(() => {
2445
3985
  window.addEventListener("keydown", handleKeyDown);
2446
3986
  window.addEventListener("keyup", handleKeyUp);
2447
3987
  window.addEventListener("wheel", handleWheel, { passive: false });
@@ -2450,7 +3990,7 @@ var ShortcutListener = defineComponent13({
2450
3990
  window.addEventListener("mousemove", handleMouseMove);
2451
3991
  window.addEventListener("blur", handleWindowBlur);
2452
3992
  });
2453
- onUnmounted7(() => {
3993
+ onUnmounted8(() => {
2454
3994
  window.removeEventListener("keydown", handleKeyDown);
2455
3995
  window.removeEventListener("keyup", handleKeyUp);
2456
3996
  window.removeEventListener("wheel", handleWheel);
@@ -2478,9 +4018,15 @@ var Panel = defineComponent14({
2478
4018
  inline: {
2479
4019
  type: Boolean,
2480
4020
  default: false
2481
- }
4021
+ },
4022
+ variant: {
4023
+ type: String,
4024
+ default: "root"
4025
+ },
4026
+ toolbarExtra: Function
2482
4027
  },
2483
- setup(props) {
4028
+ emits: ["openChange"],
4029
+ setup(props, { emit }) {
2484
4030
  const shortcutCtx = useShortcutContext();
2485
4031
  const values = ref12(DialStore.getValues(props.panel.id));
2486
4032
  const presets = ref12(DialStore.getPresets(props.panel.id));
@@ -2489,14 +4035,14 @@ var Panel = defineComponent14({
2489
4035
  const hasShortcuts = () => Object.keys(DialStore.getPanel(props.panel.id)?.shortcuts ?? {}).length > 0;
2490
4036
  let unsubscribe;
2491
4037
  let copiedTimeout = null;
2492
- onMounted9(() => {
4038
+ onMounted10(() => {
2493
4039
  unsubscribe = DialStore.subscribe(props.panel.id, () => {
2494
4040
  values.value = DialStore.getValues(props.panel.id);
2495
4041
  presets.value = DialStore.getPresets(props.panel.id);
2496
4042
  activePresetId.value = DialStore.getActivePresetId(props.panel.id);
2497
4043
  });
2498
4044
  });
2499
- onUnmounted8(() => {
4045
+ onUnmounted9(() => {
2500
4046
  unsubscribe?.();
2501
4047
  if (copiedTimeout) {
2502
4048
  window.clearTimeout(copiedTimeout);
@@ -2529,6 +4075,9 @@ Apply these values as the new defaults in the useDialKit call.`;
2529
4075
  copied.value = false;
2530
4076
  }, 1500);
2531
4077
  };
4078
+ const handleOpenChange = (open) => {
4079
+ emit("openChange", open);
4080
+ };
2532
4081
  const renderControl = (control) => {
2533
4082
  const value = values.value[control.path];
2534
4083
  switch (control.type) {
@@ -2695,15 +4244,32 @@ Apply these values as the new defaults in the useDialKit call.`;
2695
4244
  })
2696
4245
  ]),
2697
4246
  "Copy"
2698
- ])
4247
+ ]),
4248
+ props.toolbarExtra?.()
2699
4249
  ]);
4250
+ if (props.variant === "section") {
4251
+ return h14(Folder, {
4252
+ title: props.panel.name,
4253
+ defaultOpen: props.defaultOpen,
4254
+ onOpenChange: handleOpenChange
4255
+ }, {
4256
+ default: () => [
4257
+ h14("div", {
4258
+ class: "dialkit-panel-section-toolbar",
4259
+ onClick: (event) => event.stopPropagation()
4260
+ }, [toolbarNode]),
4261
+ ...props.panel.controls.map(renderControl)
4262
+ ]
4263
+ });
4264
+ }
2700
4265
  return h14("div", { class: "dialkit-panel-wrapper" }, [
2701
4266
  h14(Folder, {
2702
4267
  title: props.panel.name,
2703
4268
  defaultOpen: props.defaultOpen,
2704
4269
  isRoot: true,
2705
4270
  inline: props.inline,
2706
- toolbar: () => toolbarNode
4271
+ toolbar: () => toolbarNode,
4272
+ onOpenChange: handleOpenChange
2707
4273
  }, {
2708
4274
  default: () => props.panel.controls.map(renderControl)
2709
4275
  })
@@ -2712,9 +4278,106 @@ Apply these values as the new defaults in the useDialKit call.`;
2712
4278
  }
2713
4279
  });
2714
4280
 
4281
+ // src/vue/components/Timeline/TimelineToggleButton.ts
4282
+ import { defineComponent as defineComponent15, h as h15, onMounted as onMounted11, onUnmounted as onUnmounted10, ref as ref13 } from "vue";
4283
+ var TimelineToggleButton = defineComponent15({
4284
+ name: "DialKitTimelineToggleButton",
4285
+ setup() {
4286
+ const visible = ref13(TimelineUiStore.getVisible());
4287
+ let unsubscribe;
4288
+ onMounted11(() => {
4289
+ unsubscribe = TimelineUiStore.subscribe(() => {
4290
+ visible.value = TimelineUiStore.getVisible();
4291
+ });
4292
+ });
4293
+ onUnmounted10(() => unsubscribe?.());
4294
+ return () => {
4295
+ const label = visible.value ? "Hide timeline" : "Show timeline";
4296
+ return h15("button", {
4297
+ class: "dialkit-toolbar-add dialkit-timeline-toolbar-toggle",
4298
+ "data-active": visible.value || void 0,
4299
+ "aria-pressed": visible.value,
4300
+ "aria-label": label,
4301
+ title: label,
4302
+ onClick: () => TimelineUiStore.toggle()
4303
+ }, [
4304
+ h15(
4305
+ "svg",
4306
+ { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" },
4307
+ ICON_TIMELINE.map((path) => h15("path", { d: path, fill: "currentColor" }))
4308
+ )
4309
+ ]);
4310
+ };
4311
+ }
4312
+ });
4313
+
4314
+ // src/panel-drag.ts
4315
+ var PANEL_DRAG_THRESHOLD = 8;
4316
+ var COLLAPSED_PANEL_SIZE = 42;
4317
+ var DRAG_EXCLUSION_SELECTOR = [
4318
+ ".dialkit-panel-icon",
4319
+ ".dialkit-panel-toolbar",
4320
+ "button",
4321
+ "input",
4322
+ "select",
4323
+ "textarea",
4324
+ "a",
4325
+ '[role="button"]',
4326
+ '[contenteditable="true"]'
4327
+ ].join(",");
4328
+ function getPanelDragHandle(target, panel) {
4329
+ if (!(target instanceof Element) || !panel) return null;
4330
+ const inner = target.closest(".dialkit-panel-inner");
4331
+ if (!inner || !panel.contains(inner)) return null;
4332
+ if (inner.getAttribute("data-collapsed") === "true") {
4333
+ return inner;
4334
+ }
4335
+ const header = target.closest(".dialkit-panel-header");
4336
+ if (!header || !inner.contains(header)) return null;
4337
+ if (target.closest(DRAG_EXCLUSION_SELECTOR)) return null;
4338
+ return header;
4339
+ }
4340
+ function getPanelDragStart(pointerX, pointerY, panel) {
4341
+ const rect = panel.getBoundingClientRect();
4342
+ return {
4343
+ pointerX,
4344
+ pointerY,
4345
+ elX: rect.left,
4346
+ elY: rect.top
4347
+ };
4348
+ }
4349
+ function getPanelDragOffset(start, pointerX, pointerY) {
4350
+ return {
4351
+ x: start.elX + pointerX - start.pointerX,
4352
+ y: start.elY + pointerY - start.pointerY
4353
+ };
4354
+ }
4355
+ function hasPanelDragMoved(start, pointerX, pointerY) {
4356
+ const dx = pointerX - start.pointerX;
4357
+ const dy = pointerY - start.pointerY;
4358
+ return Math.hypot(dx, dy) >= PANEL_DRAG_THRESHOLD;
4359
+ }
4360
+ function getPanelOriginX(position, offset, viewportWidth = typeof window !== "undefined" ? window.innerWidth : void 0) {
4361
+ if (offset && viewportWidth) {
4362
+ return offset.x + COLLAPSED_PANEL_SIZE / 2 < viewportWidth / 2 ? "left" : "right";
4363
+ }
4364
+ return position.endsWith("left") ? "left" : "right";
4365
+ }
4366
+ function blockPanelDragClick(handle) {
4367
+ const blocker = (event) => {
4368
+ event.preventDefault();
4369
+ event.stopImmediatePropagation();
4370
+ event.stopPropagation();
4371
+ };
4372
+ handle.addEventListener("click", blocker, { capture: true, once: true });
4373
+ window.setTimeout(() => {
4374
+ handle.removeEventListener("click", blocker, true);
4375
+ }, 0);
4376
+ }
4377
+
2715
4378
  // src/vue/components/DialRoot.ts
2716
- 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;
2717
- var DialRoot = defineComponent15({
4379
+ var isDevDefault2 = 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;
4380
+ var DialRoot = defineComponent16({
2718
4381
  name: "DialKitDialRoot",
2719
4382
  props: {
2720
4383
  position: {
@@ -2735,45 +4398,203 @@ var DialRoot = defineComponent15({
2735
4398
  },
2736
4399
  productionEnabled: {
2737
4400
  type: Boolean,
2738
- default: isDevDefault
4401
+ default: isDevDefault2
2739
4402
  }
2740
4403
  },
2741
- setup(props) {
2742
- const panels = ref13([]);
2743
- const mounted = ref13(false);
2744
- let unsubscribe;
2745
- onMounted10(() => {
4404
+ emits: ["openChange"],
4405
+ setup(props, { emit }) {
4406
+ const panels = ref14([]);
4407
+ const timelines = ref14([]);
4408
+ const mounted = ref14(false);
4409
+ const panelRef = ref14(null);
4410
+ const dragOffset = ref14(null);
4411
+ const activePosition = ref14(props.position);
4412
+ let unsubscribePanels;
4413
+ let unsubscribeTimelines;
4414
+ let observer;
4415
+ let lastDragOffset = null;
4416
+ let dragging = false;
4417
+ let dragStart = null;
4418
+ let didDrag = false;
4419
+ let dragTarget = null;
4420
+ let panelOpenStates = /* @__PURE__ */ new Map();
4421
+ let rootOpen;
4422
+ const syncPanelOpenStates = () => {
4423
+ const fallbackOpen = props.mode === "inline" || props.defaultOpen;
4424
+ const nextStates = /* @__PURE__ */ new Map();
4425
+ for (const panel of panels.value) {
4426
+ nextStates.set(panel.id, panelOpenStates.get(panel.id) ?? fallbackOpen);
4427
+ }
4428
+ panelOpenStates = nextStates;
4429
+ rootOpen = Array.from(nextStates.values()).some(Boolean);
4430
+ };
4431
+ const connectObserver = () => {
4432
+ if (observer || props.mode === "inline" || !panelRef.value) return;
4433
+ observer = new MutationObserver(() => {
4434
+ const inners = panelRef.value?.querySelectorAll(".dialkit-panel-inner");
4435
+ if (!inners || inners.length === 0) return;
4436
+ const collapsed = Array.from(inners).every((el) => el.getAttribute("data-collapsed") === "true");
4437
+ const currentDragOffset = dragOffset.value;
4438
+ if (!collapsed) {
4439
+ if (currentDragOffset) {
4440
+ lastDragOffset = currentDragOffset;
4441
+ const bubbleCenterX = currentDragOffset.x + 21;
4442
+ activePosition.value = bubbleCenterX < window.innerWidth / 2 ? "top-left" : "top-right";
4443
+ } else {
4444
+ activePosition.value = props.position;
4445
+ }
4446
+ dragOffset.value = null;
4447
+ } else if (currentDragOffset) {
4448
+ lastDragOffset = currentDragOffset;
4449
+ } else if (lastDragOffset) {
4450
+ dragOffset.value = lastDragOffset;
4451
+ }
4452
+ });
4453
+ observer.observe(panelRef.value, { subtree: true, attributes: true, attributeFilter: ["data-collapsed"] });
4454
+ };
4455
+ const handlePointerDown = (event) => {
4456
+ const panel = panelRef.value;
4457
+ const handle = getPanelDragHandle(event.target, panel);
4458
+ if (!panel || !handle) return;
4459
+ dragTarget = handle;
4460
+ dragStart = getPanelDragStart(event.clientX, event.clientY, panel);
4461
+ didDrag = false;
4462
+ dragging = true;
4463
+ handle.setPointerCapture(event.pointerId);
4464
+ };
4465
+ const handlePointerMove = (event) => {
4466
+ if (!dragging || !dragStart) return;
4467
+ if (!didDrag && !hasPanelDragMoved(dragStart, event.clientX, event.clientY)) return;
4468
+ didDrag = true;
4469
+ dragOffset.value = getPanelDragOffset(dragStart, event.clientX, event.clientY);
4470
+ };
4471
+ const handlePointerUp = (event) => {
4472
+ if (!dragging) return;
4473
+ dragging = false;
4474
+ dragStart = null;
4475
+ const handle = dragTarget;
4476
+ if (handle?.hasPointerCapture(event.pointerId)) {
4477
+ handle.releasePointerCapture(event.pointerId);
4478
+ }
4479
+ if (didDrag) {
4480
+ event.stopPropagation();
4481
+ if (handle) {
4482
+ blockPanelDragClick(handle);
4483
+ }
4484
+ }
4485
+ dragTarget = null;
4486
+ };
4487
+ const handlePanelOpenChange = (panelId, open) => {
4488
+ panelOpenStates.set(panelId, open);
4489
+ const fallbackOpen = props.mode === "inline" || props.defaultOpen;
4490
+ const nextRootOpen = panels.value.some((panel) => panelOpenStates.get(panel.id) ?? fallbackOpen);
4491
+ if (rootOpen === nextRootOpen) return;
4492
+ rootOpen = nextRootOpen;
4493
+ emit("openChange", nextRootOpen);
4494
+ };
4495
+ const handleRootOpenChange = (open) => {
4496
+ if (rootOpen === open) return;
4497
+ rootOpen = open;
4498
+ emit("openChange", open);
4499
+ };
4500
+ const getDragStyle = () => dragOffset.value ? {
4501
+ top: `${dragOffset.value.y}px`,
4502
+ left: `${dragOffset.value.x}px`,
4503
+ right: "auto",
4504
+ bottom: "auto"
4505
+ } : void 0;
4506
+ const originX = computed6(() => props.mode === "inline" ? void 0 : getPanelOriginX(activePosition.value, dragOffset.value));
4507
+ onMounted12(() => {
2746
4508
  mounted.value = true;
2747
- panels.value = DialStore.getPanels();
2748
- unsubscribe = DialStore.subscribeGlobal(() => {
2749
- panels.value = DialStore.getPanels();
4509
+ panels.value = DialStore.getPanels("panel");
4510
+ timelines.value = TimelineStore.getTimelines();
4511
+ syncPanelOpenStates();
4512
+ unsubscribePanels = DialStore.subscribeGlobal(() => {
4513
+ panels.value = DialStore.getPanels("panel");
4514
+ syncPanelOpenStates();
2750
4515
  });
4516
+ unsubscribeTimelines = TimelineStore.subscribeGlobal(() => {
4517
+ timelines.value = TimelineStore.getTimelines();
4518
+ });
4519
+ nextTick3(connectObserver);
2751
4520
  });
2752
- onUnmounted9(() => {
2753
- unsubscribe?.();
4521
+ onUnmounted11(() => {
4522
+ unsubscribePanels?.();
4523
+ unsubscribeTimelines?.();
4524
+ observer?.disconnect();
2754
4525
  });
2755
- const renderContent = () => h15(ShortcutListener, null, {
2756
- default: () => h15("div", { class: "dialkit-root", "data-mode": props.mode, "data-theme": props.theme }, [
2757
- h15("div", {
4526
+ const timelineToggle = () => timelines.value.length > 0 ? h16(TimelineToggleButton) : null;
4527
+ const renderPanels = () => {
4528
+ if (panels.value.length === 0) {
4529
+ return [h16("div", { class: "dialkit-panel-wrapper" }, [
4530
+ h16(Folder, {
4531
+ title: "DialKit",
4532
+ defaultOpen: props.mode === "inline" || props.defaultOpen,
4533
+ isRoot: true,
4534
+ inline: props.mode === "inline",
4535
+ toolbar: timelineToggle,
4536
+ onOpenChange: handleRootOpenChange,
4537
+ panelHeightOffset: 2
4538
+ }, { default: () => [h16("div", { class: "dialkit-timeline-toolkit-only" }, "Timeline")] })
4539
+ ])];
4540
+ }
4541
+ if (panels.value.length > 1) {
4542
+ return [h16("div", { class: "dialkit-panel-wrapper" }, [
4543
+ h16(Folder, {
4544
+ title: "DialKit",
4545
+ defaultOpen: props.mode === "inline" || props.defaultOpen,
4546
+ isRoot: true,
4547
+ inline: props.mode === "inline",
4548
+ toolbar: timelineToggle,
4549
+ onOpenChange: handleRootOpenChange,
4550
+ panelHeightOffset: 2
4551
+ }, {
4552
+ default: () => panels.value.map((panel) => h16(Panel, {
4553
+ key: panel.id,
4554
+ panel,
4555
+ defaultOpen: true,
4556
+ variant: "section"
4557
+ }))
4558
+ })
4559
+ ])];
4560
+ }
4561
+ return panels.value.map((panel) => h16(Panel, {
4562
+ key: panel.id,
4563
+ panel,
4564
+ defaultOpen: props.mode === "inline" || props.defaultOpen,
4565
+ inline: props.mode === "inline",
4566
+ toolbarExtra: timelineToggle,
4567
+ onOpenChange: (open) => handlePanelOpenChange(panel.id, open)
4568
+ }));
4569
+ };
4570
+ const renderContent = () => h16(ShortcutListener, null, {
4571
+ default: () => h16("div", { class: "dialkit-root", "data-mode": props.mode, "data-theme": props.theme }, [
4572
+ h16("div", {
4573
+ ref: (el) => {
4574
+ panelRef.value = el;
4575
+ connectObserver();
4576
+ },
2758
4577
  class: "dialkit-panel",
2759
- "data-position": props.mode === "inline" ? void 0 : props.position,
2760
- "data-mode": props.mode
2761
- }, panels.value.map((panel) => h15(Panel, {
2762
- key: panel.id,
2763
- panel,
2764
- defaultOpen: props.mode === "inline" || props.defaultOpen,
2765
- inline: props.mode === "inline"
2766
- })))
4578
+ "data-position": props.mode === "inline" ? void 0 : dragOffset.value ? void 0 : activePosition.value,
4579
+ "data-origin-x": originX.value,
4580
+ "data-mode": props.mode,
4581
+ "data-multiple": panels.value.length > 1 ? "true" : void 0,
4582
+ style: getDragStyle(),
4583
+ onPointerdown: props.mode === "inline" ? void 0 : handlePointerDown,
4584
+ onPointermove: props.mode === "inline" ? void 0 : handlePointerMove,
4585
+ onPointerup: props.mode === "inline" ? void 0 : handlePointerUp,
4586
+ onPointercancel: props.mode === "inline" ? void 0 : handlePointerUp
4587
+ }, renderPanels())
2767
4588
  ])
2768
4589
  });
2769
4590
  return () => {
2770
- if (!props.productionEnabled || !mounted.value || typeof window === "undefined" || panels.value.length === 0) {
4591
+ if (!props.productionEnabled || !mounted.value || typeof window === "undefined" || panels.value.length === 0 && timelines.value.length === 0) {
2771
4592
  return null;
2772
4593
  }
2773
4594
  if (props.mode === "inline") {
2774
4595
  return renderContent();
2775
4596
  }
2776
- return h15(Teleport3, { to: "body" }, renderContent());
4597
+ return h16(Teleport3, { to: "body" }, renderContent());
2777
4598
  };
2778
4599
  }
2779
4600
  });
@@ -2791,11 +4612,11 @@ function mountDialRoot(el, value) {
2791
4612
  if (typeof window === "undefined") return;
2792
4613
  const host = document.createElement("div");
2793
4614
  el.appendChild(host);
2794
- const props = shallowRef2(normalizeDirectiveValue(value));
2795
- const RootHost = defineComponent16({
4615
+ const props = shallowRef3(normalizeDirectiveValue(value));
4616
+ const RootHost = defineComponent17({
2796
4617
  name: "DialKitDirectiveHost",
2797
4618
  setup() {
2798
- return () => h16(DialRoot, props.value);
4619
+ return () => h17(DialRoot, props.value);
2799
4620
  }
2800
4621
  });
2801
4622
  const app = createApp(RootHost);
@@ -2826,8 +4647,1129 @@ var vDialKit = {
2826
4647
  }
2827
4648
  };
2828
4649
 
4650
+ // src/vue/components/Timeline/DialTimeline.ts
4651
+ import {
4652
+ Teleport as Teleport4,
4653
+ computed as computed7,
4654
+ defineComponent as defineComponent19,
4655
+ h as h19,
4656
+ nextTick as nextTick4,
4657
+ onMounted as onMounted13,
4658
+ onUnmounted as onUnmounted12,
4659
+ ref as ref15,
4660
+ watch as watch8
4661
+ } from "vue";
4662
+
4663
+ // src/vue/components/ControlRenderer.ts
4664
+ import { Fragment as Fragment2, defineComponent as defineComponent18, h as h18 } from "vue";
4665
+ import { inject as inject2 } from "vue";
4666
+ var ControlRenderer = defineComponent18({
4667
+ name: "DialKitControlRenderer",
4668
+ props: {
4669
+ panelId: { type: String, required: true },
4670
+ controls: { type: Array, required: true },
4671
+ values: { type: Object, required: true },
4672
+ transitionDuration: Object
4673
+ },
4674
+ setup(props) {
4675
+ const shortcut = inject2(ShortcutKey, void 0);
4676
+ const renderControl = (control) => {
4677
+ const value = props.values[control.path];
4678
+ switch (control.type) {
4679
+ case "slider":
4680
+ return h18(Slider, {
4681
+ key: control.path,
4682
+ label: control.label,
4683
+ value,
4684
+ min: control.min,
4685
+ max: control.max,
4686
+ step: control.step,
4687
+ shortcut: control.shortcut,
4688
+ shortcutActive: shortcut?.activePanelId.value === props.panelId && shortcut.activePath.value === control.path,
4689
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4690
+ });
4691
+ case "toggle":
4692
+ return h18(Toggle, {
4693
+ key: control.path,
4694
+ label: control.label,
4695
+ checked: value,
4696
+ shortcut: control.shortcut,
4697
+ shortcutActive: shortcut?.activePanelId.value === props.panelId && shortcut.activePath.value === control.path,
4698
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4699
+ });
4700
+ case "spring":
4701
+ return h18(SpringControl, {
4702
+ key: control.path,
4703
+ panelId: props.panelId,
4704
+ path: control.path,
4705
+ label: control.label,
4706
+ spring: value,
4707
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4708
+ });
4709
+ case "transition":
4710
+ return h18(TransitionControl, {
4711
+ key: control.path,
4712
+ panelId: props.panelId,
4713
+ path: control.path,
4714
+ label: control.label,
4715
+ value,
4716
+ durationControl: props.transitionDuration,
4717
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4718
+ });
4719
+ case "folder":
4720
+ return h18(Folder, {
4721
+ key: control.path,
4722
+ title: control.label,
4723
+ defaultOpen: control.defaultOpen ?? true
4724
+ }, { default: () => (control.children ?? []).map(renderControl) });
4725
+ case "text":
4726
+ return h18(TextControl, {
4727
+ key: control.path,
4728
+ label: control.label,
4729
+ value,
4730
+ placeholder: control.placeholder,
4731
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4732
+ });
4733
+ case "select":
4734
+ return h18(SelectControl, {
4735
+ key: control.path,
4736
+ label: control.label,
4737
+ value,
4738
+ options: control.options ?? [],
4739
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4740
+ });
4741
+ case "color":
4742
+ return h18(ColorControl, {
4743
+ key: control.path,
4744
+ label: control.label,
4745
+ value,
4746
+ onChange: (next) => DialStore.updateValue(props.panelId, control.path, next)
4747
+ });
4748
+ case "action":
4749
+ return h18("button", {
4750
+ key: control.path,
4751
+ class: "dialkit-button",
4752
+ onClick: () => DialStore.triggerAction(props.panelId, control.path)
4753
+ }, control.label);
4754
+ default:
4755
+ return null;
4756
+ }
4757
+ };
4758
+ return () => h18(Fragment2, null, props.controls.map(renderControl));
4759
+ }
4760
+ });
4761
+
4762
+ // src/vue/components/Timeline/DialTimeline.ts
4763
+ var DRAG_THRESHOLD_PX = 3;
4764
+ var MAJOR_TICK_TARGET_PX = 140;
4765
+ var MILLISECOND_STEP = 1e-3;
4766
+ var SECOND_TICK_STEPS = [
4767
+ 1e-3,
4768
+ 2e-3,
4769
+ 5e-3,
4770
+ 0.01,
4771
+ 0.02,
4772
+ 0.05,
4773
+ 0.1,
4774
+ 0.2,
4775
+ 0.5,
4776
+ 1,
4777
+ 2,
4778
+ 5,
4779
+ 10,
4780
+ 15,
4781
+ 30,
4782
+ 60,
4783
+ 120,
4784
+ 300,
4785
+ 600
4786
+ ];
4787
+ var MIN_TIMELINE_MAX_ZOOM = 8;
4788
+ var PLAYHEAD_FLAG_WIDTH = 52;
4789
+ var POPOVER_WIDTH = 280;
4790
+ var ZOOM_DRAG_DISTANCE = 180;
4791
+ var DialTimeline = defineComponent19({
4792
+ name: "DialKitTimeline",
4793
+ props: {
4794
+ theme: { type: String, default: "system" },
4795
+ defaultVisible: { type: Boolean, default: true },
4796
+ visible: {
4797
+ type: Boolean,
4798
+ default: void 0
4799
+ },
4800
+ onVisibilityChange: Function,
4801
+ defaultOpen: { type: Boolean, default: true },
4802
+ productionEnabled: { type: Boolean, default: isDevDefault }
4803
+ },
4804
+ setup(props) {
4805
+ const timelines = ref15(TimelineStore.getTimelines());
4806
+ const dockVisible = ref15(TimelineUiStore.getVisible());
4807
+ const mounted = ref15(false);
4808
+ const controllerId = /* @__PURE__ */ Symbol("dialkit-timeline-visibility");
4809
+ let unsubscribeTimelines;
4810
+ let unsubscribeVisibility;
4811
+ let unregisterController;
4812
+ onMounted13(() => {
4813
+ mounted.value = true;
4814
+ unsubscribeVisibility = TimelineUiStore.subscribe(() => {
4815
+ dockVisible.value = TimelineUiStore.getVisible();
4816
+ });
4817
+ unregisterController = TimelineUiStore.registerController(controllerId, {
4818
+ visible: props.visible,
4819
+ defaultVisible: props.defaultVisible,
4820
+ onVisibilityChange: props.onVisibilityChange
4821
+ });
4822
+ dockVisible.value = TimelineUiStore.getVisible();
4823
+ unsubscribeTimelines = TimelineStore.subscribeGlobal(() => {
4824
+ timelines.value = TimelineStore.getTimelines();
4825
+ });
4826
+ });
4827
+ watch8(() => [props.visible, props.defaultVisible, props.onVisibilityChange], () => {
4828
+ TimelineUiStore.updateController(controllerId, {
4829
+ visible: props.visible,
4830
+ defaultVisible: props.defaultVisible,
4831
+ onVisibilityChange: props.onVisibilityChange
4832
+ });
4833
+ });
4834
+ onUnmounted12(() => {
4835
+ unregisterController?.();
4836
+ unsubscribeTimelines?.();
4837
+ unsubscribeVisibility?.();
4838
+ });
4839
+ return () => {
4840
+ if (!props.productionEnabled || !mounted.value || timelines.value.length === 0) return null;
4841
+ return h19(Teleport4, { to: "body" }, [
4842
+ h19("div", {
4843
+ class: "dialkit-root dialkit-timeline",
4844
+ "data-theme": props.theme,
4845
+ hidden: !dockVisible.value
4846
+ }, [
4847
+ h19("div", { class: "dialkit-timeline-dock" }, timelines.value.map((meta) => h19(TimelineSection, {
4848
+ key: meta.id,
4849
+ meta,
4850
+ defaultOpen: props.defaultOpen,
4851
+ theme: props.theme,
4852
+ dockVisible: dockVisible.value
4853
+ })))
4854
+ ])
4855
+ ]);
4856
+ };
4857
+ }
4858
+ });
4859
+ var PlayPauseButton = defineComponent19({
4860
+ props: { id: { type: String, required: true } },
4861
+ setup(props) {
4862
+ const playing = ref15(TimelineStore.getTransport(props.id).playing);
4863
+ let unsubscribe;
4864
+ onMounted13(() => {
4865
+ unsubscribe = TimelineStore.subscribe(props.id, () => {
4866
+ playing.value = TimelineStore.getTransport(props.id).playing;
4867
+ });
4868
+ });
4869
+ onUnmounted12(() => unsubscribe?.());
4870
+ return () => {
4871
+ const label = playing.value ? "Pause" : "Play";
4872
+ const icon = playing.value ? h19(
4873
+ "svg",
4874
+ { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: iconStyle },
4875
+ ICON_PAUSE.map((path) => h19("path", { d: path, fill: "currentColor" }))
4876
+ ) : h19("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: iconStyle }, [
4877
+ h19("path", { d: ICON_PLAY, fill: "currentColor" })
4878
+ ]);
4879
+ return h19("button", {
4880
+ class: "dialkit-toolbar-add",
4881
+ title: label,
4882
+ "aria-label": label,
4883
+ onClick: () => playing.value ? TimelineStore.pause(props.id) : TimelineStore.play(props.id)
4884
+ }, [h19("span", { style: { position: "relative", width: "16px", height: "16px" } }, [icon])]);
4885
+ };
4886
+ }
4887
+ });
4888
+ var iconStyle = {
4889
+ position: "absolute",
4890
+ inset: 0,
4891
+ width: "16px",
4892
+ height: "16px",
4893
+ color: "var(--dial-text-label)"
4894
+ };
4895
+ var TimelineOverview = defineComponent19({
4896
+ props: {
4897
+ id: { type: String, required: true },
4898
+ duration: { type: Number, required: true },
4899
+ viewStart: { type: Number, required: true },
4900
+ viewEnd: { type: Number, required: true },
4901
+ onNavigate: { type: Function, required: true }
4902
+ },
4903
+ setup(props) {
4904
+ const time = ref15(TimelineStore.getTransport(props.id).time);
4905
+ let scrub = null;
4906
+ let unsubscribe;
4907
+ onMounted13(() => {
4908
+ unsubscribe = TimelineStore.subscribe(props.id, () => {
4909
+ time.value = TimelineStore.getTransport(props.id).time;
4910
+ });
4911
+ });
4912
+ onUnmounted12(() => unsubscribe?.());
4913
+ const seek = (clientX) => {
4914
+ if (!scrub || scrub.rect.width <= 0 || props.duration <= 0) return;
4915
+ const next = clamp((clientX - scrub.rect.left) / scrub.rect.width * props.duration, 0, props.duration);
4916
+ TimelineStore.seek(props.id, next);
4917
+ props.onNavigate(next);
4918
+ };
4919
+ const finish = () => {
4920
+ if (scrub?.wasPlaying) TimelineStore.play(props.id);
4921
+ scrub = null;
4922
+ };
4923
+ return () => {
4924
+ const viewportWidth = props.duration > 0 ? (props.viewEnd - props.viewStart) / props.duration * 100 : 100;
4925
+ const playhead = props.duration > 0 ? time.value / props.duration * 100 : 0;
4926
+ return h19("div", {
4927
+ class: "dialkit-timeline-overview",
4928
+ title: "Drag to scrub the full timeline",
4929
+ onPointerdown: (event) => {
4930
+ event.preventDefault();
4931
+ event.currentTarget.setPointerCapture(event.pointerId);
4932
+ scrub = {
4933
+ wasPlaying: TimelineStore.getTransport(props.id).playing,
4934
+ rect: event.currentTarget.getBoundingClientRect()
4935
+ };
4936
+ TimelineStore.pause(props.id);
4937
+ seek(event.clientX);
4938
+ },
4939
+ onPointermove: (event) => scrub && seek(event.clientX),
4940
+ onPointerup: finish,
4941
+ onPointercancel: finish,
4942
+ onLostpointercapture: finish
4943
+ }, [
4944
+ h19("div", {
4945
+ class: "dialkit-timeline-overview-viewport",
4946
+ "data-zoomed": viewportWidth < 99.999 || void 0,
4947
+ style: { left: `${props.duration > 0 ? props.viewStart / props.duration * 100 : 0}%`, width: `${viewportWidth}%` }
4948
+ }),
4949
+ h19("div", { class: "dialkit-timeline-overview-progress", style: { width: `${playhead}%` } }),
4950
+ h19("div", { class: "dialkit-timeline-overview-playhead", style: { left: `${playhead}%` } })
4951
+ ]);
4952
+ };
4953
+ }
4954
+ });
4955
+ var TimelinePlayheadFlag = defineComponent19({
4956
+ props: {
4957
+ id: { type: String, required: true },
4958
+ duration: { type: Number, required: true },
4959
+ pxPerSecond: { type: Number, required: true },
4960
+ viewStart: { type: Number, required: true },
4961
+ viewEnd: { type: Number, required: true },
4962
+ laneWidth: { type: Number, required: true },
4963
+ ruler: Object,
4964
+ headerClearStart: { type: Number, required: true },
4965
+ headerClearEnd: { type: Number, required: true },
4966
+ onResetView: { type: Function, required: true }
4967
+ },
4968
+ setup(props) {
4969
+ const time = ref15(TimelineStore.getTransport(props.id).time);
4970
+ let unsubscribe;
4971
+ let scrub = null;
4972
+ let cleanup = null;
4973
+ onMounted13(() => {
4974
+ unsubscribe = TimelineStore.subscribe(props.id, () => {
4975
+ time.value = TimelineStore.getTransport(props.id).time;
4976
+ });
4977
+ });
4978
+ onUnmounted12(() => {
4979
+ unsubscribe?.();
4980
+ cleanup?.();
4981
+ });
4982
+ const seek = (clientX) => {
4983
+ if (!scrub || scrub.rect.width <= 0) return;
4984
+ TimelineStore.seek(props.id, clamp(
4985
+ scrub.viewStart + (clientX - scrub.rect.left) / scrub.rect.width * (scrub.viewEnd - scrub.viewStart),
4986
+ scrub.viewStart,
4987
+ scrub.viewEnd
4988
+ ));
4989
+ };
4990
+ return () => {
4991
+ if (time.value < props.viewStart || time.value > props.viewEnd || props.laneWidth <= 0) return null;
4992
+ const x = clamp((time.value - props.viewStart) * props.pxPerSecond, 0, props.laneWidth);
4993
+ const left = x - PLAYHEAD_FLAG_WIDTH / 2;
4994
+ const placement = left >= props.headerClearStart && left + PLAYHEAD_FLAG_WIDTH <= props.headerClearEnd ? "raised" : "lowered";
4995
+ return h19("div", {
4996
+ class: "dialkit-timeline-playhead-control",
4997
+ "data-edge": "center",
4998
+ "data-placement": placement,
4999
+ style: { left: `calc(var(--dial-timeline-label-w) + ${x}px)` },
5000
+ role: "slider",
5001
+ "aria-label": "Timeline current time",
5002
+ "aria-valuemin": 0,
5003
+ "aria-valuemax": props.duration,
5004
+ "aria-valuenow": time.value,
5005
+ title: "Drag to scrub the timeline",
5006
+ onPointerdown: (event) => {
5007
+ const rect = props.ruler?.getBoundingClientRect();
5008
+ if (!rect) return;
5009
+ event.preventDefault();
5010
+ event.stopPropagation();
5011
+ cleanup?.();
5012
+ const reset = event.shiftKey;
5013
+ scrub = {
5014
+ wasPlaying: TimelineStore.getTransport(props.id).playing,
5015
+ rect,
5016
+ viewStart: reset ? 0 : props.viewStart,
5017
+ viewEnd: reset ? props.duration : props.viewEnd
5018
+ };
5019
+ if (reset) props.onResetView();
5020
+ TimelineStore.pause(props.id);
5021
+ seek(event.clientX);
5022
+ const move = (next) => {
5023
+ next.preventDefault();
5024
+ seek(next.clientX);
5025
+ };
5026
+ const finish = () => {
5027
+ window.removeEventListener("pointermove", move);
5028
+ window.removeEventListener("pointerup", finish);
5029
+ window.removeEventListener("pointercancel", finish);
5030
+ if (scrub?.wasPlaying) TimelineStore.play(props.id);
5031
+ scrub = null;
5032
+ cleanup = null;
5033
+ };
5034
+ window.addEventListener("pointermove", move, { passive: false });
5035
+ window.addEventListener("pointerup", finish);
5036
+ window.addEventListener("pointercancel", finish);
5037
+ cleanup = finish;
5038
+ }
5039
+ }, [
5040
+ h19("div", { class: "dialkit-timeline-playhead-flag" }, time.value.toFixed(2)),
5041
+ h19("div", { class: "dialkit-timeline-playhead-stem" })
5042
+ ]);
5043
+ };
5044
+ }
5045
+ });
5046
+ function clampViewStart(start, duration, visibleDuration) {
5047
+ return clamp(start, 0, Math.max(0, duration - visibleDuration));
5048
+ }
5049
+ function formatRulerSeconds(time, step) {
5050
+ if (step >= 1 && Number.isInteger(time)) return formatClock(time);
5051
+ const decimals = Math.min(3, Math.max(1, Math.ceil(-Math.log10(step))));
5052
+ return `${time.toFixed(decimals)}s`;
5053
+ }
5054
+ var TimelineSection = defineComponent19({
5055
+ props: {
5056
+ meta: { type: Object, required: true },
5057
+ defaultOpen: { type: Boolean, required: true },
5058
+ theme: { type: String, required: true },
5059
+ dockVisible: { type: Boolean, required: true }
5060
+ },
5061
+ setup(props) {
5062
+ const open = ref15(props.defaultOpen);
5063
+ const copied = ref15(false);
5064
+ const popover = ref15(null);
5065
+ const collapsedGroups = ref15(/* @__PURE__ */ new Set());
5066
+ const expandedTracks = ref15(/* @__PURE__ */ new Set());
5067
+ const zoom = ref15(1);
5068
+ const viewStart = ref15(0);
5069
+ const values = ref15(DialStore.getValues(props.meta.id));
5070
+ const presets = ref15(DialStore.getPresets(props.meta.id));
5071
+ const activePresetId = ref15(DialStore.getActivePresetId(props.meta.id));
5072
+ const laneAreaRef = ref15(null);
5073
+ const titleRef = ref15(null);
5074
+ const actionsRef = ref15(null);
5075
+ const laneWidth = ref15(0);
5076
+ const flagClearRange = ref15({ start: 0, end: 0 });
5077
+ let unsubscribeValues;
5078
+ let resizeObserver;
5079
+ const measure = () => {
5080
+ if (!laneAreaRef.value || !titleRef.value || !actionsRef.value) return;
5081
+ const ruler = laneAreaRef.value.getBoundingClientRect();
5082
+ const title = titleRef.value.getBoundingClientRect();
5083
+ const actions = actionsRef.value.getBoundingClientRect();
5084
+ laneWidth.value = ruler.width;
5085
+ flagClearRange.value = {
5086
+ start: Math.round(title.right + 10 - ruler.left),
5087
+ end: Math.round(actions.left - 10 - ruler.left)
5088
+ };
5089
+ };
5090
+ const connectMeasure = async () => {
5091
+ resizeObserver?.disconnect();
5092
+ if (!open.value) return;
5093
+ await nextTick4();
5094
+ if (!laneAreaRef.value || !titleRef.value || !actionsRef.value) return;
5095
+ measure();
5096
+ resizeObserver = new ResizeObserver(measure);
5097
+ resizeObserver.observe(laneAreaRef.value);
5098
+ resizeObserver.observe(titleRef.value);
5099
+ resizeObserver.observe(actionsRef.value);
5100
+ };
5101
+ onMounted13(() => {
5102
+ unsubscribeValues = DialStore.subscribe(props.meta.id, () => {
5103
+ values.value = DialStore.getValues(props.meta.id);
5104
+ presets.value = DialStore.getPresets(props.meta.id);
5105
+ activePresetId.value = DialStore.getActivePresetId(props.meta.id);
5106
+ });
5107
+ void connectMeasure();
5108
+ });
5109
+ onUnmounted12(() => {
5110
+ unsubscribeValues?.();
5111
+ resizeObserver?.disconnect();
5112
+ });
5113
+ watch8(open, connectMeasure);
5114
+ watch8(() => props.dockVisible, (visible) => {
5115
+ if (!visible) popover.value = null;
5116
+ });
5117
+ const visibleDuration = computed7(() => props.meta.duration > 0 ? props.meta.duration / zoom.value : props.meta.duration);
5118
+ const safeViewStart = computed7(() => clampViewStart(viewStart.value, props.meta.duration, visibleDuration.value));
5119
+ const viewEnd = computed7(() => safeViewStart.value + visibleDuration.value);
5120
+ const pxPerSecond = computed7(() => visibleDuration.value > 0 && laneWidth.value > 0 ? laneWidth.value / visibleDuration.value : 0);
5121
+ const maxZoom = computed7(() => Math.max(
5122
+ MIN_TIMELINE_MAX_ZOOM,
5123
+ laneWidth.value > 0 && props.meta.duration > 0 ? MAJOR_TICK_TARGET_PX * props.meta.duration / (MILLISECOND_STEP * 10 * laneWidth.value) : MIN_TIMELINE_MAX_ZOOM
5124
+ ));
5125
+ watch8(maxZoom, (next) => {
5126
+ zoom.value = clamp(zoom.value, 1, next);
5127
+ }, { immediate: true });
5128
+ watch8([() => props.meta.duration, zoom], () => {
5129
+ viewStart.value = clampViewStart(viewStart.value, props.meta.duration, props.meta.duration / zoom.value);
5130
+ });
5131
+ const centerViewAt = (time) => {
5132
+ if (zoom.value <= 1 || props.meta.duration <= 0) return;
5133
+ const duration = props.meta.duration / zoom.value;
5134
+ viewStart.value = clampViewStart(time - duration / 2, props.meta.duration, duration);
5135
+ };
5136
+ const resetView = () => {
5137
+ zoom.value = 1;
5138
+ viewStart.value = 0;
5139
+ };
5140
+ let zoomDrag = null;
5141
+ let rulerScrub = null;
5142
+ let trackScrub = null;
5143
+ const seekRuler = (clientX) => {
5144
+ if (!rulerScrub || rulerScrub.rect.width <= 0) return;
5145
+ TimelineStore.seek(props.meta.id, clamp(
5146
+ rulerScrub.viewStart + (clientX - rulerScrub.rect.left) / rulerScrub.rect.width * rulerScrub.visibleDuration,
5147
+ rulerScrub.viewStart,
5148
+ rulerScrub.viewStart + rulerScrub.visibleDuration
5149
+ ));
5150
+ };
5151
+ const seekTrack = (clientX) => {
5152
+ if (!trackScrub || trackScrub.rect.width <= 0) return;
5153
+ TimelineStore.seek(props.meta.id, clamp(
5154
+ trackScrub.viewStart + (clientX - trackScrub.rect.left) / trackScrub.rect.width * trackScrub.visibleDuration,
5155
+ trackScrub.viewStart,
5156
+ trackScrub.viewStart + trackScrub.visibleDuration
5157
+ ));
5158
+ };
5159
+ const finishRuler = () => {
5160
+ if (rulerScrub?.wasPlaying) TimelineStore.play(props.meta.id);
5161
+ rulerScrub = null;
5162
+ zoomDrag = null;
5163
+ };
5164
+ const finishTrack = () => {
5165
+ if (trackScrub?.wasPlaying) TimelineStore.play(props.meta.id);
5166
+ trackScrub = null;
5167
+ };
5168
+ const handleCopy = () => {
5169
+ const normalized = normalizeTimelineValuesForCopy(DialStore.getValues(props.meta.id), props.meta.clips);
5170
+ void navigator.clipboard.writeText(buildCopyInstruction("useDialTimeline", props.meta.name, normalized));
5171
+ copied.value = true;
5172
+ window.setTimeout(() => {
5173
+ copied.value = false;
5174
+ }, 1500);
5175
+ };
5176
+ const handleAddPreset = () => DialStore.savePreset(props.meta.id, `Version ${presets.value.length + 2}`);
5177
+ const closePopover = () => {
5178
+ popover.value = null;
5179
+ };
5180
+ const openClipPopover = (clip, rect, stepKey) => {
5181
+ const target = stepKey ? `${clip.key}.${stepKey}` : clip.key;
5182
+ if (getClipControls(props.meta.id, target, stepKey ? void 0 : clipPopoverExclusions(clip)).length === 0) return;
5183
+ popover.value = popover.value?.clip.key === clip.key && popover.value.stepKey === stepKey ? null : {
5184
+ clip,
5185
+ stepKey,
5186
+ anchor: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height }
5187
+ };
5188
+ };
5189
+ const toggleSet = (state, key) => {
5190
+ const next = new Set(state.value);
5191
+ if (next.has(key)) next.delete(key);
5192
+ else next.add(key);
5193
+ state.value = next;
5194
+ };
5195
+ const toggleTracks = (key) => toggleSet(expandedTracks, key);
5196
+ const toggleGroup = (key) => toggleSet(collapsedGroups, key);
5197
+ const handleBarClick = (clip, rect, stepKey) => {
5198
+ if (!stepKey && clip.tracks?.length) toggleTracks(clip.key);
5199
+ else openClipPopover(clip, rect, stepKey);
5200
+ };
5201
+ const ticks = computed7(() => {
5202
+ const raw = pxPerSecond.value > 0 ? MAJOR_TICK_TARGET_PX / pxPerSecond.value : 1;
5203
+ const adaptive = SECOND_TICK_STEPS.find((step) => step >= raw) ?? SECOND_TICK_STEPS[SECOND_TICK_STEPS.length - 1];
5204
+ const majorStep = zoom.value < 1.5 && props.meta.duration >= 1 ? Math.max(1, adaptive) : adaptive;
5205
+ const fineStep = majorStep / 10;
5206
+ const major = [];
5207
+ const medium = [];
5208
+ const fine = [];
5209
+ for (let time = Math.ceil((safeViewStart.value - 1e-6) / majorStep) * majorStep; time <= viewEnd.value + 1e-6; time += majorStep) {
5210
+ major.push(Number(time.toFixed(4)));
5211
+ }
5212
+ const first = Math.ceil((safeViewStart.value - 1e-6) / fineStep);
5213
+ const last = Math.floor((viewEnd.value + 1e-6) / fineStep);
5214
+ for (let index = first; index <= last; index++) {
5215
+ if (index % 10 === 0) continue;
5216
+ const tick = Number((index * fineStep).toFixed(6));
5217
+ if (index % 5 === 0) medium.push(tick);
5218
+ else fine.push(tick);
5219
+ }
5220
+ return { major, medium, fine, majorStep };
5221
+ });
5222
+ const renderRows = () => {
5223
+ const rows = [];
5224
+ let lastGroup;
5225
+ for (const clip of props.meta.clips) {
5226
+ if (clip.group !== lastGroup) {
5227
+ lastGroup = clip.group;
5228
+ if (clip.group) {
5229
+ const group = clip.group;
5230
+ const collapsed = collapsedGroups.value.has(group);
5231
+ rows.push(h19("div", { key: `group:${group}`, class: "dialkit-timeline-row dialkit-timeline-group-row" }, [
5232
+ h19("div", { class: "dialkit-timeline-label" }, [
5233
+ h19("button", {
5234
+ class: "dialkit-timeline-group-toggle",
5235
+ "data-open": !collapsed,
5236
+ title: collapsed ? "Expand layer" : "Collapse layer",
5237
+ onClick: () => toggleGroup(group)
5238
+ }, [chevronIcon()]),
5239
+ h19("span", formatLabel(group))
5240
+ ]),
5241
+ h19("div", { class: "dialkit-timeline-lane" })
5242
+ ]));
5243
+ }
5244
+ }
5245
+ if (clip.group && collapsedGroups.value.has(clip.group)) continue;
5246
+ const isProps = Boolean(clip.tracks?.length);
5247
+ const tracksOpen = isProps && expandedTracks.value.has(clip.key);
5248
+ const stat = computeClipStaticFromValues(values.value, clip, props.meta.duration);
5249
+ const selected = popover.value?.clip.key === clip.key;
5250
+ rows.push(h19("div", { key: clip.key, class: "dialkit-timeline-row", "data-grouped": clip.group ? "" : void 0 }, [
5251
+ h19("div", { class: "dialkit-timeline-label" }, [
5252
+ isProps ? h19("button", {
5253
+ class: "dialkit-timeline-group-toggle",
5254
+ "data-open": tracksOpen,
5255
+ title: tracksOpen ? "Collapse properties" : "Expand properties",
5256
+ onClick: (event) => {
5257
+ event.stopPropagation();
5258
+ toggleTracks(clip.key);
5259
+ }
5260
+ }, [chevronIcon()]) : null,
5261
+ clip.label
5262
+ ]),
5263
+ h19("div", { class: "dialkit-timeline-lane" }, [h19(TimelineClip, {
5264
+ timelineId: props.meta.id,
5265
+ clip,
5266
+ at: stat.at,
5267
+ duration: stat.duration,
5268
+ loop: stat.loop,
5269
+ steps: clip.stepKeys?.length ? stat.tracks[0]?.steps : void 0,
5270
+ fixedDuration: isProps ? true : stat.isPhysics,
5271
+ composite: isProps,
5272
+ pxPerSecond: pxPerSecond.value,
5273
+ viewStart: safeViewStart.value,
5274
+ timelineDuration: props.meta.duration,
5275
+ selected,
5276
+ selectedStepKey: selected ? popover.value?.stepKey : void 0,
5277
+ onClick: handleBarClick,
5278
+ onDrag: closePopover
5279
+ })])
5280
+ ]));
5281
+ if (!tracksOpen) continue;
5282
+ for (const trackRef of clip.tracks ?? []) {
5283
+ const track = stat.tracks.find((candidate) => candidate.prop === trackRef.prop);
5284
+ if (!track) continue;
5285
+ const trackKey = `${clip.key}.${trackRef.prop}`;
5286
+ const trackMeta = {
5287
+ key: trackKey,
5288
+ label: `${clip.label} \xB7 ${formatLabel(trackRef.prop)}`,
5289
+ color: clip.color,
5290
+ loop: clip.loop,
5291
+ group: clip.group,
5292
+ stepKeys: trackRef.stepKeys
5293
+ };
5294
+ const trackSelected = popover.value?.clip.key === trackKey;
5295
+ rows.push(h19("div", { key: trackKey, class: "dialkit-timeline-row dialkit-timeline-track-row", "data-grouped": clip.group ? "" : void 0 }, [
5296
+ h19("div", { class: "dialkit-timeline-label" }, formatLabel(trackRef.prop)),
5297
+ h19("div", { class: "dialkit-timeline-lane" }, [h19(TimelineClip, {
5298
+ timelineId: props.meta.id,
5299
+ clip: trackMeta,
5300
+ at: stat.at + track.delay,
5301
+ duration: track.duration,
5302
+ loop: stat.loop,
5303
+ steps: trackRef.stepKeys?.length ? track.steps : void 0,
5304
+ fixedDuration: !trackRef.stepKeys?.length && track.steps[0]?.isPhysics === true,
5305
+ baseAt: stat.at,
5306
+ delayMode: true,
5307
+ pxPerSecond: pxPerSecond.value,
5308
+ viewStart: safeViewStart.value,
5309
+ timelineDuration: props.meta.duration,
5310
+ selected: trackSelected,
5311
+ selectedStepKey: trackSelected ? popover.value?.stepKey : void 0,
5312
+ onClick: openClipPopover,
5313
+ onDrag: closePopover
5314
+ })])
5315
+ ]));
5316
+ }
5317
+ }
5318
+ return rows;
5319
+ };
5320
+ return () => h19("div", { class: "dialkit-timeline-section" }, [
5321
+ h19("div", { class: "dialkit-timeline-header", "data-open": open.value || void 0 }, [
5322
+ h19("div", { class: "dialkit-timeline-identity" }, [
5323
+ h19("span", { ref: titleRef, class: "dialkit-timeline-title" }, props.meta.name)
5324
+ ]),
5325
+ !open.value ? h19(TimelineOverview, {
5326
+ id: props.meta.id,
5327
+ duration: props.meta.duration,
5328
+ viewStart: safeViewStart.value,
5329
+ viewEnd: viewEnd.value,
5330
+ onNavigate: centerViewAt
5331
+ }) : null,
5332
+ h19("div", { ref: actionsRef, class: "dialkit-timeline-actions" }, [
5333
+ h19(PlayPauseButton, { id: props.meta.id }),
5334
+ h19("button", { class: "dialkit-toolbar-add", title: "Add timeline version", "aria-label": "Add timeline version", onClick: handleAddPreset }, [
5335
+ h19(
5336
+ "svg",
5337
+ { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" },
5338
+ ICON_ADD_PRESET.map((path) => h19("path", { d: path }))
5339
+ )
5340
+ ]),
5341
+ h19(PresetManager, { panelId: props.meta.id, presets: presets.value, activePresetId: activePresetId.value }),
5342
+ h19("button", {
5343
+ class: "dialkit-toolbar-add",
5344
+ title: "Copy parameters",
5345
+ "aria-label": copied.value ? "Copied parameters" : "Copy parameters",
5346
+ onClick: handleCopy
5347
+ }, [h19("span", { style: { position: "relative", width: "16px", height: "16px" } }, [
5348
+ copied.value ? h19("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", style: iconStyle }, [h19("path", { d: ICON_CHECK })]) : h19("svg", { viewBox: "0 0 24 24", fill: "none", style: iconStyle }, [
5349
+ h19("path", { d: ICON_CLIPBOARD.board, stroke: "currentColor", "stroke-width": "2", "stroke-linejoin": "round" }),
5350
+ h19("path", { d: ICON_CLIPBOARD.sparkle, fill: "currentColor" }),
5351
+ h19("path", { d: ICON_CLIPBOARD.body, stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" })
5352
+ ])
5353
+ ])]),
5354
+ h19("button", {
5355
+ class: "dialkit-timeline-chevron",
5356
+ "data-open": open.value,
5357
+ "aria-expanded": open.value,
5358
+ title: open.value ? "Collapse timeline" : "Expand timeline",
5359
+ onClick: () => {
5360
+ open.value = !open.value;
5361
+ }
5362
+ }, [chevronIcon()])
5363
+ ])
5364
+ ]),
5365
+ open.value ? h19("div", {
5366
+ class: "dialkit-timeline-body",
5367
+ onPointerdown: (event) => {
5368
+ const target = event.target;
5369
+ if (target.closest(".dialkit-timeline-label, button")) return;
5370
+ if (!event.shiftKey && target.closest(".dialkit-timeline-clip")) return;
5371
+ const rect = laneAreaRef.value?.getBoundingClientRect();
5372
+ if (!rect) return;
5373
+ event.preventDefault();
5374
+ event.currentTarget.setPointerCapture(event.pointerId);
5375
+ const reset = event.shiftKey;
5376
+ trackScrub = {
5377
+ wasPlaying: TimelineStore.getTransport(props.meta.id).playing,
5378
+ rect,
5379
+ viewStart: reset ? 0 : safeViewStart.value,
5380
+ visibleDuration: reset ? props.meta.duration : visibleDuration.value
5381
+ };
5382
+ if (reset) resetView();
5383
+ popover.value = null;
5384
+ TimelineStore.pause(props.meta.id);
5385
+ seekTrack(event.clientX);
5386
+ },
5387
+ onPointermove: (event) => trackScrub && seekTrack(event.clientX),
5388
+ onPointerup: finishTrack,
5389
+ onPointercancel: finishTrack,
5390
+ onLostpointercapture: finishTrack
5391
+ }, [h19("div", { class: "dialkit-timeline-grid" }, [
5392
+ h19("div", { class: "dialkit-timeline-row dialkit-timeline-ruler-row" }, [
5393
+ h19("div", { class: "dialkit-timeline-label" }),
5394
+ h19("div", {
5395
+ ref: laneAreaRef,
5396
+ class: "dialkit-timeline-ruler",
5397
+ title: "Drag to seek \xB7 Option-drag to zoom \xB7 Shift-drag to reset zoom",
5398
+ onPointerdown: (event) => {
5399
+ event.preventDefault();
5400
+ event.stopPropagation();
5401
+ const rect = event.currentTarget.getBoundingClientRect();
5402
+ if (rect.width <= 0) return;
5403
+ event.currentTarget.setPointerCapture(event.pointerId);
5404
+ if (!event.altKey) {
5405
+ const reset = event.shiftKey;
5406
+ rulerScrub = {
5407
+ wasPlaying: TimelineStore.getTransport(props.meta.id).playing,
5408
+ rect,
5409
+ viewStart: reset ? 0 : safeViewStart.value,
5410
+ visibleDuration: reset ? props.meta.duration : visibleDuration.value
5411
+ };
5412
+ if (reset) resetView();
5413
+ TimelineStore.pause(props.meta.id);
5414
+ seekRuler(event.clientX);
5415
+ return;
5416
+ }
5417
+ const ratio = clamp((event.clientX - rect.left) / rect.width, 0, 1);
5418
+ zoomDrag = {
5419
+ pointerX: event.clientX,
5420
+ zoom: zoom.value,
5421
+ anchorRatio: ratio,
5422
+ anchorTime: safeViewStart.value + ratio * visibleDuration.value,
5423
+ moved: false
5424
+ };
5425
+ },
5426
+ onPointermove: (event) => {
5427
+ if (rulerScrub) {
5428
+ seekRuler(event.clientX);
5429
+ return;
5430
+ }
5431
+ if (!zoomDrag || props.meta.duration <= 0) return;
5432
+ const dx = event.clientX - zoomDrag.pointerX;
5433
+ if (!zoomDrag.moved && Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
5434
+ zoomDrag.moved = true;
5435
+ const nextZoom = clamp(zoomDrag.zoom * Math.exp(dx / ZOOM_DRAG_DISTANCE), 1, maxZoom.value);
5436
+ const duration = props.meta.duration / nextZoom;
5437
+ zoom.value = nextZoom;
5438
+ viewStart.value = clampViewStart(zoomDrag.anchorTime - zoomDrag.anchorRatio * duration, props.meta.duration, duration);
5439
+ },
5440
+ onPointerup: finishRuler,
5441
+ onPointercancel: finishRuler,
5442
+ onLostpointercapture: finishRuler
5443
+ }, [
5444
+ ...ticks.value.fine.map((time) => h19("div", { key: `fine:${time}`, class: "dialkit-timeline-tick dialkit-timeline-tick-fine", style: { left: `${(time - safeViewStart.value) * pxPerSecond.value}px` } })),
5445
+ ...ticks.value.medium.map((time) => h19("div", { key: `medium:${time}`, class: "dialkit-timeline-tick dialkit-timeline-tick-medium", style: { left: `${(time - safeViewStart.value) * pxPerSecond.value}px` } })),
5446
+ ...ticks.value.major.map((time) => h19("div", { key: time, class: "dialkit-timeline-tick", style: { left: `${(time - safeViewStart.value) * pxPerSecond.value}px` } }, [
5447
+ h19("span", { class: "dialkit-timeline-tick-label" }, formatRulerSeconds(time, ticks.value.majorStep))
5448
+ ]))
5449
+ ])
5450
+ ]),
5451
+ ...renderRows(),
5452
+ pxPerSecond.value > 0 ? h19(TimelinePlayheadFlag, {
5453
+ id: props.meta.id,
5454
+ duration: props.meta.duration,
5455
+ pxPerSecond: pxPerSecond.value,
5456
+ viewStart: safeViewStart.value,
5457
+ viewEnd: viewEnd.value,
5458
+ laneWidth: laneWidth.value,
5459
+ ruler: laneAreaRef.value ?? void 0,
5460
+ headerClearStart: flagClearRange.value.start,
5461
+ headerClearEnd: flagClearRange.value.end,
5462
+ onResetView: resetView
5463
+ }) : null
5464
+ ])]) : null,
5465
+ popover.value ? h19(ClipPopover, {
5466
+ panelId: props.meta.id,
5467
+ popover: popover.value,
5468
+ values: values.value,
5469
+ theme: props.theme,
5470
+ onClose: closePopover
5471
+ }) : null
5472
+ ]);
5473
+ }
5474
+ });
5475
+ function chevronIcon() {
5476
+ return h19("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2.5", "stroke-linecap": "round", "stroke-linejoin": "round" }, [
5477
+ h19("path", { d: ICON_CHEVRON })
5478
+ ]);
5479
+ }
5480
+ var ClipPopover = defineComponent19({
5481
+ props: {
5482
+ panelId: { type: String, required: true },
5483
+ popover: { type: Object, required: true },
5484
+ values: { type: Object, required: true },
5485
+ theme: { type: String, required: true },
5486
+ onClose: { type: Function, required: true }
5487
+ },
5488
+ setup(props) {
5489
+ const element = ref15(null);
5490
+ const naturalHeight = ref15(0);
5491
+ const viewport = ref15(readViewport());
5492
+ let observer;
5493
+ const measure = () => {
5494
+ if (element.value) naturalHeight.value = element.value.scrollHeight + 2;
5495
+ };
5496
+ const updateViewport = () => {
5497
+ viewport.value = readViewport();
5498
+ };
5499
+ const outside = (event) => {
5500
+ const target = event.target;
5501
+ if (element.value?.contains(target) || target.closest?.(".dialkit-timeline-clip") || target.closest?.(".dialkit-timeline-label")) return;
5502
+ props.onClose();
5503
+ };
5504
+ const keydown = (event) => {
5505
+ if (event.key === "Escape") props.onClose();
5506
+ };
5507
+ onMounted13(() => {
5508
+ measure();
5509
+ observer = new ResizeObserver(measure);
5510
+ if (element.value) observer.observe(element.value.querySelector(".dialkit-timeline-popover-body") ?? element.value);
5511
+ window.addEventListener("resize", updateViewport);
5512
+ window.visualViewport?.addEventListener("resize", updateViewport);
5513
+ window.visualViewport?.addEventListener("scroll", updateViewport);
5514
+ document.addEventListener("pointerdown", outside, true);
5515
+ document.addEventListener("keydown", keydown);
5516
+ });
5517
+ onUnmounted12(() => {
5518
+ observer?.disconnect();
5519
+ window.removeEventListener("resize", updateViewport);
5520
+ window.visualViewport?.removeEventListener("resize", updateViewport);
5521
+ window.visualViewport?.removeEventListener("scroll", updateViewport);
5522
+ document.removeEventListener("pointerdown", outside, true);
5523
+ document.removeEventListener("keydown", keydown);
5524
+ });
5525
+ return () => {
5526
+ const { clip, stepKey } = props.popover;
5527
+ let controls;
5528
+ let title;
5529
+ if (stepKey) {
5530
+ controls = getClipControls(props.panelId, `${clip.key}.${stepKey}`);
5531
+ if (stepKey === clip.stepKeys?.[0]) {
5532
+ const from = getControlAt(props.panelId, `${clip.key}.from`);
5533
+ if (from) {
5534
+ const index = controls.findIndex((control) => control.path === `${clip.key}.${stepKey}.to`);
5535
+ controls = index >= 0 ? [...controls.slice(0, index), from, ...controls.slice(index)] : [...controls, from];
5536
+ }
5537
+ }
5538
+ title = `${clip.label} \xB7 ${formatStepLabel(stepKey)}`;
5539
+ } else {
5540
+ controls = getClipControls(props.panelId, clip.key, clipPopoverExclusions(clip));
5541
+ title = clip.label;
5542
+ }
5543
+ if (controls.length === 0) return null;
5544
+ const target = stepKey ? `${clip.key}.${stepKey}` : clip.key;
5545
+ const durationMeta = getControlAt(props.panelId, `${target}.duration`);
5546
+ const durationValue = durationMeta ? props.values[durationMeta.path] : void 0;
5547
+ const transitionDuration = durationMeta?.type === "slider" && typeof durationValue === "number" ? {
5548
+ value: durationValue,
5549
+ onChange: (next) => DialStore.updateValue(props.panelId, durationMeta.path, next),
5550
+ min: Math.max(TIMELINE_MIN_CLIP_DURATION, durationMeta.min ?? 0),
5551
+ max: durationMeta.max,
5552
+ step: durationMeta.step
5553
+ } : void 0;
5554
+ const current = viewport.value;
5555
+ const right = current.offsetLeft + current.width;
5556
+ const bottom = current.offsetTop + current.height;
5557
+ const width = Math.min(POPOVER_WIDTH, Math.max(220, current.width - 24));
5558
+ const left = clamp(props.popover.anchor.left + props.popover.anchor.width / 2 - width / 2, current.offsetLeft + 12, Math.max(current.offsetLeft + 12, right - width - 12));
5559
+ const above = Math.max(0, props.popover.anchor.top - current.offsetTop - 22);
5560
+ const below = Math.max(0, bottom - props.popover.anchor.bottom - 22);
5561
+ const placeAbove = naturalHeight.value === 0 ? above >= below : naturalHeight.value <= above || naturalHeight.value > below && above >= below;
5562
+ const availableHeight = placeAbove ? above : below;
5563
+ const renderedHeight = Math.min(naturalHeight.value || availableHeight, availableHeight);
5564
+ const rawTop = placeAbove ? props.popover.anchor.top - 10 - renderedHeight : props.popover.anchor.bottom + 10;
5565
+ const top = clamp(rawTop, current.offsetTop + 12, Math.max(current.offsetTop + 12, bottom - renderedHeight - 12));
5566
+ return h19(Teleport4, { to: "body" }, [h19("div", { class: "dialkit-root", "data-theme": props.theme }, [
5567
+ h19("div", {
5568
+ ref: element,
5569
+ class: "dialkit-timeline-popover",
5570
+ "data-placement": placeAbove ? "above" : "below",
5571
+ style: { left: `${left}px`, top: `${top}px`, width: `${width}px`, maxHeight: `${availableHeight}px`, visibility: naturalHeight.value > 0 ? "visible" : "hidden" },
5572
+ role: "dialog",
5573
+ "aria-label": `Edit ${title}`
5574
+ }, [
5575
+ h19("div", { class: "dialkit-timeline-popover-header" }, [
5576
+ h19("span", { class: "dialkit-timeline-popover-title" }, title),
5577
+ h19("button", { class: "dialkit-timeline-popover-close", title: "Close editor", "aria-label": "Close editor", onClick: props.onClose }, [
5578
+ h19("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round" }, [h19("path", { d: "M6 6L18 18M18 6L6 18" })])
5579
+ ])
5580
+ ]),
5581
+ h19("div", { class: "dialkit-timeline-popover-body" }, [h19(ControlRenderer, {
5582
+ panelId: props.panelId,
5583
+ controls,
5584
+ values: timelinePopoverDisplayValues(props.values, clip.key, clip.stepKeys, stepKey),
5585
+ transitionDuration
5586
+ })])
5587
+ ])
5588
+ ])]);
5589
+ };
5590
+ }
5591
+ });
5592
+ function readViewport() {
5593
+ return {
5594
+ width: window.visualViewport?.width ?? window.innerWidth,
5595
+ height: window.visualViewport?.height ?? window.innerHeight,
5596
+ offsetLeft: window.visualViewport?.offsetLeft ?? 0,
5597
+ offsetTop: window.visualViewport?.offsetTop ?? 0
5598
+ };
5599
+ }
5600
+ function clipPopoverExclusions(clip) {
5601
+ return /* @__PURE__ */ new Set([...clip.stepKeys ?? [], ...clip.tracks?.map((track) => track.prop) ?? []]);
5602
+ }
5603
+ function getClipControls(panelId, path, exclusions) {
5604
+ const panel = DialStore.getPanel(panelId);
5605
+ const folder = panel ? findControl(panel.controls, path) : null;
5606
+ if (!folder?.children) return [];
5607
+ return folder.children.filter((control) => {
5608
+ const key = control.path.slice(path.length + 1);
5609
+ return key !== "at" && key !== "duration" && !exclusions?.has(key);
5610
+ });
5611
+ }
5612
+ function getControlAt(panelId, path) {
5613
+ const panel = DialStore.getPanel(panelId);
5614
+ return panel ? findControl(panel.controls, path) : null;
5615
+ }
5616
+ var TimelineClip = defineComponent19({
5617
+ props: {
5618
+ timelineId: { type: String, required: true },
5619
+ clip: { type: Object, required: true },
5620
+ at: { type: Number, required: true },
5621
+ duration: { type: Number, required: true },
5622
+ loop: { type: String, required: true },
5623
+ steps: Array,
5624
+ fixedDuration: { type: Boolean, required: true },
5625
+ composite: Boolean,
5626
+ baseAt: { type: Number, default: 0 },
5627
+ delayMode: Boolean,
5628
+ pxPerSecond: { type: Number, required: true },
5629
+ viewStart: { type: Number, required: true },
5630
+ timelineDuration: { type: Number, required: true },
5631
+ selected: { type: Boolean, required: true },
5632
+ selectedStepKey: String,
5633
+ onClick: { type: Function, required: true },
5634
+ onDrag: { type: Function, required: true }
5635
+ },
5636
+ setup(props) {
5637
+ const dragging = ref15(false);
5638
+ let drag = null;
5639
+ const finish = (event) => {
5640
+ const previous = drag;
5641
+ drag = null;
5642
+ dragging.value = false;
5643
+ if (previous && !previous.moved && event) {
5644
+ const anchor = previous.clickEl ?? event.currentTarget;
5645
+ props.onClick(props.clip, anchor.getBoundingClientRect(), previous.clickEl?.dataset.step);
5646
+ }
5647
+ };
5648
+ return () => {
5649
+ const width = Math.max(props.duration * props.pxPerSecond, 14);
5650
+ const isSteps = Boolean(props.steps?.length);
5651
+ const looping = props.loop === "repeat" && props.duration > 0;
5652
+ const resizable = props.duration > 0 && !props.fixedDuration && !props.composite;
5653
+ const durationText = `${props.fixedDuration && !props.composite ? "~" : ""}${formatSeconds(props.duration)}`;
5654
+ const ghosts = [];
5655
+ if (looping) {
5656
+ const first = Math.max(1, Math.floor((props.viewStart - props.at) / props.duration));
5657
+ for (let offset = 0; offset < 256; offset++) {
5658
+ const index = first + offset;
5659
+ const start = props.at + props.duration * index;
5660
+ if (start >= props.timelineDuration - 1e-6) break;
5661
+ const duration = Math.min(props.duration, props.timelineDuration - start);
5662
+ ghosts.push(h19("div", {
5663
+ key: `ghost:${index}`,
5664
+ class: "dialkit-timeline-clip-ghost",
5665
+ "data-steps": isSteps || void 0,
5666
+ "aria-hidden": "true",
5667
+ style: { left: `${(start - props.viewStart) * props.pxPerSecond + 1}px`, width: `${Math.max(1, duration * props.pxPerSecond - 2)}px`, background: props.clip.color }
5668
+ }, props.steps?.map((step) => h19("span", { class: "dialkit-timeline-clip-ghost-segment", style: { width: `${step.duration * props.pxPerSecond}px` } }))));
5669
+ }
5670
+ }
5671
+ let cumulative = 0;
5672
+ const boundaries = props.steps?.map((step) => cumulative += step.duration) ?? [];
5673
+ const children = [];
5674
+ if (props.composite) {
5675
+ if (width > 56) children.push(h19("span", { class: "dialkit-timeline-clip-duration" }, durationText));
5676
+ } else if (isSteps) {
5677
+ for (const step of props.steps ?? []) {
5678
+ const segmentWidth = step.duration * props.pxPerSecond;
5679
+ children.push(h19("div", {
5680
+ key: step.key ?? "step",
5681
+ class: "dialkit-timeline-clip-segment",
5682
+ "data-step": step.key,
5683
+ "data-selected": props.selectedStepKey === step.key || void 0,
5684
+ style: { width: `${segmentWidth}px` }
5685
+ }, segmentWidth > 52 ? [h19("span", { class: "dialkit-timeline-clip-duration" }, formatSeconds(step.duration))] : []));
5686
+ }
5687
+ (props.steps ?? []).forEach((step, index) => {
5688
+ if (!step.isPhysics) children.push(h19("div", { key: `boundary:${step.key}`, class: "dialkit-timeline-clip-handle", "data-boundary": index, style: { left: `${boundaries[index] * props.pxPerSecond - 4}px` } }));
5689
+ });
5690
+ if (!props.steps?.[0]?.isPhysics) children.push(h19("div", { class: "dialkit-timeline-clip-handle", "data-edge": "start" }));
5691
+ } else {
5692
+ if (resizable) children.push(h19("div", { class: "dialkit-timeline-clip-handle", "data-edge": "start" }));
5693
+ if (width > 56) children.push(h19("span", { class: "dialkit-timeline-clip-duration" }, durationText));
5694
+ if (resizable) children.push(h19("div", { class: "dialkit-timeline-clip-handle", "data-edge": "end" }));
5695
+ }
5696
+ const title = props.composite ? `${props.clip.label} \u2014 composite of its property tracks${looping ? " \xB7 repeats through timeline" : ""} \xB7 click to expand` : `${props.clip.label} \u2014 ${formatSeconds(props.at)} for ${durationText}${props.fixedDuration ? " (duration set by spring physics)" : ""}${looping ? " \xB7 repeats through timeline" : ""}${props.delayMode ? " \xB7 drag to phase-shift" : ""}`;
5697
+ return [...ghosts, h19("div", {
5698
+ class: "dialkit-timeline-clip",
5699
+ "data-steps": isSteps || void 0,
5700
+ "data-composite": props.composite || void 0,
5701
+ "data-selected": props.selected || void 0,
5702
+ "data-dragging": dragging.value || void 0,
5703
+ style: { left: `${(props.at - props.viewStart) * props.pxPerSecond}px`, width: `${width}px`, background: props.composite ? `${props.clip.color}80` : props.clip.color },
5704
+ title,
5705
+ onPointerdown: (event) => {
5706
+ if (event.shiftKey) return;
5707
+ event.stopPropagation();
5708
+ const target = event.target;
5709
+ let mode = "move";
5710
+ let boundaryIndex;
5711
+ if (target.dataset.boundary !== void 0) {
5712
+ mode = "boundary";
5713
+ boundaryIndex = Number(target.dataset.boundary);
5714
+ } else if (!props.fixedDuration) {
5715
+ const edge = target.dataset.edge;
5716
+ if (edge) mode = edge;
5717
+ }
5718
+ drag = {
5719
+ mode,
5720
+ boundaryIndex,
5721
+ pointerX: event.clientX,
5722
+ at: props.at,
5723
+ duration: props.duration,
5724
+ stepDurations: props.steps?.map((step) => step.duration),
5725
+ clickEl: target.closest?.("[data-step]"),
5726
+ moved: false
5727
+ };
5728
+ event.currentTarget.setPointerCapture(event.pointerId);
5729
+ },
5730
+ onPointermove: (event) => {
5731
+ if (!drag || props.pxPerSecond <= 0) return;
5732
+ const dx = event.clientX - drag.pointerX;
5733
+ if (!drag.moved) {
5734
+ if (Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
5735
+ drag.moved = true;
5736
+ dragging.value = true;
5737
+ props.onDrag();
5738
+ }
5739
+ const dt = dx / props.pxPerSecond;
5740
+ if (drag.mode === "boundary" && props.steps && drag.stepDurations) {
5741
+ const index = drag.boundaryIndex ?? 0;
5742
+ const others = drag.stepDurations.reduce((sum, duration, stepIndex) => stepIndex === index ? sum : sum + duration, 0);
5743
+ DialStore.updateValue(props.timelineId, `${props.clip.key}.${props.steps[index].key ?? ""}.duration`, clampStepResize(drag.stepDurations[index] + dt, drag.at, others, props.timelineDuration));
5744
+ } else if (drag.mode === "move") {
5745
+ if (props.delayMode) DialStore.updateValue(props.timelineId, `${props.clip.key}.delay`, clampTrackDelay(drag.at + dt - props.baseAt, props.baseAt, drag.duration, props.timelineDuration));
5746
+ else DialStore.updateValue(props.timelineId, `${props.clip.key}.at`, clampClipMove(drag.at + dt, drag.duration, props.timelineDuration));
5747
+ } else if (drag.mode === "end") {
5748
+ DialStore.updateValue(props.timelineId, `${props.clip.key}.duration`, clampClipResizeEnd(drag.duration + dt, drag.at, props.timelineDuration));
5749
+ } else if (props.steps && drag.stepDurations) {
5750
+ const next = clampClipResizeStart(Math.max(drag.at + dt, Math.max(props.baseAt, 0)), drag.at, drag.stepDurations[0]);
5751
+ DialStore.updateValues(props.timelineId, {
5752
+ [props.delayMode ? `${props.clip.key}.delay` : `${props.clip.key}.at`]: props.delayMode ? Math.max(0, next.at - props.baseAt) : next.at,
5753
+ [`${props.clip.key}.${props.steps[0].key ?? ""}.duration`]: next.duration
5754
+ });
5755
+ } else {
5756
+ const next = clampClipResizeStart(Math.max(drag.at + dt, Math.max(props.baseAt, 0)), drag.at, drag.duration);
5757
+ DialStore.updateValues(props.timelineId, {
5758
+ [props.delayMode ? `${props.clip.key}.delay` : `${props.clip.key}.at`]: props.delayMode ? Math.max(0, next.at - props.baseAt) : next.at,
5759
+ [`${props.clip.key}.duration`]: next.duration
5760
+ });
5761
+ }
5762
+ },
5763
+ onPointerup: finish,
5764
+ onPointercancel: () => finish(),
5765
+ onLostpointercapture: () => finish()
5766
+ }, children), looping ? h19("span", { class: "dialkit-timeline-loop-infinity", "aria-hidden": "true", title: "Repeats indefinitely" }, "\u221E") : null];
5767
+ };
5768
+ }
5769
+ });
5770
+
2829
5771
  // src/vue/components/ShortcutsMenu.ts
2830
- import { defineComponent as defineComponent17, h as h17, onUnmounted as onUnmounted10, ref as ref14, Teleport as Teleport4 } from "vue";
5772
+ import { defineComponent as defineComponent20, h as h20, onUnmounted as onUnmounted13, ref as ref16, Teleport as Teleport5 } from "vue";
2831
5773
  function formatShortcutKey(sc) {
2832
5774
  if (!sc.key) return "\u2014";
2833
5775
  const mod = sc.modifier === "alt" ? "\u2325" : sc.modifier === "shift" ? "\u21E7" : sc.modifier === "meta" ? "\u2318" : "";
@@ -2846,7 +5788,7 @@ function formatInteraction(sc) {
2846
5788
  return "scroll";
2847
5789
  }
2848
5790
  }
2849
- var ShortcutsMenu = defineComponent17({
5791
+ var ShortcutsMenu = defineComponent20({
2850
5792
  name: "DialKitShortcutsMenu",
2851
5793
  props: {
2852
5794
  panelId: {
@@ -2855,10 +5797,10 @@ var ShortcutsMenu = defineComponent17({
2855
5797
  }
2856
5798
  },
2857
5799
  setup(props) {
2858
- const isOpen = ref14(false);
2859
- const triggerRef = ref14(null);
2860
- const dropdownRef = ref14(null);
2861
- const pos = ref14({ top: 0, right: 0 });
5800
+ const isOpen = ref16(false);
5801
+ const triggerRef = ref16(null);
5802
+ const dropdownRef = ref16(null);
5803
+ const pos = ref16({ top: 0, right: 0 });
2862
5804
  const open = () => {
2863
5805
  const rect = triggerRef.value?.getBoundingClientRect();
2864
5806
  if (rect) {
@@ -2888,7 +5830,7 @@ var ShortcutsMenu = defineComponent17({
2888
5830
  mousedownHandler = null;
2889
5831
  }
2890
5832
  };
2891
- onUnmounted10(() => {
5833
+ onUnmounted13(() => {
2892
5834
  removeOutsideClickListener();
2893
5835
  });
2894
5836
  return () => {
@@ -2917,13 +5859,13 @@ var ShortcutsMenu = defineComponent17({
2917
5859
  removeOutsideClickListener();
2918
5860
  }
2919
5861
  return [
2920
- h17("button", {
5862
+ h20("button", {
2921
5863
  ref: triggerRef,
2922
5864
  class: "dialkit-shortcuts-trigger",
2923
5865
  onClick: toggle,
2924
5866
  title: "Keyboard shortcuts"
2925
5867
  }, [
2926
- h17("svg", {
5868
+ h20("svg", {
2927
5869
  viewBox: "0 0 24 24",
2928
5870
  fill: "none",
2929
5871
  stroke: "currentColor",
@@ -2931,16 +5873,16 @@ var ShortcutsMenu = defineComponent17({
2931
5873
  "stroke-linecap": "round",
2932
5874
  "stroke-linejoin": "round"
2933
5875
  }, [
2934
- h17("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
2935
- h17("path", { d: "M6 10H6.01" }),
2936
- h17("path", { d: "M10 10H10.01" }),
2937
- h17("path", { d: "M14 10H14.01" }),
2938
- h17("path", { d: "M18 10H18.01" }),
2939
- h17("path", { d: "M8 14H16" })
5876
+ h20("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }),
5877
+ h20("path", { d: "M6 10H6.01" }),
5878
+ h20("path", { d: "M10 10H10.01" }),
5879
+ h20("path", { d: "M14 10H14.01" }),
5880
+ h20("path", { d: "M18 10H18.01" }),
5881
+ h20("path", { d: "M8 14H16" })
2940
5882
  ])
2941
5883
  ]),
2942
- isOpen.value ? h17(Teleport4, { to: "body" }, [
2943
- h17("div", {
5884
+ isOpen.value ? h20(Teleport5, { to: "body" }, [
5885
+ h20("div", {
2944
5886
  ref: dropdownRef,
2945
5887
  class: "dialkit-root dialkit-shortcuts-dropdown",
2946
5888
  style: {
@@ -2949,19 +5891,19 @@ var ShortcutsMenu = defineComponent17({
2949
5891
  right: `${pos.value.right}px`
2950
5892
  }
2951
5893
  }, [
2952
- h17("div", { class: "dialkit-shortcuts-title" }, "Keyboard Shortcuts"),
2953
- h17(
5894
+ h20("div", { class: "dialkit-shortcuts-title" }, "Keyboard Shortcuts"),
5895
+ h20(
2954
5896
  "div",
2955
5897
  { class: "dialkit-shortcuts-list" },
2956
5898
  rows.map(
2957
- (row) => h17("div", { key: row.path, class: "dialkit-shortcuts-row" }, [
2958
- h17("span", { class: "dialkit-shortcuts-row-key" }, formatShortcutKey(row.shortcut)),
2959
- h17("span", { class: "dialkit-shortcuts-row-label" }, row.label),
2960
- h17("span", { class: "dialkit-shortcuts-row-mode" }, formatInteraction(row.shortcut))
5899
+ (row) => h20("div", { key: row.path, class: "dialkit-shortcuts-row" }, [
5900
+ h20("span", { class: "dialkit-shortcuts-row-key" }, formatShortcutKey(row.shortcut)),
5901
+ h20("span", { class: "dialkit-shortcuts-row-label" }, row.label),
5902
+ h20("span", { class: "dialkit-shortcuts-row-mode" }, formatInteraction(row.shortcut))
2961
5903
  ])
2962
5904
  )
2963
5905
  ),
2964
- h17("div", { class: "dialkit-shortcuts-hint" }, "See pill badges on controls for keys")
5906
+ h20("div", { class: "dialkit-shortcuts-hint" }, "See pill badges on controls for keys")
2965
5907
  ])
2966
5908
  ]) : null
2967
5909
  ];
@@ -2970,8 +5912,8 @@ var ShortcutsMenu = defineComponent17({
2970
5912
  });
2971
5913
 
2972
5914
  // src/vue/components/ButtonGroup.ts
2973
- import { defineComponent as defineComponent18, h as h18 } from "vue";
2974
- var ButtonGroup = defineComponent18({
5915
+ import { defineComponent as defineComponent21, h as h21 } from "vue";
5916
+ var ButtonGroup = defineComponent21({
2975
5917
  name: "DialKitButtonGroup",
2976
5918
  props: {
2977
5919
  buttons: {
@@ -2980,11 +5922,11 @@ var ButtonGroup = defineComponent18({
2980
5922
  }
2981
5923
  },
2982
5924
  setup(props) {
2983
- return () => h18(
5925
+ return () => h21(
2984
5926
  "div",
2985
5927
  { class: "dialkit-button-group" },
2986
5928
  props.buttons.map(
2987
- (button) => h18("button", { class: "dialkit-button", onClick: button.onClick }, button.label)
5929
+ (button) => h21("button", { class: "dialkit-button", onClick: button.onClick }, button.label)
2988
5930
  )
2989
5931
  );
2990
5932
  }
@@ -2992,8 +5934,10 @@ var ButtonGroup = defineComponent18({
2992
5934
  export {
2993
5935
  ButtonGroup,
2994
5936
  ColorControl,
5937
+ ControlRenderer,
2995
5938
  DialRoot,
2996
5939
  DialStore,
5940
+ DialTimeline,
2997
5941
  EasingVisualization,
2998
5942
  Folder,
2999
5943
  PresetManager,
@@ -3008,6 +5952,8 @@ export {
3008
5952
  Toggle,
3009
5953
  TransitionControl,
3010
5954
  useDialKit,
5955
+ useDialKitController,
5956
+ useDialTimeline,
3011
5957
  useShortcutContext,
3012
5958
  vDialKit
3013
5959
  };