@popover-kit/svelte 0.1.0 → 0.1.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.
- package/README.md +17 -2
- package/dist/components/Popover.svelte +16 -11
- package/dist/components/PopoverContent.svelte +16 -4
- package/dist/components/PopoverTrigger.svelte +1 -1
- package/dist/controller.svelte.js +43 -13
- package/dist/elements/PopoverArrow.svelte +1 -1
- package/dist/elements/PopoverPanel.svelte +1 -1
- package/dist/index.d.ts +21 -18
- package/dist/index.js +1 -1
- package/dist/types.js +0 -0
- package/dist/utils/a11y.js +1 -1
- package/dist/utils/position.js +1 -1
- package/package.json +2 -6
- /package/dist/{constants/config.js → constants.js} +0 -0
package/README.md
CHANGED
|
@@ -22,8 +22,9 @@ and **native RTL** support via Tailwind CSS v4 + daisyUI v5, and optional
|
|
|
22
22
|
pnpm add @popover-kit/svelte
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Peer dependency: `svelte@^5`.
|
|
26
|
-
|
|
25
|
+
Peer dependency: `svelte@^5`. No icon library dependency — the built-in
|
|
26
|
+
close button renders a plain "✕" character. Pass your own `iconSnippet`
|
|
27
|
+
prop to `<PopoverContent>` (e.g. a `lucide-svelte` icon) to override it.
|
|
27
28
|
|
|
28
29
|
## Quick start
|
|
29
30
|
|
|
@@ -46,6 +47,13 @@ dependency (used for the built-in close icon).
|
|
|
46
47
|
</Popover>
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
See `demos/PageDemo.svelte` for 12 runnable scenarios (placements,
|
|
51
|
+
alignments, `autoVertical`, hover/focus triggers, controlled mode, a
|
|
52
|
+
shared/injected controller, a fully headless example, and three
|
|
53
|
+
booking/e-commerce-flavored patterns: a Kayak-style search bar, a checkout
|
|
54
|
+
confirmation, and a product-discovery tooltip). Run it in a browser via
|
|
55
|
+
`examples/svelte` (see its README).
|
|
56
|
+
|
|
49
57
|
## Dark theme & RTL
|
|
50
58
|
|
|
51
59
|
- **Dark theme**: the panel uses daisyUI's `card bg-base-100` tokens, so it
|
|
@@ -70,6 +78,13 @@ ctrl.open();
|
|
|
70
78
|
`PopoverController` is pure TypeScript (a `.svelte.ts` module using runes
|
|
71
79
|
for its reactive state) — no component required.
|
|
72
80
|
|
|
81
|
+
## Scripts
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm build # bundles src/ -> dist/index.js + dist/index.d.ts
|
|
85
|
+
pnpm test # vitest (position engine, a11y utils, controller)
|
|
86
|
+
pnpm typecheck # svelte-check
|
|
87
|
+
```
|
|
73
88
|
|
|
74
89
|
## License
|
|
75
90
|
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
* {#snippet content(ctx: ContentSnippetCtx)} — render the panel body
|
|
19
19
|
* {#snippet children(ctrl: IPopoverController)} — shorthand: default <button>
|
|
20
20
|
*/
|
|
21
|
-
import { onDestroy, type Snippet } from 'svelte';
|
|
21
|
+
import { onDestroy, untrack, type Snippet } from 'svelte';
|
|
22
22
|
import type {
|
|
23
23
|
PopoverProps,
|
|
24
24
|
IPopoverController,
|
|
25
25
|
TriggerSnippetCtx,
|
|
26
26
|
ContentSnippetCtx
|
|
27
|
-
} from '../types
|
|
27
|
+
} from '../types.js';
|
|
28
28
|
|
|
29
|
-
import { PopoverController } from '../controller.svelte';
|
|
29
|
+
import { PopoverController } from '../controller.svelte.js';
|
|
30
30
|
import PopoverPanel from '../elements/PopoverPanel.svelte';
|
|
31
31
|
import {
|
|
32
32
|
DEFAULT_OPTIONS,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
DEFAULT_OPEN_DELAY,
|
|
36
36
|
DEFAULT_CLOSE_DELAY,
|
|
37
37
|
DEFAULT_SHOW_ARROW
|
|
38
|
-
} from '../constants
|
|
38
|
+
} from '../constants.js';
|
|
39
39
|
|
|
40
40
|
// ── Props ────────────────────────────────────────────────────────────────────
|
|
41
41
|
|
|
@@ -72,15 +72,20 @@
|
|
|
72
72
|
// ── Controller ────────────────────────────────────────────────────────────────
|
|
73
73
|
// owned = we created it → destroy on unmount.
|
|
74
74
|
// injected = caller's lifecycle → only wire refs, never destroy.
|
|
75
|
+
//
|
|
76
|
+
// Deliberately constructed ONCE from the props' initial values — `id`,
|
|
77
|
+
// `placement`, etc. configure the controller at creation time only; later
|
|
78
|
+
// prop changes are intentionally not reactive here; `open` is the one
|
|
79
|
+
// exception, handled by its own $effect below. `untrack` tells the
|
|
80
|
+
// compiler this one-time read is intentional (not a missed dependency).
|
|
75
81
|
|
|
76
82
|
let owned = false;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} else {
|
|
83
|
+
const ctrl: PopoverController = untrack(() => {
|
|
84
|
+
if (injectedCtrl instanceof PopoverController) {
|
|
85
|
+
return injectedCtrl;
|
|
86
|
+
}
|
|
82
87
|
owned = true;
|
|
83
|
-
|
|
88
|
+
return new PopoverController({
|
|
84
89
|
id,
|
|
85
90
|
placement,
|
|
86
91
|
offset,
|
|
@@ -91,7 +96,7 @@
|
|
|
91
96
|
onClose,
|
|
92
97
|
onStateChange
|
|
93
98
|
});
|
|
94
|
-
}
|
|
99
|
+
});
|
|
95
100
|
|
|
96
101
|
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
|
97
102
|
//
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
* {/snippet}
|
|
19
19
|
*/
|
|
20
20
|
import type { Snippet } from 'svelte';
|
|
21
|
-
import {
|
|
22
|
-
import type { ContentSnippetCtx } from '../types/types.js';
|
|
21
|
+
import type { ContentSnippetCtx } from '../types.js';
|
|
23
22
|
|
|
24
23
|
interface Props {
|
|
25
24
|
ctx: ContentSnippetCtx;
|
|
@@ -31,6 +30,14 @@
|
|
|
31
30
|
body?: Snippet<[ContentSnippetCtx]>;
|
|
32
31
|
/** Fallback when no body snippet is provided. */
|
|
33
32
|
children?: Snippet<[ContentSnippetCtx]>;
|
|
33
|
+
/**
|
|
34
|
+
* Overrides the default close-button glyph (a plain "X" character —
|
|
35
|
+
* no icon library dependency shipped by this package). Pass e.g. a
|
|
36
|
+
* `lucide-svelte` icon snippet from your own app to swap it in:
|
|
37
|
+
*
|
|
38
|
+
* {#snippet iconSnippet()}<X class="h-3 w-3" />{/snippet}
|
|
39
|
+
*/
|
|
40
|
+
iconSnippet?: Snippet;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
const {
|
|
@@ -40,7 +47,8 @@
|
|
|
40
47
|
showClose = false,
|
|
41
48
|
class: cls = '',
|
|
42
49
|
body,
|
|
43
|
-
children
|
|
50
|
+
children,
|
|
51
|
+
iconSnippet
|
|
44
52
|
}: Props = $props();
|
|
45
53
|
</script>
|
|
46
54
|
|
|
@@ -57,7 +65,11 @@
|
|
|
57
65
|
aria-label="Close"
|
|
58
66
|
onclick={() => ctx.ctrl.close()}
|
|
59
67
|
>
|
|
60
|
-
|
|
68
|
+
{#if iconSnippet}
|
|
69
|
+
{@render iconSnippet()}
|
|
70
|
+
{:else}
|
|
71
|
+
<span class="text-xs leading-none" aria-hidden="true">✕</span>
|
|
72
|
+
{/if}
|
|
61
73
|
</button>
|
|
62
74
|
{/if}
|
|
63
75
|
</div>
|
|
@@ -5,22 +5,35 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Open sequence (critical — previous versions got this wrong):
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
8
|
+
// Two DOM-mounting strategies are supported, both converging on the same
|
|
9
|
+
// #finishOpen():
|
|
10
|
+
//
|
|
11
|
+
// A) Conditionally-mounted panel (Popover.svelte's default):
|
|
12
|
+
// 1. open() sets state → 'opening' (isVisible = true) and returns —
|
|
13
|
+
// #panelEl isn't set yet, so it can't finish synchronously.
|
|
14
|
+
// 2. Svelte sees isVisible changed, mounts the panel element in the DOM.
|
|
15
|
+
// 3. Svelte runs $effects. The $effect in Popover.svelte sees panelEl is
|
|
16
|
+
// now set and calls ctrl.attachPanel(el).
|
|
17
|
+
// 4. attachPanel() detects state === 'opening' → calls #finishOpen().
|
|
18
|
+
//
|
|
19
|
+
// B) Headless / always-mounted panel (attachTrigger + attachPanel called
|
|
20
|
+
// once up front, before open() is ever called):
|
|
21
|
+
// 1. open() sets state → 'opening', then immediately sees #panelEl is
|
|
22
|
+
// already set and calls #finishOpen() itself — there's no later
|
|
23
|
+
// attachPanel() call coming to do it.
|
|
24
|
+
//
|
|
25
|
+
// #finishOpen() reads panel dimensions (element is in DOM), computes
|
|
26
|
+
// position, sets up listeners, sets state → 'open'.
|
|
15
27
|
//
|
|
16
28
|
// This ordering guarantees:
|
|
17
29
|
// • #panelEl is always non-null when #setupListeners() runs.
|
|
18
30
|
// • Click-outside uses the real panel element (not null).
|
|
19
31
|
// • No rAF/setTimeout hacks needed.
|
|
32
|
+
// • open() completes synchronously regardless of mounting strategy.
|
|
20
33
|
// =============================================================================
|
|
21
34
|
import { computePosition } from "./utils/position.js";
|
|
22
35
|
import { createFocusTrap, onClickOutside, announce } from "./utils/a11y.js";
|
|
23
|
-
import { uid, DEFAULT_OPTIONS, ANIMATION_DURATION_MS } from "./constants
|
|
36
|
+
import { uid, DEFAULT_OPTIONS, ANIMATION_DURATION_MS } from "./constants.js";
|
|
24
37
|
export class PopoverController {
|
|
25
38
|
// ─── Stable IDs ───────────────────────────────────────────────────────────────
|
|
26
39
|
triggerId;
|
|
@@ -122,7 +135,7 @@ export class PopoverController {
|
|
|
122
135
|
if (this.#state === "opening") {
|
|
123
136
|
this.#finishOpen();
|
|
124
137
|
}
|
|
125
|
-
else if (this
|
|
138
|
+
else if (this.#state === "open") {
|
|
126
139
|
this.#compute();
|
|
127
140
|
}
|
|
128
141
|
}
|
|
@@ -138,8 +151,15 @@ export class PopoverController {
|
|
|
138
151
|
return;
|
|
139
152
|
this.#cancelClose();
|
|
140
153
|
// Set 'opening' FIRST → isVisible becomes true → Svelte mounts the panel
|
|
141
|
-
// → $effect fires → attachPanel() → #finishOpen().
|
|
154
|
+
// → $effect fires → attachPanel() → #finishOpen().
|
|
142
155
|
this.#setState(false, "opening");
|
|
156
|
+
// Headless usage (or any consumer that keeps the panel element in the
|
|
157
|
+
// DOM at all times instead of conditionally mounting it) already has
|
|
158
|
+
// #panelEl set at this point — no attachPanel() call is coming to
|
|
159
|
+
// finish the sequence, so finish it here instead. This makes open()
|
|
160
|
+
// correct regardless of DOM-mounting strategy.
|
|
161
|
+
if (this.#panelEl)
|
|
162
|
+
this.#finishOpen();
|
|
143
163
|
}
|
|
144
164
|
close() {
|
|
145
165
|
if (!this.#isOpen || this.#state === "closing")
|
|
@@ -161,7 +181,7 @@ export class PopoverController {
|
|
|
161
181
|
}
|
|
162
182
|
setPlacement(p) {
|
|
163
183
|
this.#placement = p;
|
|
164
|
-
if (this
|
|
184
|
+
if (this.#state === "open")
|
|
165
185
|
this.#compute();
|
|
166
186
|
}
|
|
167
187
|
// ─── Destroy ──────────────────────────────────────────────────────────────────
|
|
@@ -190,7 +210,10 @@ export class PopoverController {
|
|
|
190
210
|
#compute() {
|
|
191
211
|
if (!this.#triggerEl || !this.#panelEl)
|
|
192
212
|
return;
|
|
193
|
-
|
|
213
|
+
// Pass the previous resolved placement for flip hysteresis — keeps the
|
|
214
|
+
// panel on whichever side it's already showing on when that side still
|
|
215
|
+
// fits, instead of flip-flopping right at the viewport boundary.
|
|
216
|
+
const result = computePosition(this.#triggerEl, this.#panelEl, this.#placement, this.#offset, this.#resolvedPlacement);
|
|
194
217
|
this.#pos = result;
|
|
195
218
|
this.#resolvedPlacement = result.placement;
|
|
196
219
|
}
|
|
@@ -199,7 +222,14 @@ export class PopoverController {
|
|
|
199
222
|
return;
|
|
200
223
|
this.#rafId = requestAnimationFrame(() => {
|
|
201
224
|
this.#rafId = null;
|
|
202
|
-
|
|
225
|
+
// Guaranteed 'open' in practice — the scroll/resize/ResizeObserver
|
|
226
|
+
// listeners that call this are only ever wired up in
|
|
227
|
+
// #setupListeners() (called from #finishOpen()) and torn down
|
|
228
|
+
// synchronously the moment close() runs, so no stale rAF can land
|
|
229
|
+
// here mid-'closing'. Checked anyway as defense-in-depth against
|
|
230
|
+
// exactly the class of "recomputed mid-exit-animation" bug that
|
|
231
|
+
// causes a panel to visibly jump/flip while fading out.
|
|
232
|
+
if (this.#state === "open")
|
|
203
233
|
this.#compute();
|
|
204
234
|
});
|
|
205
235
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* A border on two sides of the square, combined with the clip, produces a
|
|
19
19
|
* bordered triangle that matches the card's border colour.
|
|
20
20
|
*/
|
|
21
|
-
import type { Placement } from '../types
|
|
21
|
+
import type { Placement } from '../types.js';
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
placement: Placement;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Pure CSS handles animations — no Svelte transitions needed.
|
|
17
17
|
*/
|
|
18
18
|
import type { Snippet } from 'svelte';
|
|
19
|
-
import type { Placement, ResolvedPosition, PopoverAnimation } from '../types
|
|
19
|
+
import type { Placement, ResolvedPosition, PopoverAnimation } from '../types.js';
|
|
20
20
|
import PopoverArrow from './PopoverArrow.svelte';
|
|
21
21
|
|
|
22
22
|
interface Props {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { Snippet } from 'svelte';
|
|
2
2
|
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Popover Micro-Plugin — Type Definitions (single source of truth)
|
|
5
|
-
// =============================================================================
|
|
6
|
-
// ─── Placement ────────────────────────────────────────────────────────────────
|
|
7
3
|
/**
|
|
8
4
|
* Where the panel appears relative to the trigger.
|
|
9
5
|
*
|
|
@@ -14,7 +10,6 @@ import { Snippet } from 'svelte';
|
|
|
14
10
|
export type Side = "top" | "bottom" | "left" | "right";
|
|
15
11
|
export type Alignment = "start" | "end";
|
|
16
12
|
export type Placement = "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end" | "autoVertical";
|
|
17
|
-
// ─── Computed position ────────────────────────────────────────────────────────
|
|
18
13
|
/**
|
|
19
14
|
* Output of the positioning engine.
|
|
20
15
|
* x / y are viewport-relative px values suitable for `position: fixed`.
|
|
@@ -22,18 +17,15 @@ export type Placement = "top" | "top-start" | "top-end" | "bottom" | "bottom-sta
|
|
|
22
17
|
export interface ResolvedPosition {
|
|
23
18
|
x: number;
|
|
24
19
|
y: number;
|
|
25
|
-
placement: Placement;
|
|
20
|
+
placement: Placement;
|
|
26
21
|
}
|
|
27
|
-
// ─── State ────────────────────────────────────────────────────────────────────
|
|
28
22
|
/** Fine-grained lifecycle state — drives CSS enter/exit transitions. */
|
|
29
23
|
export type PopoverState = "closed" | "opening" | "open" | "closing";
|
|
30
|
-
// ─── Controller interface ─────────────────────────────────────────────────────
|
|
31
24
|
/**
|
|
32
25
|
* Public surface of PopoverController.
|
|
33
26
|
* All readonly getters are $state runes — Svelte templates track them.
|
|
34
27
|
*/
|
|
35
28
|
export interface IPopoverController {
|
|
36
|
-
// Reactive getters
|
|
37
29
|
readonly isOpen: boolean;
|
|
38
30
|
readonly state: PopoverState;
|
|
39
31
|
readonly resolvedPlacement: Placement;
|
|
@@ -42,12 +34,10 @@ export interface IPopoverController {
|
|
|
42
34
|
readonly isVisible: boolean;
|
|
43
35
|
readonly triggerId: string;
|
|
44
36
|
readonly contentId: string;
|
|
45
|
-
// Core actions
|
|
46
37
|
open(): void;
|
|
47
38
|
close(): void;
|
|
48
39
|
toggle(): void;
|
|
49
40
|
setPlacement(p: Placement): void;
|
|
50
|
-
// Element wiring (called by Popover.svelte $effects and headless consumers)
|
|
51
41
|
attachTrigger(el: HTMLElement): void;
|
|
52
42
|
attachPanel(el: HTMLElement): void;
|
|
53
43
|
detachTrigger(): void;
|
|
@@ -65,7 +55,6 @@ export interface PopoverSnapshot {
|
|
|
65
55
|
}
|
|
66
56
|
export type SubscriberFn = (snap: PopoverSnapshot) => void;
|
|
67
57
|
export type UnsubscribeFn = () => void;
|
|
68
|
-
// ─── Controller options ───────────────────────────────────────────────────────
|
|
69
58
|
export interface ControllerOptions {
|
|
70
59
|
/** ID prefix for generated ARIA ids. Auto-generated if omitted. */
|
|
71
60
|
id?: string;
|
|
@@ -80,7 +69,6 @@ export interface ControllerOptions {
|
|
|
80
69
|
onClose?: () => void;
|
|
81
70
|
onStateChange?: (state: PopoverState) => void;
|
|
82
71
|
}
|
|
83
|
-
// ─── Component props ──────────────────────────────────────────────────────────
|
|
84
72
|
export type PopoverAnimation = "scale" | "slide" | "fade" | "none";
|
|
85
73
|
export type TriggerInteraction = "click" | "hover" | "focus" | "manual";
|
|
86
74
|
export interface PopoverProps extends ControllerOptions {
|
|
@@ -98,7 +86,6 @@ export interface PopoverProps extends ControllerOptions {
|
|
|
98
86
|
/** Inject an existing controller; this component will not destroy it. */
|
|
99
87
|
controller?: IPopoverController;
|
|
100
88
|
}
|
|
101
|
-
// ─── Snippet contexts (Bits UI style — fully typed parameters) ────────────────
|
|
102
89
|
/** Passed into `{#snippet trigger(ctx)}`. Spread these onto your trigger element. */
|
|
103
90
|
export interface TriggerSnippetCtx {
|
|
104
91
|
id: string;
|
|
@@ -118,13 +105,11 @@ export interface ContentSnippetCtx {
|
|
|
118
105
|
resolvedPlacement: Placement;
|
|
119
106
|
state: PopoverState;
|
|
120
107
|
}
|
|
121
|
-
// ─── Accessibility handles ────────────────────────────────────────────────────
|
|
122
108
|
export interface FocusTrapHandle {
|
|
123
109
|
activate(): void;
|
|
124
110
|
deactivate(): void;
|
|
125
111
|
destroy(): void;
|
|
126
112
|
}
|
|
127
|
-
// ─── Plugin factory ───────────────────────────────────────────────────────────
|
|
128
113
|
export interface ThemeTokens {
|
|
129
114
|
borderRadius: string;
|
|
130
115
|
backdropBlur: string;
|
|
@@ -204,6 +189,14 @@ interface Props$2 {
|
|
|
204
189
|
children?: Snippet<[
|
|
205
190
|
ContentSnippetCtx
|
|
206
191
|
]>;
|
|
192
|
+
/**
|
|
193
|
+
* Overrides the default close-button glyph (a plain "X" character —
|
|
194
|
+
* no icon library dependency shipped by this package). Pass e.g. a
|
|
195
|
+
* `lucide-svelte` icon snippet from your own app to swap it in:
|
|
196
|
+
*
|
|
197
|
+
* {#snippet iconSnippet()}<X class="h-3 w-3" />{/snippet}
|
|
198
|
+
*/
|
|
199
|
+
iconSnippet?: Snippet;
|
|
207
200
|
}
|
|
208
201
|
export declare const PopoverContent: import("svelte").Component<Props$2, {}, "">;
|
|
209
202
|
export type PopoverContent = ReturnType<typeof PopoverContent>;
|
|
@@ -241,15 +234,18 @@ export declare function getDirection(el: HTMLElement): "ltr" | "rtl";
|
|
|
241
234
|
* Compute the `position:fixed` coordinates for the popover panel.
|
|
242
235
|
*
|
|
243
236
|
* The panel element MUST already be in the DOM (even if visibility:hidden)
|
|
244
|
-
* so its dimensions can be read
|
|
237
|
+
* so its dimensions can be read.
|
|
245
238
|
*
|
|
246
239
|
* @param triggerEl - The trigger / reference element.
|
|
247
240
|
* @param panelEl - The panel element to position.
|
|
248
241
|
* @param placement - Requested placement (may be overridden by auto-flip).
|
|
249
242
|
* @param offset - Gap between trigger edge and panel in px.
|
|
243
|
+
* @param previousPlacement - The last resolved placement, if any. Used for
|
|
244
|
+
* flip hysteresis (see `resolveAutoSide`) — pass the previous call's
|
|
245
|
+
* `result.placement` on recomputes; omit on the first compute.
|
|
250
246
|
* @returns { x, y, placement } — coordinates and the actual placement used.
|
|
251
247
|
*/
|
|
252
|
-
export declare function computePosition(triggerEl: HTMLElement, panelEl: HTMLElement, placement: Placement, offset: number): ResolvedPosition;
|
|
248
|
+
export declare function computePosition(triggerEl: HTMLElement, panelEl: HTMLElement, placement: Placement, offset: number, previousPlacement?: Placement | null): ResolvedPosition;
|
|
253
249
|
/**
|
|
254
250
|
* Keeps keyboard focus inside `container` while active.
|
|
255
251
|
* Activation is deferred so it can be timed to the enter animation.
|
|
@@ -273,12 +269,19 @@ export declare const DEFAULT_OPTIONS: {
|
|
|
273
269
|
readonly closeOnOutsideClick: true;
|
|
274
270
|
readonly closeOnEsc: true;
|
|
275
271
|
};
|
|
272
|
+
export declare const DEFAULT_ANIMATION: "scale";
|
|
273
|
+
export declare const DEFAULT_TRIGGER: "click";
|
|
274
|
+
export declare const DEFAULT_OPEN_DELAY: number;
|
|
275
|
+
export declare const DEFAULT_CLOSE_DELAY: number;
|
|
276
|
+
export declare const DEFAULT_SHOW_ARROW: boolean;
|
|
276
277
|
/**
|
|
277
278
|
* Must match the longest `transition-duration` in PopoverPanel's CSS.
|
|
278
279
|
* The controller waits this long after close() before setting state → 'closed'
|
|
279
280
|
* so the exit animation completes before the panel is unmounted.
|
|
280
281
|
*/
|
|
281
282
|
export declare const ANIMATION_DURATION_MS = 200;
|
|
283
|
+
export declare const FOCUSABLE_SELECTOR: string;
|
|
284
|
+
export declare const LIVE_REGION_ID = "pop-live-region";
|
|
282
285
|
export declare const DEFAULT_THEME_TOKENS: ThemeTokens;
|
|
283
286
|
/**
|
|
284
287
|
* Generates a stable, unique DOM id.
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{default as
|
|
1
|
+
import{default as Q}from"./components/Popover.svelte";import{default as tt}from"./components/PopoverTrigger.svelte";import{default as ot}from"./components/PopoverContent.svelte";import{default as rt}from"./elements/PopoverPanel.svelte";import{default as st}from"./elements/PopoverArrow.svelte";import{PopoverController as ut}from"./controller.svelte.js";function b(t){if(t==="autoVertical")return{side:"bottom",alignment:null};let[n,o=null]=t.split("-");return{side:n,alignment:o}}function D(t,n,o,i,s,c,a){let r,e;switch(i){case"bottom":e=t.bottom+c;break;case"top":e=t.top-o-c;break;case"left":r=t.left-n-c;break;case"right":r=t.right+c;break}if(i==="top"||i==="bottom"){let u=a?t.right-n:t.left,d=a?t.left:t.right-n;s==="start"?r=u:s==="end"?r=d:r=t.left+(t.width-n)/2}else s==="start"?e=t.top:s==="end"?e=t.bottom-o:e=t.top+(t.height-o)/2;return{x:r,y:e}}function v(t){return getComputedStyle(t).direction==="rtl"?"rtl":"ltr"}function T(t,n,o,i=6){return Math.max(i,Math.min(o-n-i,t))}var I={top:"bottom",bottom:"top",left:"right",right:"left"};function F(t,n,o,i,s,c,a,r=0){switch(t){case"bottom":return n.bottom+s+o+r<=a;case"top":return n.top-s-o-r>=0;case"right":return n.right+s+i+r<=c;case"left":return n.left-s-i-r>=0}}var S=24;function k(t,n,o,i,s,c,a,r){let e=I[t],u=(d,p=0)=>F(d,n,o,i,s,c,a,p);return r===e?u(t,S)?t:u(e)?e:t:u(t)?t:u(e)?e:r??t}function C(t,n,o,i,s){let c=t.getBoundingClientRect(),a=n.offsetWidth,r=n.offsetHeight,e=window.innerWidth,u=window.innerHeight,d=v(t)==="rtl",{side:p,alignment:f}=b(o),L=s?b(s).side:null,m=k(p,c,r,a,i,e,u,L),O=o==="autoVertical"?m==="top"?"top":"bottom":m===p?o:f?`${m}-${f}`:m,{x:A,y:h}=D(c,a,r,m,f,i,d);return m==="top"||m==="bottom"?A=T(A,a,e):h=T(h,r,u),{x:A,y:h,placement:O}}var N={placement:"bottom",offset:8,trapFocus:!0,closeOnOutsideClick:!0,closeOnEsc:!0},P="scale",U="click",w=0,y=100,M=!1,B=200,x=["a[href]","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])','[contenteditable="true"]',"details > summary"].join(", "),E="pop-live-region",_={borderRadius:"0.75rem",backdropBlur:"12px",shadowColor:"rgba(0,0,0,0.15)",lightBg:"oklch(var(--color-base-100))",darkBg:"oklch(var(--color-base-200))",lightBorder:"oklch(var(--color-base-300))",darkBorder:"oklch(var(--color-base-300))",lightText:"oklch(var(--color-base-content))",darkText:"oklch(var(--color-base-content))",accentColor:"oklch(var(--color-primary))"},G=0;function R(t="pop"){return`${t}-${++G}`}function K(t,n){let o=!1,i=document.activeElement;function s(){return Array.from(t.querySelectorAll(x)).filter(e=>!e.hasAttribute("disabled")&&e.offsetParent!==null)}function c(e){if(!o||e.key!=="Tab")return;let u=s();if(!u.length){e.preventDefault();return}let d=u[0],p=u[u.length-1],f=document.activeElement;e.shiftKey?(f===d||!t.contains(f))&&(e.preventDefault(),p.focus()):(f===p||!t.contains(f))&&(e.preventDefault(),d.focus())}function a(){if(o)return;o=!0,i=document.activeElement,document.addEventListener("keydown",c,!0);let e=s()[0]??t;requestAnimationFrame(()=>e.focus())}function r(){if(!o)return;o=!1,document.removeEventListener("keydown",c,!0);let e=n??i;e&&typeof e.focus=="function"&&requestAnimationFrame(()=>e.focus())}return{activate:a,deactivate:r,destroy:r}}var l=null;function g(){return l||(l=document.getElementById(E),l||(l=document.createElement("div"),l.id=E,l.setAttribute("aria-live","polite"),l.setAttribute("aria-atomic","true"),Object.assign(l.style,{position:"absolute",width:"1px",height:"1px",overflow:"hidden",clip:"rect(0 0 0 0)",whiteSpace:"nowrap",border:"0",padding:"0",margin:"-1px"}),document.body.appendChild(l)),l)}function H(t,n=80){let o=g();o.textContent="",setTimeout(()=>{o.textContent=t},n)}function V(t,n){function o(i){let s=i.target;t.some(c=>c.contains(s))||n(i)}return document.addEventListener("pointerdown",o,!0),()=>document.removeEventListener("pointerdown",o,!0)}function X(t={}){return{defaults:t.defaults??{},tokens:{..._,...t.theme}}}export{B as ANIMATION_DURATION_MS,P as DEFAULT_ANIMATION,y as DEFAULT_CLOSE_DELAY,w as DEFAULT_OPEN_DELAY,N as DEFAULT_OPTIONS,M as DEFAULT_SHOW_ARROW,_ as DEFAULT_THEME_TOKENS,U as DEFAULT_TRIGGER,x as FOCUSABLE_SELECTOR,E as LIVE_REGION_ID,Q as Popover,st as PopoverArrow,ot as PopoverContent,ut as PopoverController,rt as PopoverPanel,tt as PopoverTrigger,H as announce,C as computePosition,K as createFocusTrap,X as createPopoverPlugin,v as getDirection,V as onClickOutside,b as parsePlacement,R as uid};
|
package/dist/types.js
ADDED
|
File without changes
|
package/dist/utils/a11y.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FOCUSABLE_SELECTOR as v,LIVE_REGION_ID as m}from"../constants
|
|
1
|
+
import{FOCUSABLE_SELECTOR as v,LIVE_REGION_ID as m}from"../constants.js";function y(o,r){let t=!1,i=document.activeElement;function u(){return Array.from(o.querySelectorAll(v)).filter(e=>!e.hasAttribute("disabled")&&e.offsetParent!==null)}function c(e){if(!t||e.key!=="Tab")return;const s=u();if(!s.length){e.preventDefault();return}const d=s[0],l=s[s.length-1],a=document.activeElement;e.shiftKey?(a===d||!o.contains(a))&&(e.preventDefault(),l.focus()):(a===l||!o.contains(a))&&(e.preventDefault(),d.focus())}function p(){if(t)return;t=!0,i=document.activeElement,document.addEventListener("keydown",c,!0);const e=u()[0]??o;requestAnimationFrame(()=>e.focus())}function f(){if(!t)return;t=!1,document.removeEventListener("keydown",c,!0);const e=r??i;e&&typeof e.focus=="function"&&requestAnimationFrame(()=>e.focus())}return{activate:p,deactivate:f,destroy:f}}let n=null;function g(){return n||(n=document.getElementById(m),n||(n=document.createElement("div"),n.id=m,n.setAttribute("aria-live","polite"),n.setAttribute("aria-atomic","true"),Object.assign(n.style,{position:"absolute",width:"1px",height:"1px",overflow:"hidden",clip:"rect(0 0 0 0)",whiteSpace:"nowrap",border:"0",padding:"0",margin:"-1px"}),document.body.appendChild(n)),n)}function h(o,r=80){const t=g();t.textContent="",setTimeout(()=>{t.textContent=o},r)}function b(o,r){function t(i){const u=i.target;o.some(c=>c.contains(u))||r(i)}return document.addEventListener("pointerdown",t,!0),()=>document.removeEventListener("pointerdown",t,!0)}export{h as announce,y as createFocusTrap,b as onClickOutside};
|
package/dist/utils/position.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function w(t){if(t==="autoVertical")return{side:"bottom",alignment:null};const[o,i=null]=t.split("-");return{side:o,alignment:i}}function P(t,o,i,l,u,c,e){let n,s;switch(l){case"bottom":s=t.bottom+c;break;case"top":s=t.top-i-c;break;case"left":n=t.left-o-c;break;case"right":n=t.right+c;break}if(l==="top"||l==="bottom"){const h=e?t.right-o:t.left,b=e?t.left:t.right-o;u==="start"?n=h:u==="end"?n=b:n=t.left+(t.width-o)/2}else u==="start"?s=t.top:u==="end"?s=t.bottom-i:s=t.top+(t.height-i)/2;return{x:n,y:s}}function k(t){return getComputedStyle(t).direction==="rtl"?"rtl":"ltr"}function p(t,o,i,l=6){return Math.max(l,Math.min(i-o-l,t))}const v={top:"bottom",bottom:"top",left:"right",right:"left"};function y(t,o,i,l,u,c,e,n=0){switch(t){case"bottom":return o.bottom+u+i+n<=e;case"top":return o.top-u-i-n>=0;case"right":return o.right+u+l+n<=c;case"left":return o.left-u-l-n>=0}}const A=24;function I(t,o,i,l,u,c,e,n){const s=v[t],h=(b,f=0)=>y(b,o,i,l,u,c,e,f);return n===s?h(t,A)?t:h(s)?s:t:h(t)?t:h(s)?s:n??t}function _(t,o,i,l,u){const c=t.getBoundingClientRect(),e=o.offsetWidth,n=o.offsetHeight,s=window.innerWidth,h=window.innerHeight,b=k(t)==="rtl";let{side:f,alignment:m}=w(i);const x=u?w(u).side:null,a=I(f,c,n,e,l,s,h,x),S=i==="autoVertical"?a==="top"?"top":"bottom":a===f?i:m?`${a}-${m}`:a;let{x:d,y:r}=P(c,e,n,a,m,l,b);return a==="top"||a==="bottom"?d=p(d,e,s):r=p(r,n,h),{x:d,y:r,placement:S}}export{_ as computePosition,k as getDirection,w as parsePlacement};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popover-kit/svelte",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -34,9 +34,6 @@
|
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"svelte": "^5.0.0"
|
|
36
36
|
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"lucide-svelte": "^0.462.0"
|
|
39
|
-
},
|
|
40
37
|
"devDependencies": {
|
|
41
38
|
"@sveltejs/package": "^2.3.0",
|
|
42
39
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
@@ -63,7 +60,6 @@
|
|
|
63
60
|
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
64
61
|
"typecheck:test": "tsc -p tsconfig.test.json",
|
|
65
62
|
"test": "vitest run",
|
|
66
|
-
"test:watch": "vitest"
|
|
67
|
-
"prepublishOnlyx": "pnpm run typecheck && pnpm run test && pnpm run build"
|
|
63
|
+
"test:watch": "vitest"
|
|
68
64
|
}
|
|
69
65
|
}
|
|
File without changes
|