vanilla-jui 1.4.5 → 1.4.7

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 (103) hide show
  1. package/README.md +6 -0
  2. package/dist/index.d.ts +1273 -1896
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +21 -2
  7. package/src/components/accordion.ts +297 -216
  8. package/src/components/flow.ts +222 -258
  9. package/src/components/form.ts +44 -102
  10. package/src/components/menu.ts +268 -356
  11. package/src/components/modal.ts +181 -641
  12. package/src/components/offcanvas.ts +225 -106
  13. package/src/components/pagination.ts +296 -326
  14. package/src/components/sticky.ts +55 -54
  15. package/src/components/swiper.ts +237 -247
  16. package/src/components/tabs.ts +278 -271
  17. package/src/components/toc.ts +42 -46
  18. package/src/core/Component.ts +69 -47
  19. package/src/css/accordion.css +2 -2
  20. package/src/css/animation.css +1 -5
  21. package/src/css/avatar.css +1 -1
  22. package/src/css/badge.css +1 -1
  23. package/src/css/breadcrumb.css +9 -10
  24. package/src/css/button.css +1 -1
  25. package/src/css/card.css +2 -2
  26. package/src/css/color.css +974 -1002
  27. package/src/css/color_p3.css +974 -1004
  28. package/src/css/components.css +1 -1
  29. package/src/css/content.css +72 -63
  30. package/src/css/drop.css +1 -1
  31. package/src/css/editor.css +14 -14
  32. package/src/css/flow.css +4 -14
  33. package/src/css/font.css +5 -4
  34. package/src/css/form.css +76 -52
  35. package/src/css/gradient.css +299 -222
  36. package/src/css/icon.css +57 -57
  37. package/src/css/index.css +1 -1
  38. package/src/css/layout.css +13 -10
  39. package/src/css/loading.css +60 -12
  40. package/src/css/menu.css +262 -188
  41. package/src/css/modal.css +1 -1
  42. package/src/css/offcanvas.css +9 -13
  43. package/src/css/pagination.css +5 -1
  44. package/src/css/popup.css +1 -1
  45. package/src/css/skeleton.css +3 -2
  46. package/src/css/status.css +2 -3
  47. package/src/css/swiper.css +1 -1
  48. package/src/css/table.css +1 -1
  49. package/src/css/tabs.css +12 -11
  50. package/src/css/tag.css +1 -1
  51. package/src/css/theme-palette.css +19 -5
  52. package/src/css/themes/default/accordion.css +61 -61
  53. package/src/css/themes/default/avatar.css +54 -54
  54. package/src/css/themes/default/badge.css +62 -62
  55. package/src/css/themes/default/breadcrumb.css +23 -23
  56. package/src/css/themes/default/button.css +203 -203
  57. package/src/css/themes/default/content.css +53 -53
  58. package/src/css/themes/default/drop.css +10 -10
  59. package/src/css/themes/default/editor.css +9 -9
  60. package/src/css/themes/default/index.css +1 -1
  61. package/src/css/themes/default/loading.css +26 -20
  62. package/src/css/themes/default/menu.css +75 -11
  63. package/src/css/themes/default/modal.css +32 -36
  64. package/src/css/themes/default/pagination.css +7 -7
  65. package/src/css/themes/default/popup.css +6 -6
  66. package/src/css/themes/default/swiper.css +32 -32
  67. package/src/css/themes/default/table.css +16 -16
  68. package/src/css/themes/default/tabs.css +30 -30
  69. package/src/css/themes/default/tag.css +71 -71
  70. package/src/css/themes/default/theme-palette.css +11 -11
  71. package/src/css/themes/default/tip.css +42 -42
  72. package/src/css/themes/default/toast.css +60 -60
  73. package/src/css/themes/default/toolbar.css +23 -29
  74. package/src/css/themes/default/tooltip.css +43 -13
  75. package/src/css/themes/index.css +1 -1
  76. package/src/css/tip.css +2 -2
  77. package/src/css/toast.css +3 -3
  78. package/src/css/tokens.css +12 -17
  79. package/src/css/toolbar.css +9 -10
  80. package/src/css/tooltip.css +3 -2
  81. package/src/index.ts +22 -19
  82. package/src/{components → primitives}/drop.ts +58 -49
  83. package/src/{components → primitives}/icons.ts +1 -1
  84. package/src/primitives/loading.ts +23 -0
  85. package/src/primitives/parabola.ts +388 -0
  86. package/src/primitives/popup.ts +61 -0
  87. package/src/{components → primitives}/theme.ts +89 -57
  88. package/src/{components → primitives}/toast.ts +18 -25
  89. package/src/{components → primitives}/tooltip.ts +100 -25
  90. package/src/utilities/browser.ts +23 -49
  91. package/src/utilities/class-name.ts +5 -0
  92. package/src/utilities/dom.ts +14 -32
  93. package/src/utilities/function.ts +4 -0
  94. package/src/utilities/http.ts +7 -7
  95. package/src/utilities/id.ts +57 -0
  96. package/src/utilities/object.ts +5 -0
  97. package/src/utilities/timer.ts +38 -0
  98. package/src/utilities/types.ts +420 -0
  99. package/src/{components → validation}/validator.ts +64 -53
  100. package/src/components/parabola.ts +0 -335
  101. package/src/utilities/cache.ts +0 -57
  102. package/src/utilities/core.ts +0 -614
  103. package/src/utilities/storage.ts +0 -46
