@cplieger/ui 0.0.0-stage → 1.0.0-dev.1

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 (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +3 -0
  3. package/README.md +157 -2
  4. package/THIRD_PARTY_NOTICES.md +8 -0
  5. package/css/ui-primitives.css +344 -0
  6. package/jsr.json +44 -0
  7. package/package.json +105 -4
  8. package/src/actions/api.ts +268 -0
  9. package/src/actions/async-feedback.ts +217 -0
  10. package/src/actions/cleanup.ts +66 -0
  11. package/src/actions/debounce.ts +150 -0
  12. package/src/actions/define-helpers.ts +87 -0
  13. package/src/actions/define.ts +851 -0
  14. package/src/actions/error.ts +161 -0
  15. package/src/actions/index.ts +60 -0
  16. package/src/actions/loading.ts +131 -0
  17. package/src/actions/notifier.ts +61 -0
  18. package/src/actions/poll-until.ts +133 -0
  19. package/src/actions/poll.ts +156 -0
  20. package/src/actions/registry.ts +259 -0
  21. package/src/actions/retry.ts +79 -0
  22. package/src/actions/testing.ts +29 -0
  23. package/src/actions/transport.ts +104 -0
  24. package/src/actions/types.ts +249 -0
  25. package/src/dom/bind-list.ts +103 -0
  26. package/src/dom/el.ts +67 -0
  27. package/src/dom/index.ts +9 -0
  28. package/src/dom/reconcile-tree.ts +268 -0
  29. package/src/dom/reconcile.ts +76 -0
  30. package/src/fetch/index.ts +18 -0
  31. package/src/fetch/instance.ts +37 -0
  32. package/src/fetch/request.ts +437 -0
  33. package/src/fetch/timeout.ts +67 -0
  34. package/src/fetch/types.ts +206 -0
  35. package/src/fetch/verbs.ts +103 -0
  36. package/src/index.ts +4 -0
  37. package/src/primitives/announce.ts +134 -0
  38. package/src/primitives/ask.ts +406 -0
  39. package/src/primitives/dialog-engine.ts +500 -0
  40. package/src/primitives/dialog.ts +263 -0
  41. package/src/primitives/disclosure.ts +287 -0
  42. package/src/primitives/focus-trap.ts +157 -0
  43. package/src/primitives/fx.ts +204 -0
  44. package/src/primitives/live-regions.ts +83 -0
  45. package/src/primitives/modal-host.ts +214 -0
  46. package/src/primitives/modal.ts +247 -0
  47. package/src/primitives/owner.ts +322 -0
  48. package/src/primitives/popover.ts +500 -0
  49. package/src/primitives/popup-core.ts +603 -0
  50. package/src/primitives/popup.ts +23 -0
  51. package/src/primitives/roving-focus.ts +198 -0
  52. package/src/primitives/scroll-lock.ts +119 -0
  53. package/src/primitives/skeleton.ts +142 -0
  54. package/src/primitives/theme.ts +202 -0
  55. package/src/primitives/toast/engine.ts +327 -0
  56. package/src/primitives/toast/index.ts +139 -0
  57. package/src/primitives/toast/view.ts +244 -0
  58. package/src/primitives/tooltip.ts +377 -0
  59. package/src/primitives/transition.ts +119 -0
  60. package/src/primitives/view-transition.ts +57 -0
  61. package/src/signal/bus.ts +126 -0
  62. package/src/signal/collection.ts +182 -0
  63. package/src/signal/index.ts +34 -0
  64. package/src/signal/resource.ts +102 -0
  65. package/src/signal/signal-map.ts +46 -0
  66. package/src/signal/signal.ts +1002 -0
  67. package/src/signal/store.ts +84 -0
@@ -0,0 +1,500 @@
1
+ /**
2
+ * popover.ts — An anchored floating panel: placeAnchored (pure positioner) and
3
+ * createPopover (interactive controller), layered on the shared popup
4
+ * lifecycle core (popup-core.ts, internal) which owns reveal/dismiss; popover
5
+ * adds anchored placement, scroll/resize tracking, and the stretch mode.
6
+ *
7
+ * No native `popover`: the top layer re-roots a panel's containing block to the
8
+ * viewport, moving the in-flow panels `/popup` exists for, and is unconditional —
9
+ * no z-index can lower a promoted panel back under the `--uip-z-*` ladder.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import { untracked } from "../signal/signal.js";
15
+
16
+ import { directFx, type Fx } from "./fx.js";
17
+ import {
18
+ POPUP_KEYS,
19
+ createPopupCore,
20
+ normalizePopupKey,
21
+ readOptions,
22
+ type PopupConfig,
23
+ } from "./popup-core.js";
24
+ import type { PopupOptions } from "./popup-core.js";
25
+
26
+ /** The anchor side a panel is placed against. A request, not a guarantee: with
27
+ * `flip` on (the default) a side that doesn't fit becomes its opposite. */
28
+ export type PopoverPlacement = "top" | "bottom" | "left" | "right";
29
+
30
+ /** Cross-axis alignment of the panel against the anchor: leading edges flush
31
+ * (`"start"`), centres aligned, or trailing edges flush (`"end"`). */
32
+ export type PopoverAlign = "start" | "center" | "end";
33
+
34
+ /** A virtual anchor: anything that can report a bounding rect. Lets a popover
35
+ * be positioned against a coordinate/right-click point (see pointAnchor) or
36
+ * any non-DOM rect source, not just an element. */
37
+ export interface VirtualAnchor {
38
+ getBoundingClientRect(): DOMRect;
39
+ }
40
+
41
+ /** What a popover can be anchored to: a real element, or a virtual rect source
42
+ * (e.g. a coordinate via pointAnchor). */
43
+ export type PopoverAnchor = HTMLElement | VirtualAnchor;
44
+
45
+ /** Build a zero-size virtual anchor at a fixed viewport point, such as a
46
+ * right-click; for a moving point build a new one and reposition. With no
47
+ * trigger element, a disconnected panel anchored to it is hosted into the
48
+ * topmost open modal `<dialog>`, else `document.body`; a connected panel
49
+ * stays where the caller put it. */
50
+ export function pointAnchor(x: number, y: number): VirtualAnchor {
51
+ return {
52
+ getBoundingClientRect: (): DOMRect => ({
53
+ x,
54
+ y,
55
+ left: x,
56
+ top: y,
57
+ right: x,
58
+ bottom: y,
59
+ width: 0,
60
+ height: 0,
61
+ toJSON: () => ({}),
62
+ }),
63
+ };
64
+ }
65
+
66
+ /** Everything the positioner needs beyond the panel and the anchor: the side
67
+ * and cross-axis alignment, the main-axis gap, the two viewport-fit
68
+ * corrections (`flip`, `clamp`) and the edge `margin` they respect, and the
69
+ * width modes. All optional, all pure inputs — {@link placeAnchored} reads
70
+ * them per call and keeps no state. */
71
+ export interface PlacementOptions {
72
+ /** Side of the anchor the panel sits on. Default `"bottom"`. */
73
+ placement?: PopoverPlacement;
74
+ /** Cross-axis edge alignment. Default `"start"`. */
75
+ align?: PopoverAlign;
76
+ /** Main-axis gap between anchor and panel, in px. Default `4`. */
77
+ offset?: number;
78
+ /** Flip to the opposite side when the chosen side doesn't fit. Default `true`. */
79
+ flip?: boolean;
80
+ /** Clamp the cross-axis coordinate into the viewport. Default `true`. */
81
+ clamp?: boolean;
82
+ /** Set the panel's `min-width` to the anchor width (`true`) or to
83
+ * `max(anchorWidth, n)` (a number). Default `false`. Ignored when
84
+ * `stretch: "viewport"` is set (the panel spans the full width instead). */
85
+ matchAnchorWidth?: boolean | number;
86
+ /** Viewport edge margin used by flip + clamp — and, in `stretch: "viewport"`
87
+ * mode, the inline inset from each viewport edge — in px. Default `8`. */
88
+ margin?: number;
89
+ /** Full-bleed / edge-pinned mode: `"viewport"` spans the panel across the
90
+ * viewport's inline axis (respecting `margin`) instead of content-sizing
91
+ * and cross-aligning it — the mobile full-width action-sheet pattern. Only
92
+ * meaningful for a top/bottom `placement`; `align`, cross-axis `clamp`, and
93
+ * `matchAnchorWidth` do not apply. Default unset (content-sized). */
94
+ stretch?: "viewport";
95
+ }
96
+
97
+ /** Placement options plus the popup lifecycle options (dismissal, Escape
98
+ * isolation, single-open `group`, opt-in focus, open/close callbacks,
99
+ * `haspopup` — advertised on an element anchor; ignored for a virtual/point
100
+ * anchor, which has no element to annotate). The popup `trigger` is not an
101
+ * option here: the anchor is the trigger when it is a real element. */
102
+ export interface PopoverOptions extends PlacementOptions, Omit<PopupOptions, "trigger"> {}
103
+
104
+ /** A merge-patch for `setOptions`: keys PRESENT in the patch override the
105
+ * current value — including an explicit `undefined`, which clears the option
106
+ * back to its default (e.g. `{ stretch: undefined }` leaves full-bleed mode).
107
+ * Keys absent from the patch are left unchanged. */
108
+ export type PopoverOptionsPatch = {
109
+ [K in keyof PopoverOptions]?: PopoverOptions[K] | undefined;
110
+ };
111
+
112
+ /** Handle on an anchored popover: the popup lifecycle plus placement. Position
113
+ * is recomputed on show and, while open, on scroll and resize; a content
114
+ * change is the one thing it cannot observe, so `reposition()` is the caller's
115
+ * to call. */
116
+ export interface PopoverController {
117
+ show(): void;
118
+ hide(): void;
119
+ toggle(): void;
120
+ /** Recompute placement. Call after the panel's content changes size. */
121
+ reposition(): void;
122
+ readonly isOpen: boolean;
123
+ /** The panel element (the caller's, never one this controller created). */
124
+ readonly el: HTMLElement;
125
+ /** Merge-patch the options on the LIVE controller — the seam for responsive
126
+ * placement (e.g. flip `stretch` / `offset` / `margin` on a breakpoint
127
+ * change instead of disposing and rebuilding). Keys present in the patch
128
+ * override (an explicit `undefined` clears back to the default); absent
129
+ * keys are unchanged. An open popover repositions immediately and updates
130
+ * its `is-stretched` marker; dismissal listeners re-arm under the new
131
+ * flags. The anchor is constructor-bound and cannot be patched. */
132
+ setOptions(patch: PopoverOptionsPatch): void;
133
+ dispose(): void;
134
+ }
135
+
136
+ /** The six members of an anchor's rect, read once each. */
137
+ interface Rect {
138
+ readonly top: number;
139
+ readonly left: number;
140
+ readonly right: number;
141
+ readonly bottom: number;
142
+ readonly width: number;
143
+ readonly height: number;
144
+ }
145
+
146
+ /** Read each member once: a VirtualAnchor's rect may compute them in getters. */
147
+ function snapshotRect(r: DOMRect): Rect {
148
+ return {
149
+ top: r.top,
150
+ left: r.left,
151
+ right: r.right,
152
+ bottom: r.bottom,
153
+ width: r.width,
154
+ height: r.height,
155
+ };
156
+ }
157
+
158
+ interface ViewportBox {
159
+ readonly left: number;
160
+ readonly top: number;
161
+ readonly width: number;
162
+ readonly height: number;
163
+ }
164
+
165
+ /** The visible viewport box. Uses `window.visualViewport` when present (so the
166
+ * box tracks pinch-zoom and stays above the mobile on-screen keyboard), else
167
+ * the layout viewport with zero offsets. */
168
+ function viewportBox(): ViewportBox {
169
+ const vv = window.visualViewport;
170
+ if (vv != null) {
171
+ return { left: vv.offsetLeft, top: vv.offsetTop, width: vv.width, height: vv.height };
172
+ }
173
+ return { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight };
174
+ }
175
+
176
+ /** Cross-axis coordinate for `align` along an axis whose anchor extent is
177
+ * `[start, start + size]` and whose panel extent is `panelSize`. */
178
+ function alignCoord(align: PopoverAlign, start: number, size: number, panelSize: number): number {
179
+ if (align === "center") {
180
+ return start + size / 2 - panelSize / 2;
181
+ }
182
+ if (align === "end") {
183
+ return start + size - panelSize;
184
+ }
185
+ return start;
186
+ }
187
+
188
+ /** Resolve the effective placement, flipping to the opposite side when the
189
+ * chosen side overflows its viewport edge (beyond `margin`) and the opposite
190
+ * side has more room. */
191
+ function resolvePlacement(
192
+ placement: PopoverPlacement,
193
+ rect: Rect,
194
+ panelW: number,
195
+ panelH: number,
196
+ offset: number,
197
+ margin: number,
198
+ vp: ViewportBox,
199
+ ): PopoverPlacement {
200
+ const vpRight = vp.left + vp.width;
201
+ const vpBottom = vp.top + vp.height;
202
+ const spaceAbove = rect.top - vp.top;
203
+ const spaceBelow = vpBottom - rect.bottom;
204
+ const spaceLeft = rect.left - vp.left;
205
+ const spaceRight = vpRight - rect.right;
206
+
207
+ if (placement === "bottom") {
208
+ const overflows = rect.bottom + offset + panelH > vpBottom - margin;
209
+ return overflows && spaceAbove > spaceBelow ? "top" : "bottom";
210
+ }
211
+ if (placement === "top") {
212
+ const overflows = rect.top - offset - panelH < vp.top + margin;
213
+ return overflows && spaceBelow > spaceAbove ? "bottom" : "top";
214
+ }
215
+ if (placement === "right") {
216
+ const overflows = rect.right + offset + panelW > vpRight - margin;
217
+ return overflows && spaceLeft > spaceRight ? "left" : "right";
218
+ }
219
+ const overflows = rect.left - offset - panelW < vp.left + margin;
220
+ return overflows && spaceRight > spaceLeft ? "right" : "left";
221
+ }
222
+
223
+ /** Main + cross axis coordinates for a resolved placement. */
224
+ function computeCoords(
225
+ placement: PopoverPlacement,
226
+ align: PopoverAlign,
227
+ rect: Rect,
228
+ panelW: number,
229
+ panelH: number,
230
+ offset: number,
231
+ ): { left: number; top: number } {
232
+ if (placement === "top" || placement === "bottom") {
233
+ const top = placement === "bottom" ? rect.bottom + offset : rect.top - panelH - offset;
234
+ return { left: alignCoord(align, rect.left, rect.width, panelW), top };
235
+ }
236
+ const left = placement === "right" ? rect.right + offset : rect.left - panelW - offset;
237
+ return { left, top: alignCoord(align, rect.top, rect.height, panelH) };
238
+ }
239
+
240
+ /** Clamp a coordinate so the panel stays within `[margin, size - margin]` of the
241
+ * viewport box on that axis. Pins to the leading margin when the panel is
242
+ * larger than the available space. */
243
+ function clampCoord(
244
+ coord: number,
245
+ panelSize: number,
246
+ vpStart: number,
247
+ vpSize: number,
248
+ margin: number,
249
+ ): number {
250
+ const lo = vpStart + margin;
251
+ const hi = vpStart + vpSize - panelSize - margin;
252
+ if (hi < lo) {
253
+ return lo;
254
+ }
255
+ return Math.max(lo, Math.min(coord, hi));
256
+ }
257
+
258
+ /** Every PlacementOptions key, defaults resolved, frozen. */
259
+ interface Placement {
260
+ readonly placement: PopoverPlacement;
261
+ readonly align: PopoverAlign;
262
+ readonly offset: number;
263
+ readonly flip: boolean;
264
+ readonly clamp: boolean;
265
+ readonly matchAnchorWidth: boolean | number;
266
+ readonly margin: number;
267
+ readonly stretch: "viewport" | null;
268
+ }
269
+
270
+ /** The normalized value of one placement option, or `undefined` for a key that is not one. */
271
+ function normalizePlacementKey(key: string, value: unknown): unknown {
272
+ switch (key) {
273
+ case "placement":
274
+ return value ?? "bottom";
275
+ case "align":
276
+ return value ?? "start";
277
+ case "offset":
278
+ return value ?? 4;
279
+ case "flip":
280
+ case "clamp":
281
+ return value ?? true;
282
+ case "matchAnchorWidth":
283
+ return value ?? false;
284
+ case "margin":
285
+ return value ?? 8;
286
+ case "stretch":
287
+ return value ?? null;
288
+ default:
289
+ return undefined;
290
+ }
291
+ }
292
+
293
+ const PLACEMENT_KEYS = [
294
+ "placement",
295
+ "align",
296
+ "offset",
297
+ "flip",
298
+ "clamp",
299
+ "matchAnchorWidth",
300
+ "margin",
301
+ "stretch",
302
+ ] as const;
303
+
304
+ function normalizePlacement(opts: PlacementOptions | undefined): Placement {
305
+ return readOptions(opts, PLACEMENT_KEYS, normalizePlacementKey) as unknown as Placement;
306
+ }
307
+
308
+ function isStretched(p: Placement): boolean {
309
+ return p.stretch === "viewport" && (p.placement === "top" || p.placement === "bottom");
310
+ }
311
+
312
+ /** Place `panel` against `anchor` through `fx`: the anchor is measured once,
313
+ * and every panel write is one checked call. A throwing anchor leaves the
314
+ * last placement in place. */
315
+ function placeWith(fx: Fx, panel: HTMLElement, anchor: PopoverAnchor, p: Placement): void {
316
+ const raw = fx.call(() => anchor.getBoundingClientRect());
317
+ if (raw === undefined) {
318
+ return;
319
+ }
320
+ const rect = fx.measure(() => snapshotRect(raw));
321
+ if (rect === undefined) {
322
+ return;
323
+ }
324
+ fx.setStyle(panel, "position", "fixed");
325
+ const vp = viewportBox();
326
+ const { placement, margin, offset } = p;
327
+
328
+ if (isStretched(p)) {
329
+ // Pin both inline edges. In fixed positioning `right` is measured from the
330
+ // viewport's inline-end edge, so left + right together span the width minus
331
+ // `margin` on each side, robust to the panel's box-sizing and padding. A
332
+ // min-width a prior content-sized placement set would widen the pinned panel.
333
+ fx.setStyle(panel, "left", `${margin.toString()}px`);
334
+ fx.setStyle(panel, "right", `${margin.toString()}px`);
335
+ fx.setStyle(panel, "minWidth", "");
336
+ // panelW is unused by the top/bottom flip test.
337
+ const panelH = panel.offsetHeight;
338
+ const effective = p.flip
339
+ ? resolvePlacement(placement, rect, 0, panelH, offset, margin, vp)
340
+ : placement;
341
+ const top = effective === "bottom" ? rect.bottom + offset : rect.top - panelH - offset;
342
+ fx.setStyle(panel, "top", `${top.toString()}px`);
343
+ return;
344
+ }
345
+
346
+ // Content-sized: an inline-end pin a prior stretch placement wrote would fight the panel's own width.
347
+ fx.setStyle(panel, "right", "");
348
+ // matchAnchorWidth first, so the measured size reflects it; cleared when off,
349
+ // so turning it off does not leave the previous min-width stuck.
350
+ const matchWidth = p.matchAnchorWidth;
351
+ fx.setStyle(
352
+ panel,
353
+ "minWidth",
354
+ matchWidth === false
355
+ ? ""
356
+ : `${(matchWidth === true ? rect.width : Math.max(rect.width, matchWidth)).toString()}px`,
357
+ );
358
+
359
+ const panelW = panel.offsetWidth;
360
+ const panelH = panel.offsetHeight;
361
+ const effective = p.flip
362
+ ? resolvePlacement(placement, rect, panelW, panelH, offset, margin, vp)
363
+ : placement;
364
+ const { left, top } = computeCoords(effective, p.align, rect, panelW, panelH, offset);
365
+
366
+ let finalLeft = left;
367
+ let finalTop = top;
368
+ if (p.clamp) {
369
+ if (effective === "top" || effective === "bottom") {
370
+ finalLeft = clampCoord(left, panelW, vp.left, vp.width, margin);
371
+ } else {
372
+ finalTop = clampCoord(top, panelH, vp.top, vp.height, margin);
373
+ }
374
+ }
375
+ fx.setStyle(panel, "left", `${finalLeft.toString()}px`);
376
+ fx.setStyle(panel, "top", `${finalTop.toString()}px`);
377
+ }
378
+
379
+ /**
380
+ * Position `panel` (set to `position: fixed`) relative to `anchor` per `opts`.
381
+ * Every option is read once, before the anchor is measured or the panel
382
+ * written. Idempotent; safe to call repeatedly on scroll/resize or after the
383
+ * panel's content changes size. A throwing anchor or option getter reaches the caller.
384
+ */
385
+ export function placeAnchored(
386
+ panel: HTMLElement,
387
+ anchor: PopoverAnchor,
388
+ opts?: PlacementOptions,
389
+ ): void {
390
+ placeWith(directFx, panel, anchor, normalizePlacement(opts));
391
+ }
392
+
393
+ type PopoverConfig = PopupConfig & Placement;
394
+
395
+ // The anchor is the trigger and is constructor-bound, so `trigger` is never read.
396
+ const POPOVER_KEYS = [...POPUP_KEYS.filter((k) => k !== "trigger"), ...PLACEMENT_KEYS];
397
+
398
+ /** Read each popover option once into a frozen config whose trigger is `anchorEl`. */
399
+ function normalizePopoverOptions(
400
+ opts: PopoverOptions | undefined,
401
+ anchorEl: HTMLElement | null,
402
+ ): PopoverConfig {
403
+ const read = readOptions(opts, POPOVER_KEYS, normalizePopoverKey);
404
+ return Object.freeze({ ...read, trigger: anchorEl }) as unknown as PopoverConfig;
405
+ }
406
+
407
+ function normalizePopoverKey(key: string, value: unknown): unknown {
408
+ if ((PLACEMENT_KEYS as readonly string[]).includes(key)) {
409
+ return normalizePlacementKey(key, value);
410
+ }
411
+ // The anchor is the trigger: no patch changes it.
412
+ return key === "trigger" ? undefined : normalizePopupKey(key, value);
413
+ }
414
+
415
+ /** Wire `anchor` and a caller-supplied `panel` into an anchored, dismissible
416
+ * popover: it reveals and positions the panel, tracks the anchor on scroll
417
+ * and resize, and dismisses on outside click or Escape. The caller owns the
418
+ * panel element, which the controller never builds or removes. */
419
+ export function createPopover(
420
+ anchor: PopoverAnchor,
421
+ panel: HTMLElement,
422
+ opts?: PopoverOptions,
423
+ ): PopoverController {
424
+ // A virtual or point anchor has no element to annotate or hit-test.
425
+ const anchorEl = anchor instanceof HTMLElement ? anchor : null;
426
+ const config = untracked(() => normalizePopoverOptions(opts, anchorEl));
427
+
428
+ let trackingFrame: number | null = null;
429
+ let kick = (): void => undefined;
430
+ // Bursts of scroll and resize coalesce into one placement per frame.
431
+ const scheduleReposition = (): void => {
432
+ if (trackingFrame !== null) {
433
+ return;
434
+ }
435
+ trackingFrame = requestAnimationFrame(() => {
436
+ trackingFrame = null;
437
+ kick();
438
+ });
439
+ };
440
+ // Capture-phase scroll catches scrolling in any ancestor, resize covers
441
+ // layout changes, and visualViewport events cover pinch-zoom and the mobile keyboard.
442
+ const track = (on: boolean): void => {
443
+ const method = on ? "addEventListener" : "removeEventListener";
444
+ if (!on && trackingFrame !== null) {
445
+ cancelAnimationFrame(trackingFrame);
446
+ trackingFrame = null;
447
+ }
448
+ document[method]("scroll", scheduleReposition, true);
449
+ window[method]("resize", scheduleReposition);
450
+ const vv = window.visualViewport;
451
+ if (vv != null) {
452
+ vv[method]("resize", scheduleReposition);
453
+ vv[method]("scroll", scheduleReposition);
454
+ }
455
+ };
456
+
457
+ const core = createPopupCore<PopoverConfig>(
458
+ panel,
459
+ config,
460
+ {
461
+ stateClass: "uip-popover",
462
+ shown(fx, c) {
463
+ fx.toggleClass(panel, "is-stretched", isStretched(c));
464
+ placeWith(fx, panel, anchor, c);
465
+ },
466
+ leaveEnd(fx) {
467
+ fx.removeClass(panel, "is-stretched");
468
+ },
469
+ listeners: track,
470
+ },
471
+ normalizePopoverKey,
472
+ );
473
+ kick = core.kick;
474
+ const popup = core.controller;
475
+
476
+ return {
477
+ show: () => {
478
+ popup.show();
479
+ },
480
+ hide: () => {
481
+ popup.hide();
482
+ },
483
+ toggle: () => {
484
+ popup.toggle();
485
+ },
486
+ reposition: core.kick,
487
+ get isOpen(): boolean {
488
+ return popup.isOpen;
489
+ },
490
+ get el(): HTMLElement {
491
+ return panel;
492
+ },
493
+ setOptions(patch: PopoverOptionsPatch): void {
494
+ popup.setOptions(patch);
495
+ },
496
+ dispose: () => {
497
+ popup.dispose();
498
+ },
499
+ };
500
+ }