@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.
@@ -10,6 +10,8 @@
10
10
  label: string;
11
11
  action: () => void | Promise<void>;
12
12
  error?: unknown | null;
13
+ loading?: boolean;
14
+ disableWhileLoading?: boolean;
13
15
  style?: 'iconAndLabel' | 'iconOnly';
14
16
  } & Omit<HTMLButtonAttributes, 'children' | 'onclick'>;
15
17
 
@@ -18,6 +20,8 @@
18
20
  label,
19
21
  action,
20
22
  error = $bindable<unknown | null>(null),
23
+ loading = undefined,
24
+ disableWhileLoading = true,
21
25
  style: styleMode = 'iconAndLabel',
22
26
  disabled = false,
23
27
  class: className = '',
@@ -25,18 +29,31 @@
25
29
  ...props
26
30
  }: Props = $props();
27
31
 
28
- let pending = $state(false);
32
+ let internalLoading = $state(false);
33
+
34
+ const isControlledLoading = $derived(loading !== undefined);
35
+ const effectiveLoading = $derived(isControlledLoading ? loading : internalLoading);
36
+ const effectiveDisabled = $derived(disabled || (disableWhileLoading && effectiveLoading));
29
37
 
30
38
  const handleClick = async () => {
31
- if (disabled || pending) return;
39
+ if (effectiveDisabled) return;
32
40
  error = null;
33
- pending = true;
41
+ if (isControlledLoading) {
42
+ try {
43
+ await action();
44
+ } catch (caught) {
45
+ error = caught;
46
+ }
47
+ return;
48
+ }
49
+
50
+ internalLoading = true;
34
51
  try {
35
52
  await action();
36
53
  } catch (caught) {
37
54
  error = caught;
38
55
  } finally {
39
- pending = false;
56
+ internalLoading = false;
40
57
  }
41
58
  };
42
59
 
@@ -52,21 +69,23 @@
52
69
  <button
53
70
  {type}
54
71
  class="inline-flex items-center gap-2 disabled:cursor-not-allowed disabled:opacity-50 {className}"
55
- disabled={disabled || pending}
56
- aria-busy={pending}
72
+ disabled={effectiveDisabled}
73
+ aria-busy={effectiveLoading}
57
74
  aria-label={styleMode === 'iconOnly' ? label : undefined}
58
75
  data-error={error ? 'true' : 'false'}
59
76
  {...props}
60
77
  onclick={handleClick}
61
78
  >
