@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 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`. Ships with `lucide-svelte` as a regular
26
- dependency (used for the built-in close icon).
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/types.js';
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/config.js';
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
- let ctrl: PopoverController;
78
-
79
- if (injectedCtrl instanceof PopoverController) {
80
- ctrl = injectedCtrl;
81
- } else {
83
+ const ctrl: PopoverController = untrack(() => {
84
+ if (injectedCtrl instanceof PopoverController) {
85
+ return injectedCtrl;
86
+ }
82
87
  owned = true;
83
- ctrl = new PopoverController({
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 { X } from 'lucide-svelte';
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
- <X class="h-3 w-3" aria-hidden="true" />
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>
@@ -12,7 +12,7 @@
12
12
  * {/snippet}
13
13
  */
14
14
  import type { Snippet } from 'svelte';
15
- import type { TriggerSnippetCtx } from '../types/types.js';
15
+ import type { TriggerSnippetCtx } from '../types.js';
16
16
 
17
17
  interface Props {
18
18
  ctx: TriggerSnippetCtx;
@@ -5,22 +5,35 @@
5
5
  //
6
6
  // Open sequence (critical — previous versions got this wrong):
7
7
  //
8
- // 1. open() sets state → 'opening' (isVisible = true) and returns.
9
- // 2. Svelte sees isVisible changed, mounts the panel element in the DOM.
10
- // 3. Svelte runs $effects. The $effect in Popover.svelte sees panelEl is
11
- // now set and calls ctrl.attachPanel(el).
12
- // 4. attachPanel() detects state === 'opening' → calls #finishOpen().
13
- // 5. #finishOpen() reads panel dimensions (element is in DOM), computes
14
- // position, sets up listeners, sets state → 'open'.
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/config.js";
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.isVisible) {
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(). Nothing else happens here.
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.isVisible)
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
- const result = computePosition(this.#triggerEl, this.#panelEl, this.#placement, this.#offset);
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
- if (this.isVisible)
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/types.js';
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/types.js';
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; // actual placement used (may differ from requested after flip)
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 via getBoundingClientRect().
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 q}from"./components/Popover.svelte";import{default as V}from"./components/PopoverTrigger.svelte";import{default as W}from"./components/PopoverContent.svelte";import{default as j}from"./elements/PopoverPanel.svelte";import{default as z}from"./elements/PopoverArrow.svelte";import{PopoverController as Q}from"./controller.svelte";function x(t){if(t==="autoVertical")return{side:"bottom",alignment:null};let[o,e=null]=t.split("-");return{side:o,alignment:e}}function w(t,o,e,n,i,s){let a,c;switch(e){case"bottom":c=t.bottom+i;break;case"top":c=t.top-o.height-i;break;case"left":a=t.left-o.width-i;break;case"right":a=t.right+i;break}if(e==="top"||e==="bottom"){let r=s?t.right-o.width:t.left,d=s?t.left:t.right-o.width;n==="start"?a=r:n==="end"?a=d:a=t.left+(t.width-o.width)/2}else n==="start"?c=t.top:n==="end"?c=t.bottom-o.height:c=t.top+(t.height-o.height)/2;return{x:a,y:c}}function T(t){return getComputedStyle(t).direction==="rtl"?"rtl":"ltr"}function v(t,o,e,n=6){return Math.max(n,Math.min(e-o-n,t))}function O(t,o,e,n,i){let s=window.innerWidth,a=window.innerHeight;switch(t){case"bottom":return o.bottom+i+e<=a?"bottom":"top";case"top":return o.top-i-e>=0?"top":"bottom";case"right":return o.right+i+n<=s?"right":"left";case"left":return o.left-i-n>=0?"left":"right"}}function _(t,o,e,n){let i=t.getBoundingClientRect(),s=o.getBoundingClientRect(),a=window.innerWidth,c=window.innerHeight,r=T(t)==="rtl",{side:d,alignment:p}=x(e),u=O(d,i,s.height,s.width,n),f=e==="autoVertical"?u==="top"?"top":"bottom":u===d?e:p?`${u}-${p}`:u,{x:m,y:h}=w(i,s,u,p,n,r);return u==="top"||u==="bottom"?m=v(m,s.width,a):h=v(h,s.height,c),{x:m,y:h,placement:f}}var L={placement:"bottom",offset:8,trapFocus:!0,closeOnOutsideClick:!0,closeOnEsc:!0};var k=200,A=["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",b={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))"},D=0;function S(t="pop"){return`${t}-${++D}`}function I(t,o){let e=!1,n=document.activeElement;function i(){return Array.from(t.querySelectorAll(A)).filter(r=>!r.hasAttribute("disabled")&&r.offsetParent!==null)}function s(r){if(!e||r.key!=="Tab")return;let d=i();if(!d.length){r.preventDefault();return}let p=d[0],u=d[d.length-1],f=document.activeElement;r.shiftKey?(f===p||!t.contains(f))&&(r.preventDefault(),u.focus()):(f===u||!t.contains(f))&&(r.preventDefault(),p.focus())}function a(){if(e)return;e=!0,n=document.activeElement,document.addEventListener("keydown",s,!0);let r=i()[0]??t;requestAnimationFrame(()=>r.focus())}function c(){if(!e)return;e=!1,document.removeEventListener("keydown",s,!0);let r=o??n;r&&typeof r.focus=="function"&&requestAnimationFrame(()=>r.focus())}return{activate:a,deactivate:c,destroy:c}}var l=null;function P(){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 F(t,o=80){let e=P();e.textContent="",setTimeout(()=>{e.textContent=t},o)}function y(t,o){function e(n){let i=n.target;t.some(s=>s.contains(i))||o(n)}return document.addEventListener("pointerdown",e,!0),()=>document.removeEventListener("pointerdown",e,!0)}function M(t={}){return{defaults:t.defaults??{},tokens:{...b,...t.theme}}}export{k as ANIMATION_DURATION_MS,L as DEFAULT_OPTIONS,b as DEFAULT_THEME_TOKENS,q as Popover,z as PopoverArrow,W as PopoverContent,Q as PopoverController,j as PopoverPanel,V as PopoverTrigger,F as announce,_ as computePosition,I as createFocusTrap,M as createPopoverPlugin,T as getDirection,y as onClickOutside,x as parsePlacement,S as uid};
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
@@ -1 +1 @@
1
- import{FOCUSABLE_SELECTOR as v,LIVE_REGION_ID as m}from"../constants/config.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};
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};
@@ -1 +1 @@
1
- function p(t){if(t==="autoVertical")return{side:"bottom",alignment:null};const[o,n=null]=t.split("-");return{side:o,alignment:n}}const S={top:"bottom",bottom:"top",left:"right",right:"left"};function f(t,o,n,i,e,h){let l,s;switch(n){case"bottom":s=t.bottom+e;break;case"top":s=t.top-o.height-e;break;case"left":l=t.left-o.width-e;break;case"right":l=t.right+e;break}if(n==="top"||n==="bottom"){const d=h?t.right-o.width:t.left,r=h?t.left:t.right-o.width;i==="start"?l=d:i==="end"?l=r:l=t.left+(t.width-o.width)/2}else i==="start"?s=t.top:i==="end"?s=t.bottom-o.height:s=t.top+(t.height-o.height)/2;return{x:l,y:s}}function v(t){return getComputedStyle(t).direction==="rtl"?"rtl":"ltr"}function w(t,o,n,i=6){return Math.max(i,Math.min(n-o-i,t))}function x(t,o,n,i,e){const h=window.innerWidth,l=window.innerHeight;switch(t){case"bottom":return o.bottom+e+n<=l?"bottom":"top";case"top":return o.top-e-n>=0?"top":"bottom";case"right":return o.right+e+i<=h?"right":"left";case"left":return o.left-e-i>=0?"left":"right"}}function P(t,o,n,i){const e=t.getBoundingClientRect(),h=o.getBoundingClientRect(),l=window.innerWidth,s=window.innerHeight,d=v(t)==="rtl";let{side:r,alignment:c}=p(n);const u=x(r,e,h.height,h.width,i),b=n==="autoVertical"?u==="top"?"top":"bottom":u===r?n:c?`${u}-${c}`:u;let{x:m,y:a}=f(e,h,u,c,i,d);return u==="top"||u==="bottom"?m=w(m,h.width,l):a=w(a,h.height,s),{x:m,y:a,placement:b}}export{P as computePosition,v as getDirection,p as parsePlacement};
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.0",
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