@x33025/sveltely 0.0.44 → 0.0.45
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/dist/components/Popover/Popover.svelte +312 -0
- package/dist/components/Popover/Popover.svelte.d.ts +12 -0
- package/dist/components/Popover/index.d.ts +1 -0
- package/dist/components/Popover/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/style/index.css +4 -0
- package/dist/style.css +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import { portalContent } from '../../actions/portal';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
trigger: Snippet;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
open?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
align?: 'left' | 'center' | 'right';
|
|
12
|
+
anchor?: 'top' | 'bottom' | 'leading' | 'trailing';
|
|
13
|
+
}
|
|
14
|
+
type Anchor = 'top' | 'bottom' | 'leading' | 'trailing';
|
|
15
|
+
type Align = 'left' | 'center' | 'right';
|
|
16
|
+
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%)';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
trigger,
|
|
31
|
+
children,
|
|
32
|
+
open = $bindable(false),
|
|
33
|
+
class: className = '',
|
|
34
|
+
align = 'left',
|
|
35
|
+
anchor = 'bottom'
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
let triggerEl = $state<HTMLElement | null>(null);
|
|
39
|
+
let panelEl = $state<HTMLElement | null>(null);
|
|
40
|
+
let panelCoords = $state({ top: 0, left: 0 });
|
|
41
|
+
let panelTransform = $state('none');
|
|
42
|
+
let resolvedAnchor = $state<Anchor>('bottom');
|
|
43
|
+
|
|
44
|
+
const isInsideRect = (rect: DOMRect, x: number, y: number) =>
|
|
45
|
+
x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
46
|
+
|
|
47
|
+
const pointInPolygon = (point: Point, polygon: Point[]) => {
|
|
48
|
+
let inside = false;
|
|
49
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
50
|
+
const xi = polygon[i].x;
|
|
51
|
+
const yi = polygon[i].y;
|
|
52
|
+
const xj = polygon[j].x;
|
|
53
|
+
const yj = polygon[j].y;
|
|
54
|
+
const intersects =
|
|
55
|
+
yi > point.y !== yj > point.y &&
|
|
56
|
+
point.x < ((xj - xi) * (point.y - yi)) / (yj - yi + Number.EPSILON) + xi;
|
|
57
|
+
if (intersects) inside = !inside;
|
|
58
|
+
}
|
|
59
|
+
return inside;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getSafePolygon = (
|
|
63
|
+
triggerRect: DOMRect,
|
|
64
|
+
popoverRect: DOMRect,
|
|
65
|
+
currentAnchor: Anchor
|
|
66
|
+
): Point[] => {
|
|
67
|
+
if (currentAnchor === 'bottom') {
|
|
68
|
+
return [
|
|
69
|
+
{ x: triggerRect.left, y: triggerRect.top },
|
|
70
|
+
{ x: triggerRect.right, y: triggerRect.top },
|
|
71
|
+
{ x: popoverRect.right, y: popoverRect.top },
|
|
72
|
+
{ x: popoverRect.left, y: popoverRect.top }
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (currentAnchor === 'top') {
|
|
77
|
+
return [
|
|
78
|
+
{ x: triggerRect.left, y: triggerRect.bottom },
|
|
79
|
+
{ x: triggerRect.right, y: triggerRect.bottom },
|
|
80
|
+
{ x: popoverRect.right, y: popoverRect.bottom },
|
|
81
|
+
{ x: popoverRect.left, y: popoverRect.bottom }
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (currentAnchor === 'leading') {
|
|
86
|
+
return [
|
|
87
|
+
{ x: triggerRect.right, y: triggerRect.top },
|
|
88
|
+
{ x: triggerRect.right, y: triggerRect.bottom },
|
|
89
|
+
{ x: popoverRect.right, y: popoverRect.bottom },
|
|
90
|
+
{ x: popoverRect.right, y: popoverRect.top }
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [
|
|
95
|
+
{ x: triggerRect.left, y: triggerRect.top },
|
|
96
|
+
{ x: triggerRect.left, y: triggerRect.bottom },
|
|
97
|
+
{ x: popoverRect.left, y: popoverRect.bottom },
|
|
98
|
+
{ x: popoverRect.left, y: popoverRect.top }
|
|
99
|
+
];
|
|
100
|
+
};
|
|
101
|
+
|
|
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';
|
|
112
|
+
};
|
|
113
|
+
|
|
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' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (nextAnchor === 'leading') {
|
|
143
|
+
return {
|
|
144
|
+
top: rect.top + window.scrollY,
|
|
145
|
+
left: rect.left + window.scrollX,
|
|
146
|
+
transform: 'translateX(-100%)'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
top: rect.top + window.scrollY,
|
|
152
|
+
left: rect.right + window.scrollX,
|
|
153
|
+
transform: 'none'
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
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
|
+
}
|
|
171
|
+
|
|
172
|
+
return { left, top, right: left + width, bottom: top + height };
|
|
173
|
+
};
|
|
174
|
+
|
|
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;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const positionPanel = () => {
|
|
187
|
+
if (!triggerEl || !panelEl) return;
|
|
188
|
+
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;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
async function openPanel() {
|
|
225
|
+
open = true;
|
|
226
|
+
await tick();
|
|
227
|
+
positionPanel();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function closePanel() {
|
|
231
|
+
open = false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function toggle() {
|
|
235
|
+
if (open) {
|
|
236
|
+
closePanel();
|
|
237
|
+
} else {
|
|
238
|
+
void openPanel();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleOutsideClick(event: MouseEvent) {
|
|
243
|
+
if (!open) return;
|
|
244
|
+
const target = event.target as HTMLElement;
|
|
245
|
+
if (target.closest('[data-popover-root]') || target.closest('[data-popover]')) return;
|
|
246
|
+
closePanel();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function handleEscape(event: KeyboardEvent) {
|
|
250
|
+
if (!open) return;
|
|
251
|
+
if (event.key === 'Escape') closePanel();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function handlePointerMove(event: MouseEvent) {
|
|
255
|
+
if (!open || !triggerEl || !panelEl) return;
|
|
256
|
+
|
|
257
|
+
const pointer = { x: event.clientX, y: event.clientY };
|
|
258
|
+
const triggerRect = triggerEl.getBoundingClientRect();
|
|
259
|
+
const popoverRect = panelEl.getBoundingClientRect();
|
|
260
|
+
const safePolygon = getSafePolygon(triggerRect, popoverRect, resolvedAnchor);
|
|
261
|
+
|
|
262
|
+
if (isInsideRect(triggerRect, pointer.x, pointer.y)) return;
|
|
263
|
+
if (isInsideRect(popoverRect, pointer.x, pointer.y)) return;
|
|
264
|
+
if (pointInPolygon(pointer, safePolygon)) return;
|
|
265
|
+
|
|
266
|
+
closePanel();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
$effect(() => {
|
|
270
|
+
if (!open) return;
|
|
271
|
+
void tick().then(() => {
|
|
272
|
+
positionPanel();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<svelte:window
|
|
278
|
+
onclick={handleOutsideClick}
|
|
279
|
+
onscroll={closePanel}
|
|
280
|
+
onresize={closePanel}
|
|
281
|
+
onkeydown={handleEscape}
|
|
282
|
+
onmousemove={handlePointerMove}
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<div class="relative inline-block text-left" data-popover-root>
|
|
286
|
+
<div bind:this={triggerEl}>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
class="text-left"
|
|
290
|
+
onclick={toggle}
|
|
291
|
+
aria-expanded={open}
|
|
292
|
+
aria-haspopup="dialog"
|
|
293
|
+
>
|
|
294
|
+
{@render trigger()}
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{#if open}
|
|
299
|
+
<div
|
|
300
|
+
use:portalContent
|
|
301
|
+
class="popover fixed z-50 focus:outline-none {className}"
|
|
302
|
+
data-popover
|
|
303
|
+
style="top: {panelCoords.top}px; left: {panelCoords.left}px; transform: {panelTransform};"
|
|
304
|
+
role="dialog"
|
|
305
|
+
aria-modal="false"
|
|
306
|
+
tabindex="-1"
|
|
307
|
+
bind:this={panelEl}
|
|
308
|
+
>
|
|
309
|
+
{@render children()}
|
|
310
|
+
</div>
|
|
311
|
+
{/if}
|
|
312
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
trigger: Snippet;
|
|
4
|
+
children: Snippet;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
align?: 'left' | 'center' | 'right';
|
|
8
|
+
anchor?: 'top' | 'bottom' | 'leading' | 'trailing';
|
|
9
|
+
}
|
|
10
|
+
declare const Popover: import("svelte").Component<Props, {}, "open">;
|
|
11
|
+
type Popover = ReturnType<typeof Popover>;
|
|
12
|
+
export default Popover;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Popover.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Popover.svelte';
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,5 @@ export { default as Spinner } from './components/Spinner.svelte';
|
|
|
11
11
|
export { default as TextShimmer } from './components/TextShimmer.svelte';
|
|
12
12
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
13
13
|
export { default as Dropdown } from './components/Dropdown';
|
|
14
|
+
export { default as Popover } from './components/Popover';
|
|
14
15
|
export { default as ChipInput } from './components/ChipInput.svelte';
|
package/dist/index.js
CHANGED
|
@@ -11,4 +11,5 @@ export { default as Spinner } from './components/Spinner.svelte';
|
|
|
11
11
|
export { default as TextShimmer } from './components/TextShimmer.svelte';
|
|
12
12
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
13
13
|
export { default as Dropdown } from './components/Dropdown';
|
|
14
|
+
export { default as Popover } from './components/Popover';
|
|
14
15
|
export { default as ChipInput } from './components/ChipInput.svelte';
|
package/dist/style/index.css
CHANGED
package/dist/style.css
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
--color-zinc-100: oklch(96.7% 0.001 286.375);
|
|
16
16
|
--color-zinc-200: oklch(92% 0.004 286.32);
|
|
17
17
|
--color-zinc-300: oklch(87.1% 0.006 286.286);
|
|
18
|
+
--color-zinc-400: oklch(70.5% 0.015 286.067);
|
|
18
19
|
--color-zinc-500: oklch(55.2% 0.016 285.938);
|
|
19
20
|
--color-zinc-700: oklch(37% 0.013 285.805);
|
|
20
21
|
--color-zinc-900: oklch(21% 0.006 285.885);
|
|
@@ -275,6 +276,9 @@
|
|
|
275
276
|
.w-60 {
|
|
276
277
|
width: calc(var(--spacing) * 60);
|
|
277
278
|
}
|
|
279
|
+
.w-64 {
|
|
280
|
+
width: calc(var(--spacing) * 64);
|
|
281
|
+
}
|
|
278
282
|
.w-fit {
|
|
279
283
|
width: fit-content;
|
|
280
284
|
}
|
|
@@ -384,6 +388,9 @@
|
|
|
384
388
|
.px-2 {
|
|
385
389
|
padding-inline: calc(var(--spacing) * 2);
|
|
386
390
|
}
|
|
391
|
+
.px-3 {
|
|
392
|
+
padding-inline: calc(var(--spacing) * 3);
|
|
393
|
+
}
|
|
387
394
|
.px-4 {
|
|
388
395
|
padding-inline: calc(var(--spacing) * 4);
|
|
389
396
|
}
|
|
@@ -491,6 +498,11 @@
|
|
|
491
498
|
}
|
|
492
499
|
}
|
|
493
500
|
}
|
|
501
|
+
.focus\:border-zinc-400 {
|
|
502
|
+
&:focus {
|
|
503
|
+
border-color: var(--color-zinc-400);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
494
506
|
.focus\:outline-none {
|
|
495
507
|
&:focus {
|
|
496
508
|
--tw-outline-style: none;
|
|
@@ -644,6 +656,16 @@
|
|
|
644
656
|
.dropdown-item:hover {
|
|
645
657
|
background-color: var(--color-zinc-100);
|
|
646
658
|
}
|
|
659
|
+
.popover {
|
|
660
|
+
border-radius: var(--radius-md);
|
|
661
|
+
border-style: var(--tw-border-style);
|
|
662
|
+
border-width: 1px;
|
|
663
|
+
border-color: var(--color-gray-200);
|
|
664
|
+
background-color: var(--color-white);
|
|
665
|
+
padding: calc(var(--spacing) * 2);
|
|
666
|
+
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
667
|
+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
668
|
+
}
|
|
647
669
|
.sheet {
|
|
648
670
|
border-radius: var(--radius-md);
|
|
649
671
|
background-color: var(--color-white);
|