@x33025/sveltely 0.0.50 → 0.0.53
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 +128 -129
- 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 +33 -26
- package/dist/style.css +50 -47
- 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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
export function registerPopover(id) {
|
|
3
|
+
registry.set(id, { parentId: null, open: false });
|
|
4
|
+
}
|
|
5
|
+
export function unregisterPopover(id) {
|
|
6
|
+
registry.delete(id);
|
|
7
|
+
}
|
|
8
|
+
export function setParentId(id, parentId) {
|
|
9
|
+
const entry = registry.get(id);
|
|
10
|
+
if (entry)
|
|
11
|
+
entry.parentId = parentId;
|
|
12
|
+
}
|
|
13
|
+
export function setOpen(id, open) {
|
|
14
|
+
const entry = registry.get(id);
|
|
15
|
+
if (entry)
|
|
16
|
+
entry.open = open;
|
|
17
|
+
}
|
|
18
|
+
export function getParentId(id) {
|
|
19
|
+
return registry.get(id)?.parentId ?? null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if any registered popover lists `parentId` as its parent and is currently open.
|
|
23
|
+
*/
|
|
24
|
+
export function hasOpenChild(parentId) {
|
|
25
|
+
for (const [, entry] of registry) {
|
|
26
|
+
if (entry.parentId === parentId && entry.open)
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walk the parent chain from `candidateId` upward.
|
|
33
|
+
* Returns true if `ancestorId` is found in the chain.
|
|
34
|
+
*/
|
|
35
|
+
export function isAncestor(candidateId, ancestorId) {
|
|
36
|
+
let currentId = candidateId;
|
|
37
|
+
const visited = new Set();
|
|
38
|
+
while (currentId) {
|
|
39
|
+
if (currentId === ancestorId)
|
|
40
|
+
return true;
|
|
41
|
+
if (visited.has(currentId))
|
|
42
|
+
return false;
|
|
43
|
+
visited.add(currentId);
|
|
44
|
+
currentId = registry.get(currentId)?.parentId ?? null;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns the IDs of all currently open popovers.
|
|
50
|
+
*/
|
|
51
|
+
export function getOpenPopoverIds() {
|
|
52
|
+
const ids = [];
|
|
53
|
+
for (const [id, entry] of registry) {
|
|
54
|
+
if (entry.open)
|
|
55
|
+
ids.push(id);
|
|
56
|
+
}
|
|
57
|
+
return ids;
|
|
58
|
+
}
|
|
@@ -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
|
@@ -94,20 +94,8 @@
|
|
|
94
94
|
@apply rounded p-1.5 hover:bg-zinc-100;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.dropdown {
|
|
98
|
-
@apply rounded-md border border-gray-200 bg-white p-1 shadow-md;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.dropdown-item {
|
|
102
|
-
@apply rounded-md;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.dropdown-item:hover {
|
|
106
|
-
@apply bg-zinc-100;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
97
|
.popover {
|
|
110
|
-
@apply rounded-md border border-gray-200 bg-white p-
|
|
98
|
+
@apply rounded-md border border-gray-200 bg-white p-1 shadow-md;
|
|
111
99
|
}
|
|
112
100
|
|
|
113
101
|
.sheet {
|
|
@@ -184,28 +172,47 @@
|
|
|
184
172
|
}
|
|
185
173
|
|
|
186
174
|
.tooltip {
|
|
187
|
-
@apply
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.tooltip-trigger:hover .tooltip,
|
|
191
|
-
.tooltip-trigger:focus-within .tooltip {
|
|
192
|
-
@apply opacity-100;
|
|
175
|
+
@apply rounded-sm bg-black px-2 py-1 text-xs whitespace-nowrap text-white;
|
|
193
176
|
}
|
|
194
177
|
|
|
195
178
|
.tooltip::before {
|
|
196
179
|
content: '';
|
|
197
180
|
position: absolute;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
width: 10px;
|
|
201
|
-
height: 10px;
|
|
202
|
-
transform: translateX(-50%) rotate(45deg);
|
|
181
|
+
width: 8px;
|
|
182
|
+
height: 8px;
|
|
203
183
|
background: #000;
|
|
204
|
-
|
|
205
|
-
border-bottom-right-radius: 0.125rem;
|
|
184
|
+
transform: rotate(45deg);
|
|
206
185
|
z-index: -1;
|
|
207
186
|
}
|
|
208
187
|
|
|
188
|
+
/* Arrow points down (tooltip is above trigger) */
|
|
189
|
+
.tooltip[data-anchor='top']::before {
|
|
190
|
+
bottom: -4px;
|
|
191
|
+
left: 50%;
|
|
192
|
+
margin-left: -4px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Arrow points up (tooltip is below trigger) */
|
|
196
|
+
.tooltip[data-anchor='bottom']::before {
|
|
197
|
+
top: -4px;
|
|
198
|
+
left: 50%;
|
|
199
|
+
margin-left: -4px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Arrow points right (tooltip is to the left of trigger) */
|
|
203
|
+
.tooltip[data-anchor='leading']::before {
|
|
204
|
+
right: -4px;
|
|
205
|
+
top: 50%;
|
|
206
|
+
margin-top: -4px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Arrow points left (tooltip is to the right of trigger) */
|
|
210
|
+
.tooltip[data-anchor='trailing']::before {
|
|
211
|
+
left: -4px;
|
|
212
|
+
top: 50%;
|
|
213
|
+
margin-top: -4px;
|
|
214
|
+
}
|
|
215
|
+
|
|
209
216
|
.segmented-picker {
|
|
210
217
|
@apply gap-1 rounded-md bg-zinc-100 p-1;
|
|
211
218
|
}
|
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%;
|
|
@@ -671,29 +685,13 @@
|
|
|
671
685
|
}
|
|
672
686
|
}
|
|
673
687
|
}
|
|
674
|
-
.dropdown {
|
|
675
|
-
border-radius: var(--radius-md);
|
|
676
|
-
border-style: var(--tw-border-style);
|
|
677
|
-
border-width: 1px;
|
|
678
|
-
border-color: var(--color-gray-200);
|
|
679
|
-
background-color: var(--color-white);
|
|
680
|
-
padding: calc(var(--spacing) * 1);
|
|
681
|
-
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
682
|
-
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
683
|
-
}
|
|
684
|
-
.dropdown-item {
|
|
685
|
-
border-radius: var(--radius-md);
|
|
686
|
-
}
|
|
687
|
-
.dropdown-item:hover {
|
|
688
|
-
background-color: var(--color-zinc-100);
|
|
689
|
-
}
|
|
690
688
|
.popover {
|
|
691
689
|
border-radius: var(--radius-md);
|
|
692
690
|
border-style: var(--tw-border-style);
|
|
693
691
|
border-width: 1px;
|
|
694
692
|
border-color: var(--color-gray-200);
|
|
695
693
|
background-color: var(--color-white);
|
|
696
|
-
padding: calc(var(--spacing) *
|
|
694
|
+
padding: calc(var(--spacing) * 1);
|
|
697
695
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
698
696
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
699
697
|
}
|
|
@@ -768,13 +766,6 @@
|
|
|
768
766
|
box-shadow: var(--shadow-md);
|
|
769
767
|
}
|
|
770
768
|
.tooltip {
|
|
771
|
-
pointer-events: none;
|
|
772
|
-
position: absolute;
|
|
773
|
-
bottom: calc(100% + 6px);
|
|
774
|
-
left: calc(1/2 * 100%);
|
|
775
|
-
z-index: 10;
|
|
776
|
-
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
|
777
|
-
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
778
769
|
border-radius: var(--radius-sm);
|
|
779
770
|
background-color: var(--color-black);
|
|
780
771
|
padding-inline: calc(var(--spacing) * 2);
|
|
@@ -783,24 +774,36 @@
|
|
|
783
774
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
|
784
775
|
white-space: nowrap;
|
|
785
776
|
color: var(--color-white);
|
|
786
|
-
opacity: 0%;
|
|
787
|
-
}
|
|
788
|
-
.tooltip-trigger:hover .tooltip, .tooltip-trigger:focus-within .tooltip {
|
|
789
|
-
opacity: 100%;
|
|
790
777
|
}
|
|
791
778
|
.tooltip::before {
|
|
792
779
|
content: '';
|
|
793
780
|
position: absolute;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
width: 10px;
|
|
797
|
-
height: 10px;
|
|
798
|
-
transform: translateX(-50%) rotate(45deg);
|
|
781
|
+
width: 8px;
|
|
782
|
+
height: 8px;
|
|
799
783
|
background: #000;
|
|
800
|
-
|
|
801
|
-
border-bottom-right-radius: 0.125rem;
|
|
784
|
+
transform: rotate(45deg);
|
|
802
785
|
z-index: -1;
|
|
803
786
|
}
|
|
787
|
+
.tooltip[data-anchor='top']::before {
|
|
788
|
+
bottom: -4px;
|
|
789
|
+
left: 50%;
|
|
790
|
+
margin-left: -4px;
|
|
791
|
+
}
|
|
792
|
+
.tooltip[data-anchor='bottom']::before {
|
|
793
|
+
top: -4px;
|
|
794
|
+
left: 50%;
|
|
795
|
+
margin-left: -4px;
|
|
796
|
+
}
|
|
797
|
+
.tooltip[data-anchor='leading']::before {
|
|
798
|
+
right: -4px;
|
|
799
|
+
top: 50%;
|
|
800
|
+
margin-top: -4px;
|
|
801
|
+
}
|
|
802
|
+
.tooltip[data-anchor='trailing']::before {
|
|
803
|
+
left: -4px;
|
|
804
|
+
top: 50%;
|
|
805
|
+
margin-top: -4px;
|
|
806
|
+
}
|
|
804
807
|
.segmented-picker {
|
|
805
808
|
gap: calc(var(--spacing) * 1);
|
|
806
809
|
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
|
+
}
|