@x33025/sveltely 0.0.49 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AsyncButton.svelte +33 -14
- package/dist/components/AsyncButton.svelte.d.ts +2 -0
- package/dist/components/Popover/Popover.svelte +80 -132
- package/dist/components/Popover/Popover.svelte.d.ts +3 -2
- package/dist/components/Popover/PopoverDebugOverlay.svelte +163 -0
- package/dist/components/Popover/PopoverDebugOverlay.svelte.d.ts +3 -0
- package/dist/components/Popover/index.d.ts +1 -0
- package/dist/components/Popover/index.js +1 -0
- package/dist/components/Popover/registry.svelte.d.ts +18 -0
- package/dist/components/Popover/registry.svelte.js +58 -0
- package/dist/components/Tooltip.svelte +63 -11
- package/dist/components/Tooltip.svelte.d.ts +2 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/style/index.css +34 -15
- package/dist/style.css +51 -30
- package/dist/utils/positioning.d.ts +36 -0
- package/dist/utils/positioning.js +112 -0
- package/package.json +1 -1
- package/dist/components/Dropdown/Dropdown.svelte +0 -260
- package/dist/components/Dropdown/Dropdown.svelte.d.ts +0 -12
- package/dist/components/Dropdown/index.d.ts +0 -1
- package/dist/components/Dropdown/index.js +0 -1
|
@@ -1,27 +1,79 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
4
|
+
import { portalContent } from '../actions/portal';
|
|
5
|
+
import { computePosition, type Anchor } from '../utils/positioning';
|
|
3
6
|
|
|
4
7
|
let {
|
|
5
8
|
label,
|
|
6
9
|
class: className = '',
|
|
7
10
|
disabled = false,
|
|
11
|
+
anchor = 'top' as Anchor,
|
|
8
12
|
children,
|
|
9
13
|
...props
|
|
10
|
-
}: {
|
|
11
|
-
string
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
}: {
|
|
15
|
+
label: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
anchor?: Anchor;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
} & Record<string, unknown> = $props();
|
|
21
|
+
|
|
22
|
+
let triggerEl = $state<HTMLElement | null>(null);
|
|
23
|
+
let tooltipEl = $state<HTMLElement | null>(null);
|
|
24
|
+
let visible = $state(false);
|
|
25
|
+
let coords = $state({ top: 0, left: 0 });
|
|
26
|
+
let transform = $state('none');
|
|
27
|
+
let resolvedAnchor = $state<Anchor>('top');
|
|
28
|
+
|
|
29
|
+
const position = async () => {
|
|
30
|
+
await tick();
|
|
31
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
32
|
+
if (!triggerEl || !tooltipEl) return;
|
|
33
|
+
const result = computePosition(
|
|
34
|
+
triggerEl.getBoundingClientRect(),
|
|
35
|
+
tooltipEl.offsetWidth,
|
|
36
|
+
tooltipEl.offsetHeight,
|
|
37
|
+
anchor,
|
|
38
|
+
'center'
|
|
39
|
+
);
|
|
40
|
+
coords = { top: result.top, left: result.left };
|
|
41
|
+
transform = result.transform;
|
|
42
|
+
resolvedAnchor = result.anchor;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const show = () => {
|
|
46
|
+
if (disabled) return;
|
|
47
|
+
visible = true;
|
|
48
|
+
void position();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const hide = () => {
|
|
52
|
+
visible = false;
|
|
53
|
+
};
|
|
14
54
|
</script>
|
|
15
55
|
|
|
16
56
|
<span
|
|
17
|
-
|
|
18
|
-
|
|
57
|
+
bind:this={triggerEl}
|
|
58
|
+
class="inline-flex items-center {className}"
|
|
59
|
+
onmouseenter={show}
|
|
60
|
+
onmouseleave={hide}
|
|
61
|
+
onfocusin={show}
|
|
62
|
+
onfocusout={hide}
|
|
19
63
|
{...props}
|
|
20
64
|
>
|
|
21
65
|
{@render children()}
|
|
22
|
-
{#if !disabled}
|
|
23
|
-
<span class="tooltip" role="tooltip">
|
|
24
|
-
{label}
|
|
25
|
-
</span>
|
|
26
|
-
{/if}
|
|
27
66
|
</span>
|
|
67
|
+
|
|
68
|
+
{#if visible}
|
|
69
|
+
<div
|
|
70
|
+
use:portalContent
|
|
71
|
+
class="tooltip pointer-events-none fixed z-50"
|
|
72
|
+
data-anchor={resolvedAnchor}
|
|
73
|
+
style="top: {coords.top}px; left: {coords.left}px; transform: {transform};"
|
|
74
|
+
role="tooltip"
|
|
75
|
+
bind:this={tooltipEl}
|
|
76
|
+
>
|
|
77
|
+
{label}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type Anchor } from '../utils/positioning';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
label: string;
|
|
4
5
|
class?: string;
|
|
5
6
|
disabled?: boolean;
|
|
7
|
+
anchor?: Anchor;
|
|
6
8
|
children: Snippet;
|
|
7
9
|
} & Record<string, unknown>;
|
|
8
10
|
declare const Tooltip: import("svelte").Component<$$ComponentProps, {}, "">;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export { default as Sheet } from './components/Sheet';
|
|
|
10
10
|
export { default as Spinner } from './components/Spinner.svelte';
|
|
11
11
|
export { default as TextShimmer } from './components/TextShimmer.svelte';
|
|
12
12
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
13
|
-
export { default as Dropdown } from './components/Dropdown';
|
|
14
13
|
export { default as Popover } from './components/Popover';
|
|
15
14
|
export { default as ChipInput } from './components/ChipInput.svelte';
|
|
16
15
|
export { default as AsyncButton } from './components/AsyncButton.svelte';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,6 @@ export { default as Sheet } from './components/Sheet';
|
|
|
10
10
|
export { default as Spinner } from './components/Spinner.svelte';
|
|
11
11
|
export { default as TextShimmer } from './components/TextShimmer.svelte';
|
|
12
12
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
13
|
-
export { default as Dropdown } from './components/Dropdown';
|
|
14
13
|
export { default as Popover } from './components/Popover';
|
|
15
14
|
export { default as ChipInput } from './components/ChipInput.svelte';
|
|
16
15
|
export { default as AsyncButton } from './components/AsyncButton.svelte';
|
package/dist/style/index.css
CHANGED
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.vstack {
|
|
23
|
-
@apply flex min-h-0 flex-col;
|
|
23
|
+
@apply flex min-h-0 min-w-0 flex-col;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.hstack {
|
|
27
|
-
@apply flex min-w-0 flex-row;
|
|
27
|
+
@apply flex min-h-0 min-w-0 flex-row;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.spacer {
|
|
@@ -184,28 +184,47 @@
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
.tooltip {
|
|
187
|
-
@apply
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.tooltip-trigger:hover .tooltip,
|
|
191
|
-
.tooltip-trigger:focus-within .tooltip {
|
|
192
|
-
@apply opacity-100;
|
|
187
|
+
@apply rounded-sm bg-black px-2 py-1 text-xs whitespace-nowrap text-white;
|
|
193
188
|
}
|
|
194
189
|
|
|
195
190
|
.tooltip::before {
|
|
196
191
|
content: '';
|
|
197
192
|
position: absolute;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
width: 10px;
|
|
201
|
-
height: 10px;
|
|
202
|
-
transform: translateX(-50%) rotate(45deg);
|
|
193
|
+
width: 8px;
|
|
194
|
+
height: 8px;
|
|
203
195
|
background: #000;
|
|
204
|
-
|
|
205
|
-
border-bottom-right-radius: 0.125rem;
|
|
196
|
+
transform: rotate(45deg);
|
|
206
197
|
z-index: -1;
|
|
207
198
|
}
|
|
208
199
|
|
|
200
|
+
/* Arrow points down (tooltip is above trigger) */
|
|
201
|
+
.tooltip[data-anchor='top']::before {
|
|
202
|
+
bottom: -4px;
|
|
203
|
+
left: 50%;
|
|
204
|
+
margin-left: -4px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Arrow points up (tooltip is below trigger) */
|
|
208
|
+
.tooltip[data-anchor='bottom']::before {
|
|
209
|
+
top: -4px;
|
|
210
|
+
left: 50%;
|
|
211
|
+
margin-left: -4px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Arrow points right (tooltip is to the left of trigger) */
|
|
215
|
+
.tooltip[data-anchor='leading']::before {
|
|
216
|
+
right: -4px;
|
|
217
|
+
top: 50%;
|
|
218
|
+
margin-top: -4px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Arrow points left (tooltip is to the right of trigger) */
|
|
222
|
+
.tooltip[data-anchor='trailing']::before {
|
|
223
|
+
left: -4px;
|
|
224
|
+
top: 50%;
|
|
225
|
+
margin-top: -4px;
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
.segmented-picker {
|
|
210
229
|
@apply gap-1 rounded-md bg-zinc-100 p-1;
|
|
211
230
|
}
|
package/dist/style.css
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"Courier New", monospace;
|
|
10
10
|
--color-red-500: oklch(63.7% 0.237 25.331);
|
|
11
11
|
--color-red-600: oklch(57.7% 0.245 27.325);
|
|
12
|
-
--color-red-700: oklch(50.5% 0.213 27.518);
|
|
13
12
|
--color-gray-200: oklch(92.8% 0.006 264.531);
|
|
14
13
|
--color-gray-700: oklch(37.3% 0.034 259.733);
|
|
15
14
|
--color-gray-900: oklch(21% 0.034 264.665);
|
|
@@ -20,6 +19,7 @@
|
|
|
20
19
|
--color-zinc-400: oklch(70.5% 0.015 286.067);
|
|
21
20
|
--color-zinc-500: oklch(55.2% 0.016 285.938);
|
|
22
21
|
--color-zinc-700: oklch(37% 0.013 285.805);
|
|
22
|
+
--color-zinc-800: oklch(27.4% 0.006 286.033);
|
|
23
23
|
--color-zinc-900: oklch(21% 0.006 285.885);
|
|
24
24
|
--color-black: #000;
|
|
25
25
|
--color-white: #fff;
|
|
@@ -200,6 +200,9 @@
|
|
|
200
200
|
.pointer-events-none {
|
|
201
201
|
pointer-events: none;
|
|
202
202
|
}
|
|
203
|
+
.visible {
|
|
204
|
+
visibility: visible;
|
|
205
|
+
}
|
|
203
206
|
.absolute {
|
|
204
207
|
position: absolute;
|
|
205
208
|
}
|
|
@@ -224,6 +227,9 @@
|
|
|
224
227
|
.z-50 {
|
|
225
228
|
z-index: 50;
|
|
226
229
|
}
|
|
230
|
+
.z-\[9999\] {
|
|
231
|
+
z-index: 9999;
|
|
232
|
+
}
|
|
227
233
|
.mt-2 {
|
|
228
234
|
margin-top: calc(var(--spacing) * 2);
|
|
229
235
|
}
|
|
@@ -245,6 +251,9 @@
|
|
|
245
251
|
.inline-flex {
|
|
246
252
|
display: inline-flex;
|
|
247
253
|
}
|
|
254
|
+
.inline-grid {
|
|
255
|
+
display: inline-grid;
|
|
256
|
+
}
|
|
248
257
|
.size-3 {
|
|
249
258
|
width: calc(var(--spacing) * 3);
|
|
250
259
|
height: calc(var(--spacing) * 3);
|
|
@@ -275,6 +284,9 @@
|
|
|
275
284
|
.w-8 {
|
|
276
285
|
width: calc(var(--spacing) * 8);
|
|
277
286
|
}
|
|
287
|
+
.w-56 {
|
|
288
|
+
width: calc(var(--spacing) * 56);
|
|
289
|
+
}
|
|
278
290
|
.w-60 {
|
|
279
291
|
width: calc(var(--spacing) * 60);
|
|
280
292
|
}
|
|
@@ -290,6 +302,9 @@
|
|
|
290
302
|
.flex-1 {
|
|
291
303
|
flex: 1;
|
|
292
304
|
}
|
|
305
|
+
.shrink-0 {
|
|
306
|
+
flex-shrink: 0;
|
|
307
|
+
}
|
|
293
308
|
.-translate-x-px {
|
|
294
309
|
--tw-translate-x: -1px;
|
|
295
310
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
@@ -313,6 +328,9 @@
|
|
|
313
328
|
.flex-wrap {
|
|
314
329
|
flex-wrap: wrap;
|
|
315
330
|
}
|
|
331
|
+
.place-items-center {
|
|
332
|
+
place-items: center;
|
|
333
|
+
}
|
|
316
334
|
.items-center {
|
|
317
335
|
align-items: center;
|
|
318
336
|
}
|
|
@@ -399,9 +417,6 @@
|
|
|
399
417
|
.px-3 {
|
|
400
418
|
padding-inline: calc(var(--spacing) * 3);
|
|
401
419
|
}
|
|
402
|
-
.px-4 {
|
|
403
|
-
padding-inline: calc(var(--spacing) * 4);
|
|
404
|
-
}
|
|
405
420
|
.py-1 {
|
|
406
421
|
padding-block: calc(var(--spacing) * 1);
|
|
407
422
|
}
|
|
@@ -415,6 +430,10 @@
|
|
|
415
430
|
font-size: var(--text-sm);
|
|
416
431
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
|
417
432
|
}
|
|
433
|
+
.text-xs {
|
|
434
|
+
font-size: var(--text-xs);
|
|
435
|
+
line-height: var(--tw-leading, var(--text-xs--line-height));
|
|
436
|
+
}
|
|
418
437
|
.leading-none {
|
|
419
438
|
--tw-leading: 1;
|
|
420
439
|
line-height: 1;
|
|
@@ -437,9 +456,6 @@
|
|
|
437
456
|
.text-red-600 {
|
|
438
457
|
color: var(--color-red-600);
|
|
439
458
|
}
|
|
440
|
-
.text-red-700 {
|
|
441
|
-
color: var(--color-red-700);
|
|
442
|
-
}
|
|
443
459
|
.text-white {
|
|
444
460
|
color: var(--color-white);
|
|
445
461
|
}
|
|
@@ -449,6 +465,9 @@
|
|
|
449
465
|
.text-zinc-700 {
|
|
450
466
|
color: var(--color-zinc-700);
|
|
451
467
|
}
|
|
468
|
+
.text-zinc-800 {
|
|
469
|
+
color: var(--color-zinc-800);
|
|
470
|
+
}
|
|
452
471
|
.uppercase {
|
|
453
472
|
text-transform: uppercase;
|
|
454
473
|
}
|
|
@@ -538,11 +557,6 @@
|
|
|
538
557
|
color: var(--color-zinc-500);
|
|
539
558
|
}
|
|
540
559
|
}
|
|
541
|
-
.disabled\:opacity-40 {
|
|
542
|
-
&:disabled {
|
|
543
|
-
opacity: 40%;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
560
|
.disabled\:opacity-50 {
|
|
547
561
|
&:disabled {
|
|
548
562
|
opacity: 50%;
|
|
@@ -582,10 +596,12 @@
|
|
|
582
596
|
.vstack {
|
|
583
597
|
display: flex;
|
|
584
598
|
min-height: calc(var(--spacing) * 0);
|
|
599
|
+
min-width: calc(var(--spacing) * 0);
|
|
585
600
|
flex-direction: column;
|
|
586
601
|
}
|
|
587
602
|
.hstack {
|
|
588
603
|
display: flex;
|
|
604
|
+
min-height: calc(var(--spacing) * 0);
|
|
589
605
|
min-width: calc(var(--spacing) * 0);
|
|
590
606
|
flex-direction: row;
|
|
591
607
|
}
|
|
@@ -766,13 +782,6 @@
|
|
|
766
782
|
box-shadow: var(--shadow-md);
|
|
767
783
|
}
|
|
768
784
|
.tooltip {
|
|
769
|
-
pointer-events: none;
|
|
770
|
-
position: absolute;
|
|
771
|
-
bottom: calc(100% + 6px);
|
|
772
|
-
left: calc(1/2 * 100%);
|
|
773
|
-
z-index: 10;
|
|
774
|
-
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
|
775
|
-
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
776
785
|
border-radius: var(--radius-sm);
|
|
777
786
|
background-color: var(--color-black);
|
|
778
787
|
padding-inline: calc(var(--spacing) * 2);
|
|
@@ -781,24 +790,36 @@
|
|
|
781
790
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
|
782
791
|
white-space: nowrap;
|
|
783
792
|
color: var(--color-white);
|
|
784
|
-
opacity: 0%;
|
|
785
|
-
}
|
|
786
|
-
.tooltip-trigger:hover .tooltip, .tooltip-trigger:focus-within .tooltip {
|
|
787
|
-
opacity: 100%;
|
|
788
793
|
}
|
|
789
794
|
.tooltip::before {
|
|
790
795
|
content: '';
|
|
791
796
|
position: absolute;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
width: 10px;
|
|
795
|
-
height: 10px;
|
|
796
|
-
transform: translateX(-50%) rotate(45deg);
|
|
797
|
+
width: 8px;
|
|
798
|
+
height: 8px;
|
|
797
799
|
background: #000;
|
|
798
|
-
|
|
799
|
-
border-bottom-right-radius: 0.125rem;
|
|
800
|
+
transform: rotate(45deg);
|
|
800
801
|
z-index: -1;
|
|
801
802
|
}
|
|
803
|
+
.tooltip[data-anchor='top']::before {
|
|
804
|
+
bottom: -4px;
|
|
805
|
+
left: 50%;
|
|
806
|
+
margin-left: -4px;
|
|
807
|
+
}
|
|
808
|
+
.tooltip[data-anchor='bottom']::before {
|
|
809
|
+
top: -4px;
|
|
810
|
+
left: 50%;
|
|
811
|
+
margin-left: -4px;
|
|
812
|
+
}
|
|
813
|
+
.tooltip[data-anchor='leading']::before {
|
|
814
|
+
right: -4px;
|
|
815
|
+
top: 50%;
|
|
816
|
+
margin-top: -4px;
|
|
817
|
+
}
|
|
818
|
+
.tooltip[data-anchor='trailing']::before {
|
|
819
|
+
left: -4px;
|
|
820
|
+
top: 50%;
|
|
821
|
+
margin-top: -4px;
|
|
822
|
+
}
|
|
802
823
|
.segmented-picker {
|
|
803
824
|
gap: calc(var(--spacing) * 1);
|
|
804
825
|
border-radius: var(--radius-md);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared positioning engine for floating UI elements (popovers, tooltips, dropdowns, etc.).
|
|
3
|
+
* Handles anchor/align placement with automatic viewport-overflow flipping.
|
|
4
|
+
*/
|
|
5
|
+
export type Anchor = 'top' | 'bottom' | 'leading' | 'trailing';
|
|
6
|
+
export type Align = 'left' | 'center' | 'right';
|
|
7
|
+
export type Transform = 'none' | 'translateX(-50%)' | 'translateX(-100%)' | 'translateY(-100%)' | 'translate(-50%, -100%)' | 'translate(-100%, -100%)';
|
|
8
|
+
export interface Placement {
|
|
9
|
+
top: number;
|
|
10
|
+
left: number;
|
|
11
|
+
transform: Transform;
|
|
12
|
+
}
|
|
13
|
+
export interface PositionResult extends Placement {
|
|
14
|
+
anchor: Anchor;
|
|
15
|
+
align: Align;
|
|
16
|
+
}
|
|
17
|
+
export declare function oppositeAnchor(value: Anchor): Anchor;
|
|
18
|
+
export declare function flippedAlign(value: Align): Align;
|
|
19
|
+
export declare function getPlacement(rect: DOMRect, nextAnchor: Anchor, nextAlign: Align): Placement;
|
|
20
|
+
export declare function getViewportRect(placement: Placement, width: number, height: number): {
|
|
21
|
+
left: number;
|
|
22
|
+
top: number;
|
|
23
|
+
right: number;
|
|
24
|
+
bottom: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function overflowScore(rect: {
|
|
27
|
+
left: number;
|
|
28
|
+
top: number;
|
|
29
|
+
right: number;
|
|
30
|
+
bottom: number;
|
|
31
|
+
}, margin?: number): number;
|
|
32
|
+
/**
|
|
33
|
+
* Compute the best placement for a floating element anchored to a trigger.
|
|
34
|
+
* Tries the preferred anchor/align first, then flips to avoid viewport overflow.
|
|
35
|
+
*/
|
|
36
|
+
export declare function computePosition(triggerRect: DOMRect, floatingWidth: number, floatingHeight: number, preferredAnchor: Anchor, preferredAlign: Align): PositionResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared positioning engine for floating UI elements (popovers, tooltips, dropdowns, etc.).
|
|
3
|
+
* Handles anchor/align placement with automatic viewport-overflow flipping.
|
|
4
|
+
*/
|
|
5
|
+
export function oppositeAnchor(value) {
|
|
6
|
+
if (value === 'top')
|
|
7
|
+
return 'bottom';
|
|
8
|
+
if (value === 'bottom')
|
|
9
|
+
return 'top';
|
|
10
|
+
if (value === 'leading')
|
|
11
|
+
return 'trailing';
|
|
12
|
+
return 'leading';
|
|
13
|
+
}
|
|
14
|
+
export function flippedAlign(value) {
|
|
15
|
+
if (value === 'center')
|
|
16
|
+
return 'center';
|
|
17
|
+
return value === 'left' ? 'right' : 'left';
|
|
18
|
+
}
|
|
19
|
+
export function getPlacement(rect, nextAnchor, nextAlign) {
|
|
20
|
+
if (nextAnchor === 'top' || nextAnchor === 'bottom') {
|
|
21
|
+
const left = nextAlign === 'left'
|
|
22
|
+
? rect.left + window.scrollX
|
|
23
|
+
: nextAlign === 'right'
|
|
24
|
+
? rect.right + window.scrollX
|
|
25
|
+
: rect.left + rect.width / 2 + window.scrollX;
|
|
26
|
+
const top = nextAnchor === 'bottom' ? rect.bottom + window.scrollY : rect.top + window.scrollY;
|
|
27
|
+
if (nextAnchor === 'top' && nextAlign === 'center') {
|
|
28
|
+
return { top, left, transform: 'translate(-50%, -100%)' };
|
|
29
|
+
}
|
|
30
|
+
if (nextAnchor === 'top' && nextAlign === 'right') {
|
|
31
|
+
return { top, left, transform: 'translate(-100%, -100%)' };
|
|
32
|
+
}
|
|
33
|
+
if (nextAlign === 'center') {
|
|
34
|
+
return { top, left, transform: 'translateX(-50%)' };
|
|
35
|
+
}
|
|
36
|
+
if (nextAnchor === 'top') {
|
|
37
|
+
return { top, left, transform: 'translateY(-100%)' };
|
|
38
|
+
}
|
|
39
|
+
if (nextAlign === 'right') {
|
|
40
|
+
return { top, left, transform: 'translateX(-100%)' };
|
|
41
|
+
}
|
|
42
|
+
return { top, left, transform: 'none' };
|
|
43
|
+
}
|
|
44
|
+
if (nextAnchor === 'leading') {
|
|
45
|
+
return {
|
|
46
|
+
top: rect.top + window.scrollY,
|
|
47
|
+
left: rect.left + window.scrollX,
|
|
48
|
+
transform: 'translateX(-100%)'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
top: rect.top + window.scrollY,
|
|
53
|
+
left: rect.right + window.scrollX,
|
|
54
|
+
transform: 'none'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function getViewportRect(placement, width, height) {
|
|
58
|
+
let left = placement.left - window.scrollX;
|
|
59
|
+
let top = placement.top - window.scrollY;
|
|
60
|
+
if (placement.transform === 'translateX(-100%)')
|
|
61
|
+
left -= width;
|
|
62
|
+
else if (placement.transform === 'translateX(-50%)')
|
|
63
|
+
left -= width / 2;
|
|
64
|
+
else if (placement.transform === 'translateY(-100%)')
|
|
65
|
+
top -= height;
|
|
66
|
+
else if (placement.transform === 'translate(-50%, -100%)') {
|
|
67
|
+
left -= width / 2;
|
|
68
|
+
top -= height;
|
|
69
|
+
}
|
|
70
|
+
else if (placement.transform === 'translate(-100%, -100%)') {
|
|
71
|
+
left -= width;
|
|
72
|
+
top -= height;
|
|
73
|
+
}
|
|
74
|
+
return { left, top, right: left + width, bottom: top + height };
|
|
75
|
+
}
|
|
76
|
+
export function overflowScore(rect, margin = 8) {
|
|
77
|
+
const leftOverflow = Math.max(0, margin - rect.left);
|
|
78
|
+
const rightOverflow = Math.max(0, rect.right - (window.innerWidth - margin));
|
|
79
|
+
const topOverflow = Math.max(0, margin - rect.top);
|
|
80
|
+
const bottomOverflow = Math.max(0, rect.bottom - (window.innerHeight - margin));
|
|
81
|
+
return leftOverflow + rightOverflow + topOverflow + bottomOverflow;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compute the best placement for a floating element anchored to a trigger.
|
|
85
|
+
* Tries the preferred anchor/align first, then flips to avoid viewport overflow.
|
|
86
|
+
*/
|
|
87
|
+
export function computePosition(triggerRect, floatingWidth, floatingHeight, preferredAnchor, preferredAlign) {
|
|
88
|
+
const candidates = [
|
|
89
|
+
{ anchor: preferredAnchor, align: preferredAlign },
|
|
90
|
+
{ anchor: preferredAnchor, align: flippedAlign(preferredAlign) },
|
|
91
|
+
{ anchor: oppositeAnchor(preferredAnchor), align: preferredAlign },
|
|
92
|
+
{ anchor: oppositeAnchor(preferredAnchor), align: flippedAlign(preferredAlign) }
|
|
93
|
+
];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
let best = null;
|
|
96
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
const key = `${candidate.anchor}:${candidate.align}`;
|
|
99
|
+
if (seen.has(key))
|
|
100
|
+
continue;
|
|
101
|
+
seen.add(key);
|
|
102
|
+
const placement = getPlacement(triggerRect, candidate.anchor, candidate.align);
|
|
103
|
+
const rect = getViewportRect(placement, floatingWidth, floatingHeight);
|
|
104
|
+
const score = overflowScore(rect);
|
|
105
|
+
if (score < bestScore) {
|
|
106
|
+
bestScore = score;
|
|
107
|
+
best = { ...placement, anchor: candidate.anchor, align: candidate.align };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Fallback (should never happen — at least one candidate always exists)
|
|
111
|
+
return best ?? { top: 0, left: 0, transform: 'none', anchor: preferredAnchor, align: preferredAlign };
|
|
112
|
+
}
|