@@ -1,22 +1,18 @@
1
1
  import { jsx } from 'vanilla-signal';
2
2
 
3
- import {
4
- type ResolveSchema,
5
- randomId,
6
- resolveProps,
7
- } from '../utilities/core.ts';
8
3
  import {
9
4
  type DOMReference,
10
5
  type RenderableContent,
11
6
  isNode,
12
- isRenderableContent,
13
7
  normalizeContentNodes,
14
8
  resolveElement,
15
9
  } from '../utilities/dom.ts';
16
10
  import { createEventManager, type IEventManager } from '../utilities/events.ts';
11
+ import { randomId } from '../utilities/id.ts';
12
+ import { type ResolveSchema, resolveProps } from '../utilities/types.ts';
17
13
 
18
- export type DropMode = 'hover' | 'click';
19
- export type DropPosition =
14
+ type DropMode = 'hover' | 'click';
15
+ type DropPosition =
20
16
  | 'auto'
21
17
  | 'top-left'
22
18
  | 'top-center'
@@ -27,30 +23,30 @@ export type DropPosition =
27
23
  | 'left'
28
24
  | 'right';
29
25
 
30
- export interface DropClassNames {
26
+ interface DropClassNames {
31
27
  root: string;
32
28
  container: string;
33
29
  }
34
30
 
35
- export type DropClassNameConfig = Partial<DropClassNames>;
31
+ type DropClassNameConfig = Partial<DropClassNames>;
36
32
 
37
- export interface DropDelay {
33
+ interface DropDelay {
38
34
  show?: number;
39
35
  hide?: number;
40
36
  }
41
37
 
42
- export interface DropProps extends Record<string, unknown> {
38
+ interface DropProps extends Record<string, unknown> {
43
39
  name?: string | null;
44
40
  mode?: DropMode;
45
41
  position?: DropPosition;
46
42
  offset?: number;
47
- content?: RenderableContent<Drop>;
43
+ content?: RenderableContent<DropInstance>;
48
44
  className?: DropClassNameConfig;
49
45
  id?: string | null;
50
46
  delay?: number | DropDelay;
51
47
  hoverIntent?: boolean;
52
- onShown?: ((drop: Drop) => void | Promise<void>) | null;
53
- onHidden?: ((drop: Drop) => void | Promise<void>) | null;
48
+ onShown?: ((drop: DropInstance) => void | Promise<void>) | null;
49
+ onHidden?: ((drop: DropInstance) => void | Promise<void>) | null;
54
50
  }
55
51
 
56
52
  interface ResolvedDropProps extends Record<string, unknown> {
@@ -58,7 +54,7 @@ interface ResolvedDropProps extends Record<string, unknown> {
58
54
  mode: DropMode;
59
55
  position: DropPosition;
60
56
  offset: number;
61
- content: RenderableContent<Drop>;
57
+ content: RenderableContent<DropInstance>;
62
58
  className: DropClassNames;
63
59
  id: string;
64
60
  delay: number | DropDelay;
@@ -76,6 +72,23 @@ interface DropCleanup {
76
72
  events: IEventManager;
77
73
  }
78
74
 
75
+ interface DropDom {
76
+ root: HTMLElement | null;
77
+ }
78
+
79
+ interface DropInstance {
80
+ target: Element | null;
81
+ props: Record<string, unknown> | null;
82
+ dom: DropDom;
83
+ isVisible: boolean;
84
+ delayShow: number;
85
+ delayHide: number;
86
+ show(useDelay?: boolean): void;
87
+ hide(useDelay?: boolean): void;
88
+ toggle(): void;
89
+ destroy(): void;
90
+ }
91
+
79
92
  interface HoverIntentData {
80
93
  x: number;
81
94
  y: number;
@@ -108,13 +121,11 @@ const DROP_PROPS_SCHEMA = {
108
121
  offset: {
109
122
  default: 10,
110
123
  type: 'number',
111
- validate: (value: unknown) => typeof value === 'number' && value >= 0,
112
- message: 'expects a positive number or 0.',
124
+ min: 0,
113
125
  },
114
126
  content: {
115
127
  default: '',
116
- validate: isRenderableContent,
117
- message: 'expects string, Node, array, function or null.',
128
+ type: 'renderable',
118
129
  },
119
130
  className: {
120
131
  default: DEFAULT_CLASS_NAMES,
@@ -149,7 +160,7 @@ function normalizeProps(input: DropProps): ResolvedDropProps {
149
160
  mode: props.mode as DropMode,
150
161
  position: props.position as DropPosition,
151
162
  offset: props.offset as number,
152
- content: props.content as RenderableContent<Drop>,
163
+ content: props.content as RenderableContent<DropInstance>,
153
164
  className: props.className as DropClassNames,
154
165
  id: props.id as string,
155
166
  delay: props.delay as number | DropDelay,
@@ -177,10 +188,10 @@ function normalizeDelay(delay: number | DropDelay): Required<DropDelay> {
177
188
  *
178
189
  * 可用于菜单、提示、下拉面板等场景,支持点击或 hover 触发,并自动计算视口内位置。
179
190
  */
180
- export class Drop {
191
+ class Drop implements DropInstance {
181
192
  target: Element | null;
182
193
  props: ResolvedDropProps | null;
183
- root: HTMLElement | null;
194
+ dom: DropDom;
184
195
  isVisible: boolean;
185
196
  cleanup: DropCleanup | null;
186
197
  delayShow: number;
@@ -193,7 +204,7 @@ export class Drop {
193
204
  constructor(element: DOMReference, options: DropProps = {}) {
194
205
  this.target = resolveElement(element);
195
206
  this.props = normalizeProps(options);
196
- this.root = null;
207
+ this.dom = { root: null };
197
208
  this.isVisible = false;
198
209
  this.cleanup = null;
199
210
  this.delayShow = 0;
@@ -230,7 +241,7 @@ export class Drop {
230
241
  children: normalizeContentNodes(content, this),
231
242
  });
232
243
 
233
- this.root = jsx('div', {
244
+ this.dom.root = jsx('div', {
234
245
  className: className.root,
235
246
  id,
236
247
  'data-drop': name || randomId(),
@@ -271,13 +282,10 @@ export class Drop {
271
282
  }
272
283
 
273
284
  private bindRootEvents(): void {
274
- if (!this.root || this.props?.mode !== 'hover' || !this.cleanup) return;
275
- this.cleanup.events.on('root:enter', this.root, 'mouseenter', () =>
276
- this.show()
277
- );
278
- this.cleanup.events.on('root:leave', this.root, 'mouseleave', () =>
279
- this.hide()
280
- );
285
+ const { root } = this.dom;
286
+ if (!root || this.props?.mode !== 'hover' || !this.cleanup) return;
287
+ this.cleanup.events.on('root:enter', root, 'mouseenter', () => this.show());
288
+ this.cleanup.events.on('root:leave', root, 'mouseleave', () => this.hide());
281
289
  }
282
290
 
283
291
  private unbindRootEvents(): void {
@@ -331,10 +339,10 @@ export class Drop {
331
339
  }
332
340
 
333
341
  private setPosition(): void {
334
- if (!this.target || !this.root || !this.props) return;
342
+ if (!this.target || !this.dom.root || !this.props) return;
335
343
 
336
344
  const rect = this.target.getBoundingClientRect();
337
- const drop = this.root;
345
+ const drop = this.dom.root;
338
346
  const { offset, position } = this.props;
339
347
 
340
348
  drop.style.visibility = 'hidden';
@@ -398,9 +406,9 @@ export class Drop {
398
406
 
399
407
  private docClick(event: Event): void {
400
408
  if (!(event.target instanceof Node)) return;
401
- if (!this.root || !this.target) return;
409
+ if (!this.dom.root || !this.target) return;
402
410
  if (
403
- !this.root.contains(event.target) &&
411
+ !this.dom.root.contains(event.target) &&
404
412
  !this.target.contains(event.target)
405
413
  ) {
406
414
  this.hide();
@@ -408,21 +416,22 @@ export class Drop {
408
416
  }
409
417
 
410
418
  private exec(visible: boolean): void {
411
- if (!this.root || !this.props) return;
419
+ const { root } = this.dom;
420
+ if (!root || !this.props) return;
412
421
 
413
422
  if (visible) {
414
- if (!this.root.parentNode) document.body.appendChild(this.root);
423
+ if (!root.parentNode) document.body.appendChild(root);
415
424
  this.bindRootEvents();
416
425
  this.setPosition();
417
426
  } else {
418
427
  this.unbindRootEvents();
419
- this.root.style.top = '';
420
- this.root.style.left = '';
421
- this.root.remove();
428
+ root.style.top = '';
429
+ root.style.left = '';
430
+ root.remove();
422
431
  }
423
432
 
424
- this.root.setAttribute('aria-hidden', visible ? 'false' : 'true');
425
- this.root.setAttribute('aria-expanded', visible ? 'true' : 'false');
433
+ root.setAttribute('aria-hidden', visible ? 'false' : 'true');
434
+ root.setAttribute('aria-expanded', visible ? 'true' : 'false');
426
435
 
427
436
  this.isVisible = visible;
428
437
  }
@@ -467,10 +476,10 @@ export class Drop {
467
476
  if (this.timer.hide) clearTimeout(this.timer.hide);
468
477
 
469
478
  this.unbindEvents();
470
- this.root?.remove();
479
+ this.dom.root?.remove();
471
480
 
472
481
  this.props = null;
473
- this.root = null;
482
+ this.dom.root = null;
474
483
  this.target = null;
475
484
  this.timer = { show: null, hide: null };
476
485
  this.cleanup?.events.clear();
@@ -480,8 +489,8 @@ export class Drop {
480
489
  }
481
490
 
482
491
  export function createDrop(
483
- container: DOMReference,
484
- input: DropProps = {}
485
- ): Drop {
486
- return new Drop(container, input);
492
+ element: DOMReference,
493
+ props: DropProps = {}
494
+ ): DropInstance {
495
+ return new Drop(element, props);
487
496
  }
@@ -20,7 +20,7 @@ function assertIconName(name: IconName): void {
20
20
 
21
21
  function svgMarkup(name: IconName): string {
22
22
  assertIconName(name);
23
- return `<svg xmlns="${SVG_NS}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${defaultSvgObjects[name]}</svg>`;
23
+ return `<svg xmlns="${SVG_NS}" viewBox="0 0 24 24" fill="currentColor">${defaultSvgObjects[name]}</svg>`;
24
24
  }
25
25
 
26
26
  function isSVGElement(value: Element | null): value is SVGElement {
@@ -0,0 +1,23 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import { icon } from './icons.ts';
4
+ /**
5
+ * 创建通用加载状态节点
6
+ * 定位 absolute 撑满父元素 居中 背景模糊滤镜
7
+ * @returns {HTMLElement}
8
+ */
9
+ export function createLoading(): HTMLDivElement {
10
+ return jsx('div', {
11
+ 'aria-live': 'polite',
12
+ style: {
13
+ display: 'flex',
14
+ justifyContent: 'center',
15
+ alignItems: 'center',
16
+ position: 'absolute',
17
+ width: '100%',
18
+ height: '100%',
19
+ backdropFilter: 'blur(4px)',
20
+ },
21
+ children: icon('loader', { width: 24, className: 'animate-spin' }),
22
+ });
23
+ }
@@ -0,0 +1,388 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import { type DOMReference, resolveElement } from '../utilities/dom.ts';
4
+ import { randomId } from '../utilities/id.ts';
5
+ import { timer } from '../utilities/timer.ts';
6
+ import { type ResolveSchema, resolveProps } from '../utilities/types.ts';
7
+
8
+ type ParabolaDirection =
9
+ | 'center'
10
+ | 'top-right'
11
+ | 'top-left'
12
+ | 'bottom-right'
13
+ | 'bottom-left';
14
+
15
+ interface ParabolaBallProps {
16
+ color: string;
17
+ size: string;
18
+ }
19
+
20
+ interface ParabolaClassNames {
21
+ ball: string;
22
+ }
23
+
24
+ type ParabolaClassNameConfig = Partial<ParabolaClassNames>;
25
+
26
+ interface ParabolaProps extends Record<string, unknown> {
27
+ ball?: ParabolaBallProps;
28
+ className?: ParabolaClassNameConfig;
29
+ from?: DOMReference;
30
+ to?: DOMReference;
31
+ direction?: ParabolaDirection;
32
+ showDelay?: number;
33
+ onShow?: ((parabola: ParabolaInstance) => void) | null;
34
+ onHidden?: ((parabola: ParabolaInstance) => void) | null;
35
+ }
36
+
37
+ interface ResolvedParabolaProps extends Record<string, unknown> {
38
+ ball: ParabolaBallProps;
39
+ className: ParabolaClassNames;
40
+ from: DOMReference;
41
+ to: DOMReference;
42
+ direction: ParabolaDirection;
43
+ showDelay: number;
44
+ onShow: ((parabola: ParabolaInstance) => void) | null;
45
+ onHidden: ((parabola: ParabolaInstance) => void) | null;
46
+ }
47
+
48
+ interface ParabolaDOM {
49
+ root: HTMLElement | null;
50
+ from: Element | null;
51
+ to: Element | null;
52
+ balls: Set<HTMLElement>;
53
+ }
54
+
55
+ interface ParabolaRuntime {
56
+ destroyed: boolean;
57
+ }
58
+
59
+ interface ParabolaCache {
60
+ delays: Map<string, () => void>;
61
+ }
62
+
63
+ interface ParabolaPath {
64
+ startX: number;
65
+ startY: number;
66
+ endX: number;
67
+ endY: number;
68
+ }
69
+
70
+ interface ParabolaInstance {
71
+ props: ResolvedParabolaProps;
72
+ dom: ParabolaDOM;
73
+ runtime: ParabolaRuntime;
74
+ show(): Promise<boolean>;
75
+ destroy(): void;
76
+ }
77
+
78
+ const DEFAULT_BALL: ParabolaBallProps = {
79
+ color: 'var(--tone-solid)',
80
+ size: '12px',
81
+ };
82
+
83
+ const DEFAULT_CLASS_NAMES: ParabolaClassNames = {
84
+ ball: 'parabola-ball',
85
+ };
86
+
87
+ const ELEMENT_REF_RULE = {
88
+ types: ['string', 'Node', 'array', 'null', 'undefined'],
89
+ };
90
+
91
+ const PARABOLA_PROPS_SCHEMA = {
92
+ ball: {
93
+ default: DEFAULT_BALL,
94
+ type: 'plainObject',
95
+ normalize: (value: unknown) => ({
96
+ ...DEFAULT_BALL,
97
+ ...(value && typeof value === 'object' ? value : {}),
98
+ }),
99
+ shape: {
100
+ color: 'string',
101
+ size: 'string',
102
+ },
103
+ },
104
+ className: {
105
+ default: DEFAULT_CLASS_NAMES,
106
+ type: 'object',
107
+ normalize: (value: unknown) => ({
108
+ ...DEFAULT_CLASS_NAMES,
109
+ ...(value && typeof value === 'object' ? value : {}),
110
+ }),
111
+ },
112
+ from: { default: null, ...ELEMENT_REF_RULE },
113
+ to: { default: null, ...ELEMENT_REF_RULE },
114
+ direction: {
115
+ default: 'center',
116
+ type: 'string',
117
+ enum: ['center', 'top-right', 'top-left', 'bottom-right', 'bottom-left'],
118
+ },
119
+ showDelay: { default: 0, type: 'number' },
120
+ onShow: { default: null, types: ['function', 'null'] },
121
+ onHidden: { default: null, types: ['function', 'null'] },
122
+ } satisfies ResolveSchema<ParabolaProps>;
123
+
124
+ function normalizeProps(input: ParabolaProps): ResolvedParabolaProps {
125
+ const props = resolveProps(input, PARABOLA_PROPS_SCHEMA, 'Parabola.props');
126
+ return {
127
+ ball: props.ball as ParabolaBallProps,
128
+ className: props.className as ParabolaClassNames,
129
+ from: props.from as DOMReference,
130
+ to: props.to as DOMReference,
131
+ direction: props.direction as ParabolaDirection,
132
+ showDelay: props.showDelay as number,
133
+ onShow: props.onShow as ResolvedParabolaProps['onShow'],
134
+ onHidden: props.onHidden as ResolvedParabolaProps['onHidden'],
135
+ };
136
+ }
137
+
138
+ class Parabola implements ParabolaInstance {
139
+ props: ResolvedParabolaProps;
140
+ dom: ParabolaDOM;
141
+ runtime: ParabolaRuntime;
142
+ cache: ParabolaCache;
143
+
144
+ constructor(props: ParabolaProps = {}) {
145
+ this.props = normalizeProps(props);
146
+ this.dom = {
147
+ root: null,
148
+ from: resolveElement(this.props.from),
149
+ to: resolveElement(this.props.to),
150
+ balls: new Set(),
151
+ };
152
+ this.runtime = {
153
+ destroyed: false,
154
+ };
155
+ this.cache = {
156
+ delays: new Map(),
157
+ };
158
+
159
+ this.createRoot();
160
+ }
161
+
162
+ private createRoot(): void {
163
+ if (this.runtime.destroyed) return;
164
+
165
+ if (this.dom.root) {
166
+ if (!this.dom.root.isConnected) document.body.appendChild(this.dom.root);
167
+ return;
168
+ }
169
+
170
+ this.dom.root = jsx('div', {
171
+ 'data-parabola': 'root',
172
+ style: {
173
+ position: 'fixed',
174
+ inset: '0',
175
+ pointerEvents: 'none',
176
+ zIndex: '9999',
177
+ },
178
+ }) as HTMLElement;
179
+ document.body.appendChild(this.dom.root);
180
+ }
181
+
182
+ private createBall(path: ParabolaPath): HTMLElement | null {
183
+ if (this.runtime.destroyed) return null;
184
+
185
+ this.createRoot();
186
+ if (!this.dom.root) return null;
187
+
188
+ const { color, size } = this.props.ball;
189
+
190
+ const ball = jsx('div', {
191
+ className: this.props.className.ball,
192
+ 'data-parabola': 'ball',
193
+ style: {
194
+ backgroundColor: color,
195
+ width: size,
196
+ height: size,
197
+ position: 'fixed',
198
+ borderRadius: '50%',
199
+ transform: 'translate(-50%, -50%)',
200
+ pointerEvents: 'none',
201
+ zIndex: '9999',
202
+ opacity: '1',
203
+ left: `${path.startX}px`,
204
+ top: `${path.startY}px`,
205
+ },
206
+ }) as HTMLElement;
207
+ this.dom.root.appendChild(ball);
208
+ this.dom.balls.add(ball);
209
+
210
+ return ball;
211
+ }
212
+
213
+ private resolveTargets(): void {
214
+ this.dom.from = resolveElement(this.props.from);
215
+ this.dom.to = resolveElement(this.props.to);
216
+ }
217
+
218
+ private calculatePath(): ParabolaPath | null {
219
+ const fromRect = this.dom.from?.getBoundingClientRect();
220
+ const toRect = this.dom.to?.getBoundingClientRect();
221
+
222
+ if (!fromRect || !toRect) return null;
223
+
224
+ const { left: fL, top: fT, width: fW, height: fH } = fromRect;
225
+ const { left: tL, top: tT, width: tW, height: tH } = toRect;
226
+
227
+ let startX: number;
228
+ let startY: number;
229
+ switch (this.props.direction) {
230
+ case 'top-left':
231
+ startX = fL + fW * 0.2;
232
+ startY = fT + fH * 0.2;
233
+ break;
234
+ case 'top-right':
235
+ startX = fL + fW * 0.8;
236
+ startY = fT + fH * 0.2;
237
+ break;
238
+ case 'bottom-left':
239
+ startX = fL + fW * 0.2;
240
+ startY = fT + fH * 0.8;
241
+ break;
242
+ case 'bottom-right':
243
+ startX = fL + fW * 0.8;
244
+ startY = fT + fH * 0.8;
245
+ break;
246
+ default:
247
+ startX = fL + fW / 2;
248
+ startY = fT + fH / 2;
249
+ }
250
+
251
+ const endX = tL + tW / 2;
252
+ const endY = tT + tH / 2;
253
+
254
+ return { startX, startY, endX, endY };
255
+ }
256
+
257
+ private easeOutCubic(t: number): number {
258
+ return 1 - (1 - t) ** 3;
259
+ }
260
+
261
+ private animate(
262
+ ball: HTMLElement,
263
+ startX: number,
264
+ startY: number,
265
+ endX: number,
266
+ endY: number,
267
+ duration = 800
268
+ ): void {
269
+ if (!ball.isConnected) return;
270
+
271
+ const startTime = performance.now();
272
+ const deltaX = endX - startX;
273
+ const deltaY = endY - startY;
274
+ const peakOffset = -100;
275
+
276
+ const step = (currentTime: number): void => {
277
+ if (!ball.isConnected) return;
278
+
279
+ const elapsed = currentTime - startTime;
280
+ const progress = Math.min(elapsed / duration, 1);
281
+ const eased = this.easeOutCubic(progress);
282
+
283
+ const currentX = startX + deltaX * eased;
284
+ const currentY =
285
+ startY + peakOffset * Math.sin(Math.PI * eased) + deltaY * eased;
286
+
287
+ const opacity = 1 - progress;
288
+
289
+ ball.style.left = `${currentX}px`;
290
+ ball.style.top = `${currentY}px`;
291
+ ball.style.opacity = `${opacity}`;
292
+ ball.style.transform = `translate(-50%, -50%) scale(${1 - eased * 0.3})`;
293
+
294
+ if (progress < 1) {
295
+ requestAnimationFrame(step);
296
+ } else {
297
+ this.removeBall(ball, true);
298
+ }
299
+ };
300
+
301
+ requestAnimationFrame(step);
302
+ }
303
+
304
+ private startDelay(callback: () => void, onCancel: () => void): void {
305
+ const key = `parabola-show-${randomId()}`;
306
+ this.cache.delays.set(key, onCancel);
307
+
308
+ timer.start(key, this.props.showDelay, () => {
309
+ this.cache.delays.delete(key);
310
+ callback();
311
+ });
312
+ }
313
+
314
+ private cancelDelays(): void {
315
+ for (const [key, onCancel] of this.cache.delays) {
316
+ timer.cancel(key);
317
+ onCancel();
318
+ }
319
+ this.cache.delays.clear();
320
+ }
321
+
322
+ private removeBall(ball: HTMLElement, notify: boolean): void {
323
+ if (!this.dom.balls.has(ball)) return;
324
+
325
+ ball.remove();
326
+ this.dom.balls.delete(ball);
327
+
328
+ if (notify) this.props.onHidden?.(this);
329
+ this.removeRootIfIdle();
330
+ }
331
+
332
+ private removeRootIfIdle(): void {
333
+ if (!this.runtime.destroyed || this.dom.balls.size > 0) return;
334
+
335
+ this.dom.root?.remove();
336
+ this.dom.root = null;
337
+ }
338
+
339
+ show(): Promise<boolean> {
340
+ if (this.runtime.destroyed) return Promise.resolve(false);
341
+
342
+ return new Promise((resolve) => {
343
+ this.startDelay(
344
+ () => {
345
+ if (this.runtime.destroyed) {
346
+ resolve(false);
347
+ return;
348
+ }
349
+
350
+ this.resolveTargets();
351
+
352
+ const path = this.calculatePath();
353
+ if (!path) {
354
+ resolve(false);
355
+ return;
356
+ }
357
+
358
+ const ball = this.createBall(path);
359
+ if (!ball) {
360
+ resolve(false);
361
+ return;
362
+ }
363
+
364
+ this.props.onShow?.(this);
365
+
366
+ this.animate(ball, path.startX, path.startY, path.endX, path.endY);
367
+ resolve(true);
368
+ },
369
+ () => resolve(false)
370
+ );
371
+ });
372
+ }
373
+
374
+ destroy(): void {
375
+ if (this.runtime.destroyed) return;
376
+
377
+ this.cancelDelays();
378
+
379
+ this.dom.from = null;
380
+ this.dom.to = null;
381
+ this.runtime.destroyed = true;
382
+ this.removeRootIfIdle();
383
+ }
384
+ }
385
+
386
+ export function createParabola(props: ParabolaProps = {}): ParabolaInstance {
387
+ return new Parabola(props);
388
+ }