62
- {#if pending}
63
- <Spinner class="async-button-icon size-4" />
64
- {:else if error}
65
- <CircleAlertIcon class="async-button-icon size-4 text-red-600" />
66
- {:else if icon}
67
- {@const Icon = icon}
68
- <Icon class="async-button-icon size-4" />
69
- {/if}
79
+ <span class="inline-grid size-4 shrink-0 place-items-center">
80
+ {#if effectiveLoading}
81
+ <Spinner class="async-button-icon size-4" />
82
+ {:else if error}
83
+ <CircleAlertIcon class="async-button-icon size-4 text-red-600" />
84
+ {:else if icon}
85
+ {@const Icon = icon}
86
+ <Icon class="async-button-icon size-4" />
87
+ {/if}
88
+ </span>
70
89
  {#if styleMode === 'iconAndLabel'}
71
90
  <span class="async-button-text">{label}</span>
72
91
  {/if}
@@ -7,6 +7,8 @@ type Props = {
7
7
  label: string;
8
8
  action: () => void | Promise<void>;
9
9
  error?: unknown | null;
10
+ loading?: boolean;
11
+ disableWhileLoading?: boolean;
10
12
  style?: 'iconAndLabel' | 'iconOnly';
11
13
  } & Omit<HTMLButtonAttributes, 'children' | 'onclick'>;
12
14
  declare const AsyncButton: Component<Props, {}, "error">;
@@ -1,29 +1,31 @@
1
1
  <script lang="ts">
2
- import { tick } from 'svelte';
2
+ import { tick, onDestroy } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import { portalContent } from '../../actions/portal';
5
+ import { computePosition, type Anchor, type Align } from '../../utils/positioning';
6
+ import {
7
+ registerPopover,
8
+ unregisterPopover,
9
+ setParentId,
10
+ setOpen,
11
+ hasOpenChild,
12
+ isAncestor
13
+ } from './registry.svelte';
5
14
 
6
15
  interface Props {
7
16
  trigger: Snippet;
8
17
  children: Snippet;
9
18
  open?: boolean;
10
19
  class?: string;
11
- align?: 'left' | 'center' | 'right';
12
- anchor?: 'top' | 'bottom' | 'leading' | 'trailing';
20
+ align?: Align;
21
+ anchor?: Anchor;
13
22
  }
14
- type Anchor = 'top' | 'bottom' | 'leading' | 'trailing';
15
- type Align = 'left' | 'center' | 'right';
16
23
  type Point = { x: number; y: number };
17
- type Placement = {
18
- top: number;
19
- left: number;
20
- transform:
21
- | 'none'
22
- | 'translateX(-50%)'
23
- | 'translateX(-100%)'
24
- | 'translateY(-100%)'
25
- | 'translate(-50%, -100%)'
26
- | 'translate(-100%, -100%)';
24
+ const createPopoverId = () => {
25
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
26
+ return crypto.randomUUID();
27
+ }
28
+ return `popover-${Math.random().toString(36).slice(2, 10)}`;
27
29
  };
28
30
 
29
31
  let {
@@ -37,9 +39,41 @@
37
39
 
38
40
  let triggerEl = $state<HTMLElement | null>(null);
39
41
  let panelEl = $state<HTMLElement | null>(null);
42
+ let contentEl = $state<HTMLElement | null>(null);
40
43
  let panelCoords = $state({ top: 0, left: 0 });
41
44
  let panelTransform = $state('none');
42
45
  let resolvedAnchor = $state<Anchor>('bottom');
46
+ let computedPanelRadius = $state<string | null>(null);
47
+
48
+ const popoverId = createPopoverId();
49
+ let parentPopoverId = $state<string | null>(null);
50
+
51
+ /**
52
+ * Detect parent popover by walking the DOM upward from the trigger element.
53
+ * Works even with portaling because the trigger stays in its original
54
+ * DOM position (inside the parent's portaled content).
55
+ */
56
+ const detectParentPopoverId = (): string | null => {
57
+ let current = triggerEl?.parentElement ?? null;
58
+ while (current) {
59
+ const id = current.getAttribute('data-popover-id');
60
+ if (id && id !== popoverId) return id;
61
+ current = current.parentElement;
62
+ }
63
+ return null;
64
+ };
65
+
66
+ registerPopover(popoverId);
67
+
68
+ $effect(() => {
69
+ if (!triggerEl) return;
70
+ parentPopoverId = detectParentPopoverId();
71
+ setParentId(popoverId, parentPopoverId);
72
+ });
73
+
74
+ $effect(() => setOpen(popoverId, open));
75
+
76
+ onDestroy(() => unregisterPopover(popoverId));
43
77
 
44
78
  const isInsideRect = (rect: DOMRect, x: number, y: number) =>
45
79
  x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
@@ -99,136 +133,87 @@
99
133
  ];
100
134
  };
101
135
 
102
- const oppositeAnchor = (value: Anchor): Anchor => {
103
- if (value === 'top') return 'bottom';
104
- if (value === 'bottom') return 'top';
105
- if (value === 'leading') return 'trailing';
106
- return 'leading';
107
- };
108
-
109
- const flippedAlign = (value: Align): Align => {
110
- if (value === 'center') return 'center';
111
- return value === 'left' ? 'right' : 'left';
136
+ const parsePx = (value: string) => {
137
+ const first = value.split(' ')[0];
138
+ const parsed = Number.parseFloat(first);
139
+ return Number.isFinite(parsed) ? parsed : 0;
112
140
  };
113
141
 
114
- const getPlacement = (rect: DOMRect, nextAnchor: Anchor, nextAlign: Align): Placement => {
115
- if (nextAnchor === 'top' || nextAnchor === 'bottom') {
116
- const left =
117
- nextAlign === 'left'
118
- ? rect.left + window.scrollX
119
- : nextAlign === 'right'
120
- ? rect.right + window.scrollX
121
- : rect.left + rect.width / 2 + window.scrollX;
122
- const top =
123
- nextAnchor === 'bottom' ? rect.bottom + window.scrollY : rect.top + window.scrollY;
124
- if (nextAnchor === 'top' && nextAlign === 'center') {
125
- return { top, left, transform: 'translate(-50%, -100%)' };
126
- }
127
- if (nextAnchor === 'top' && nextAlign === 'right') {
128
- return { top, left, transform: 'translate(-100%, -100%)' };
129
- }
130
- if (nextAlign === 'center') {
131
- return { top, left, transform: 'translateX(-50%)' };
132
- }
133
- if (nextAnchor === 'top') {
134
- return { top, left, transform: 'translateY(-100%)' };
135
- }
136
- if (nextAlign === 'right') {
137
- return { top, left, transform: 'translateX(-100%)' };
138
- }
139
- return { top, left, transform: 'none' };
142
+ const updateComputedPanelRadius = () => {
143
+ if (!panelEl || !contentEl) {
144
+ computedPanelRadius = null;
145
+ return;
140
146
  }
141
147
 
142
- if (nextAnchor === 'leading') {
143
- return {
144
- top: rect.top + window.scrollY,
145
- left: rect.left + window.scrollX,
146
- transform: 'translateX(-100%)'
147
- };
148
+ const itemEl =
149
+ contentEl.querySelector<HTMLElement>('[data-popover-radius-item]') ||
150
+ contentEl.querySelector<HTMLElement>('.popover-item') ||
151
+ contentEl.querySelector<HTMLElement>('.dropdown-item');
152
+ if (!itemEl) {
153
+ computedPanelRadius = null;
154
+ return;
148
155
  }
149
156
 
150
- return {
151
- top: rect.top + window.scrollY,
152
- left: rect.right + window.scrollX,
153
- transform: 'none'
154
- };
157
+ const itemRect = itemEl.getBoundingClientRect();
158
+ const itemStyle = getComputedStyle(itemEl);
159
+ const itemRadius = parsePx(itemStyle.borderTopLeftRadius);
160
+ const effectiveItemRadius = Math.min(itemRadius, itemRect.height / 2, itemRect.width / 2);
161
+
162
+ const panelStyle = getComputedStyle(panelEl);
163
+ const panelPadding = Math.min(parsePx(panelStyle.paddingTop), parsePx(panelStyle.paddingLeft));
164
+
165
+ const panelRect = panelEl.getBoundingClientRect();
166
+ const outerRadius = Math.min(
167
+ effectiveItemRadius + panelPadding,
168
+ panelRect.height / 2,
169
+ panelRect.width / 2
170
+ );
171
+
172
+ computedPanelRadius = `${outerRadius}px`;
155
173
  };
156
174
 
157
- const getViewportRect = (placement: Placement, width: number, height: number) => {
158
- let left = placement.left - window.scrollX;
159
- let top = placement.top - window.scrollY;
160
-
161
- if (placement.transform === 'translateX(-100%)') left -= width;
162
- else if (placement.transform === 'translateX(-50%)') left -= width / 2;
163
- else if (placement.transform === 'translateY(-100%)') top -= height;
164
- else if (placement.transform === 'translate(-50%, -100%)') {
165
- left -= width / 2;
166
- top -= height;
167
- } else if (placement.transform === 'translate(-100%, -100%)') {
168
- left -= width;
169
- top -= height;
170
- }
175
+ const positionAndMeasure = async () => {
176
+ await tick();
177
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
178
+ positionPanel();
179
+ updateComputedPanelRadius();
180
+ };
171
181
 
172
- return { left, top, right: left + width, bottom: top + height };
182
+ const getPopoverIdsFromEvent = (event: Event) => {
183
+ const ids: string[] = [];
184
+ for (const node of event.composedPath()) {
185
+ if (!(node instanceof Element)) continue;
186
+ const id = node.getAttribute('data-popover-id');
187
+ if (!id || ids.includes(id)) continue;
188
+ ids.push(id);
189
+ }
190
+ return ids;
173
191
  };
174
192
 
175
- const overflowScore = (
176
- rect: { left: number; top: number; right: number; bottom: number },
177
- margin = 8
178
- ) => {
179
- const leftOverflow = Math.max(0, margin - rect.left);
180
- const rightOverflow = Math.max(0, rect.right - (window.innerWidth - margin));
181
- const topOverflow = Math.max(0, margin - rect.top);
182
- const bottomOverflow = Math.max(0, rect.bottom - (window.innerHeight - margin));
183
- return leftOverflow + rightOverflow + topOverflow + bottomOverflow;
193
+ const isInPopoverTree = (candidateId: string | null) => isAncestor(candidateId, popoverId);
194
+
195
+ const isEventInPopoverTree = (event: Event) => {
196
+ const ids = getPopoverIdsFromEvent(event);
197
+ return ids.some(isInPopoverTree);
184
198
  };
185
199
 
186
200
  const positionPanel = () => {
187
201
  if (!triggerEl || !panelEl) return;
188
202
  const rect = triggerEl.getBoundingClientRect();
189
- const width = panelEl.offsetWidth;
190
- const height = panelEl.offsetHeight;
191
- const candidates: Array<{ anchor: Anchor; align: Align }> = [
192
- { anchor, align },
193
- { anchor, align: flippedAlign(align) },
194
- { anchor: oppositeAnchor(anchor), align },
195
- { anchor: oppositeAnchor(anchor), align: flippedAlign(align) }
196
- ];
197
-
198
- const unique = new Set<string>();
199
- let bestPlacement: Placement | null = null;
200
- let bestAnchor: Anchor | null = null;
201
- let bestScore = Number.POSITIVE_INFINITY;
202
-
203
- for (const candidate of candidates) {
204
- const key = `${candidate.anchor}:${candidate.align}`;
205
- if (unique.has(key)) continue;
206
- unique.add(key);
207
-
208
- const placement = getPlacement(rect, candidate.anchor, candidate.align);
209
- const nextRect = getViewportRect(placement, width, height);
210
- const score = overflowScore(nextRect);
211
- if (score < bestScore) {
212
- bestScore = score;
213
- bestPlacement = placement;
214
- bestAnchor = candidate.anchor;
215
- }
216
- }
217
-
218
- if (!bestPlacement) return;
219
- panelCoords = { top: bestPlacement.top, left: bestPlacement.left };
220
- panelTransform = bestPlacement.transform;
221
- if (bestAnchor) resolvedAnchor = bestAnchor;
203
+ const result = computePosition(rect, panelEl.offsetWidth, panelEl.offsetHeight, anchor, align);
204
+ panelCoords = { top: result.top, left: result.left };
205
+ panelTransform = result.transform;
206
+ resolvedAnchor = result.anchor;
222
207
  };
223
208
 
224
209
  async function openPanel() {
225
210
  open = true;
226
- await tick();
227
- positionPanel();
211
+ await positionAndMeasure();
228
212
  }
229
213
 
230
214
  function closePanel() {
231
215
  open = false;
216
+ computedPanelRadius = null;
232
217
  }
233
218
 
234
219
  function toggle() {
@@ -241,8 +226,7 @@
241
226
 
242
227
  function handleOutsideClick(event: MouseEvent) {
243
228
  if (!open) return;
244
- const target = event.target as HTMLElement;
245
- if (target.closest('[data-popover-root]') || target.closest('[data-popover]')) return;
229
+ if (isEventInPopoverTree(event)) return;
246
230
  closePanel();
247
231
  }
248
232
 
@@ -253,6 +237,8 @@
253
237
 
254
238
  function handlePointerMove(event: MouseEvent) {
255
239
  if (!open || !triggerEl || !panelEl) return;
240
+ if (isEventInPopoverTree(event)) return;
241
+ if (hasOpenChild(popoverId)) return;
256
242
 
257
243
  const pointer = { x: event.clientX, y: event.clientY };
258
244
  const triggerRect = triggerEl.getBoundingClientRect();
@@ -268,9 +254,12 @@
268
254
 
269
255
  $effect(() => {
270
256
  if (!open) return;
271
- void tick().then(() => {
272
- positionPanel();
273
- });
257
+ void positionAndMeasure();
258
+ });
259
+
260
+ $effect(() => {
261
+ if (!open) return;
262
+ updateComputedPanelRadius();
274
263
  });
275
264
  </script>
276
265
 
@@ -282,7 +271,12 @@
282
271
  onmousemove={handlePointerMove}
283
272
  />
284
273
 
285
- <div class="relative inline-block text-left" data-popover-root>
274
+ <div
275
+ class="relative inline-block text-left"
276
+ data-popover-root
277
+ data-popover-id={popoverId}
278
+ data-popover-parent-id={parentPopoverId ?? ''}
279
+ >
286
280
  <div bind:this={triggerEl}>
287
281
  <button
288
282
  type="button"
@@ -300,13 +294,18 @@
300
294
  use:portalContent
301
295
  class="popover fixed z-50 focus:outline-none {className}"
302
296
  data-popover
303
- style="top: {panelCoords.top}px; left: {panelCoords.left}px; transform: {panelTransform};"
297
+ data-popover-id={popoverId}
298
+ data-popover-parent-id={parentPopoverId ?? ''}
299
+ style="top: {panelCoords.top}px; left: {panelCoords.left}px; transform: {panelTransform}; border-radius: {computedPanelRadius ??
300
+ '0.375rem'};"
304
301
  role="dialog"
305
302
  aria-modal="false"
306
303
  tabindex="-1"
307
304
  bind:this={panelEl}
308
305
  >
309
- {@render children()}
306
+ <div role="none" bind:this={contentEl}>
307
+ {@render children()}
308
+ </div>
310
309
  </div>
311
310
  {/if}
312
311
  </div>
@@ -1,11 +1,12 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import { type Anchor, type Align } from '../../utils/positioning';
2
3
  interface Props {
3
4
  trigger: Snippet;
4
5
  children: Snippet;
5
6
  open?: boolean;
6
7
  class?: string;
7
- align?: 'left' | 'center' | 'right';
8
- anchor?: 'top' | 'bottom' | 'leading' | 'trailing';
8
+ align?: Align;
9
+ anchor?: Anchor;
9
10
  }
10
11
  declare const Popover: import("svelte").Component<Props, {}, "open">;
11
12
  type Popover = ReturnType<typeof Popover>;
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ import { portalContent } from '../../actions/portal';
3
+ import { getOpenPopoverIds, getParentId } from './registry.svelte';
4
+
5
+ type Point = { x: number; y: number };
6
+ type Anchor = 'top' | 'bottom' | 'leading' | 'trailing';
7
+
8
+ interface PopoverGeometry {
9
+ id: string;
10
+ parentId: string | null;
11
+ triggerRect: DOMRect;
12
+ panelRect: DOMRect;
13
+ anchor: Anchor;
14
+ safePolygon: Point[];
15
+ }
16
+
17
+ let geometries = $state<PopoverGeometry[]>([]);
18
+
19
+ const inferAnchor = (trigger: DOMRect, panel: DOMRect): Anchor => {
20
+ const dx = panel.left + panel.width / 2 - (trigger.left + trigger.width / 2);
21
+ const dy = panel.top + panel.height / 2 - (trigger.top + trigger.height / 2);
22
+ if (Math.abs(dy) >= Math.abs(dx)) return dy >= 0 ? 'bottom' : 'top';
23
+ return dx >= 0 ? 'trailing' : 'leading';
24
+ };
25
+
26
+ const getSafePolygon = (trigger: DOMRect, panel: DOMRect, anchor: Anchor): Point[] => {
27
+ if (anchor === 'bottom') {
28
+ return [
29
+ { x: trigger.left, y: trigger.top },
30
+ { x: trigger.right, y: trigger.top },
31
+ { x: panel.right, y: panel.top },
32
+ { x: panel.left, y: panel.top }
33
+ ];
34
+ }
35
+ if (anchor === 'top') {
36
+ return [
37
+ { x: trigger.left, y: trigger.bottom },
38
+ { x: trigger.right, y: trigger.bottom },
39
+ { x: panel.right, y: panel.bottom },
40
+ { x: panel.left, y: panel.bottom }
41
+ ];
42
+ }
43
+ if (anchor === 'leading') {
44
+ return [
45
+ { x: trigger.right, y: trigger.top },
46
+ { x: trigger.right, y: trigger.bottom },
47
+ { x: panel.right, y: panel.bottom },
48
+ { x: panel.right, y: panel.top }
49
+ ];
50
+ }
51
+ return [
52
+ { x: trigger.left, y: trigger.top },
53
+ { x: trigger.left, y: trigger.bottom },
54
+ { x: panel.left, y: panel.bottom },
55
+ { x: panel.left, y: panel.top }
56
+ ];
57
+ };
58
+
59
+ const measure = () => {
60
+ const ids = getOpenPopoverIds();
61
+ const results: PopoverGeometry[] = [];
62
+
63
+ for (const id of ids) {
64
+ const root = document.querySelector<HTMLElement>(
65
+ `[data-popover-root][data-popover-id="${id}"]`
66
+ );
67
+ const panel = document.querySelector<HTMLElement>(`[data-popover][data-popover-id="${id}"]`);
68
+ if (!root || !panel) continue;
69
+
70
+ const triggerRect = root.getBoundingClientRect();
71
+ const panelRect = panel.getBoundingClientRect();
72
+ const anchor = inferAnchor(triggerRect, panelRect);
73
+
74
+ results.push({
75
+ id,
76
+ parentId: getParentId(id),
77
+ triggerRect,
78
+ panelRect,
79
+ anchor,
80
+ safePolygon: getSafePolygon(triggerRect, panelRect, anchor)
81
+ });
82
+ }
83
+
84
+ geometries = results;
85
+ };
86
+
87
+ $effect(() => {
88
+ let raf: number;
89
+ const loop = () => {
90
+ measure();
91
+ raf = requestAnimationFrame(loop);
92
+ };
93
+ raf = requestAnimationFrame(loop);
94
+ return () => cancelAnimationFrame(raf);
95
+ });
96
+ </script>
97
+
98
+ {#if geometries.length > 0}
99
+ <div use:portalContent class="pointer-events-none fixed inset-0 z-[9999]">
100
+ <svg width="100%" height="100%" aria-hidden="true">
101
+ {#each geometries as geo (geo.id)}
102
+ <!-- Safe polygon (rendered first so rects draw over it) -->
103
+ <polygon
104
+ points={geo.safePolygon.map((p) => `${p.x},${p.y}`).join(' ')}
105
+ fill="rgba(239,68,68,0.10)"
106
+ stroke="rgb(220,38,38)"
107
+ stroke-width="1.5"
108
+ />
109
+
110
+ <!-- Trigger rect -->
111
+ <rect
112
+ x={geo.triggerRect.left}
113
+ y={geo.triggerRect.top}
114
+ width={geo.triggerRect.width}
115
+ height={geo.triggerRect.height}
116
+ fill="rgba(37,99,235,0.06)"
117
+ stroke="rgb(37,99,235)"
118
+ stroke-width="1"
119
+ stroke-dasharray="3,2"
120
+ />
121
+
122
+ <!-- Panel rect -->
123
+ <rect
124
+ x={geo.panelRect.left}
125
+ y={geo.panelRect.top}
126
+ width={geo.panelRect.width}
127
+ height={geo.panelRect.height}
128
+ fill="rgba(22,163,74,0.06)"
129
+ stroke="rgb(22,163,74)"
130
+ stroke-width="1"
131
+ stroke-dasharray="3,2"
132
+ />
133
+
134
+ <!-- Parent → child connection line -->
135
+ {#if geo.parentId}
136
+ {@const parent = geometries.find((g) => g.id === geo.parentId)}
137
+ {#if parent}
138
+ <line
139
+ x1={parent.panelRect.left + parent.panelRect.width / 2}
140
+ y1={parent.panelRect.top + parent.panelRect.height / 2}
141
+ x2={geo.panelRect.left + geo.panelRect.width / 2}
142
+ y2={geo.panelRect.top + geo.panelRect.height / 2}
143
+ stroke="rgba(168,85,247,0.4)"
144
+ stroke-width="1"
145
+ stroke-dasharray="4,3"
146
+ />
147
+ {/if}
148
+ {/if}
149
+
150
+ <!-- Popover ID label -->
151
+ <text
152
+ x={geo.panelRect.left + 4}
153
+ y={geo.panelRect.top - 4}
154
+ font-size="10"
155
+ fill="rgb(113,113,122)"
156
+ font-family="ui-monospace, monospace"
157
+ >
158
+ {geo.id.slice(0, 8)}
159
+ </text>
160
+ {/each}
161
+ </svg>
162
+ </div>
163
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const PopoverDebugOverlay: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PopoverDebugOverlay = ReturnType<typeof PopoverDebugOverlay>;
3
+ export default PopoverDebugOverlay;
@@ -1 +1,2 @@
1
1
  export { default } from './Popover.svelte';
2
+ export { default as PopoverDebugOverlay } from './PopoverDebugOverlay.svelte';
@@ -1 +1,2 @@
1
1
  export { default } from './Popover.svelte';
2
+ export { default as PopoverDebugOverlay } from './PopoverDebugOverlay.svelte';
@@ -0,0 +1,18 @@
1
+ export declare function registerPopover(id: string): void;
2
+ export declare function unregisterPopover(id: string): void;
3
+ export declare function setParentId(id: string, parentId: string | null): void;
4
+ export declare function setOpen(id: string, open: boolean): void;
5
+ export declare function getParentId(id: string): string | null;
6
+ /**
7
+ * Returns true if any registered popover lists `parentId` as its parent and is currently open.
8
+ */
9
+ export declare function hasOpenChild(parentId: string): boolean;
10
+ /**
11
+ * Walk the parent chain from `candidateId` upward.
12
+ * Returns true if `ancestorId` is found in the chain.
13
+ */
14
+ export declare function isAncestor(candidateId: string | null, ancestorId: string): boolean;
15
+ /**
16
+ * Returns the IDs of all currently open popovers.
17
+ */
18
+ export declare function getOpenPopoverIds(): string[];