@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.
@@ -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
- }: { label: string; class?: string; disabled?: boolean; children: Snippet } & Record<
11
- string,
12
- unknown
13
- > = $props();
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
- class="tooltip-trigger relative inline-flex items-center {className}"
18
- aria-label={!disabled ? label : undefined}
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';
@@ -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-2 shadow-md;
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 pointer-events-none absolute bottom-[calc(100%+6px)] left-1/2 z-10 -translate-x-1/2 rounded-sm bg-black px-2 py-1 text-xs whitespace-nowrap text-white opacity-0;
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
- top: calc(100% - 5px);
199
- left: 50%;
200
- width: 10px;
201
- height: 10px;
202
- transform: translateX(-50%) rotate(45deg);
181
+ width: 8px;
182
+ height: 8px;
203
183
  background: #000;
204
- border-radius: 0;
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) * 2);
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
- top: calc(100% - 5px);
795
- left: 50%;
796
- width: 10px;
797
- height: 10px;
798
- transform: translateX(-50%) rotate(45deg);
781
+ width: 8px;
782
+ height: 8px;
799
783
  background: #000;
800
- border-radius: 0;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x33025/sveltely",
3
- "version": "0.0.50",
3
+ "version": "0.0.53",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",