openguessr-ui 0.3.2 → 0.3.3
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 +2 -0
- package/base.css +4 -3
- package/components/ConfirmPopup.svelte +1 -1
- package/components/ErrorPopup.svelte +1 -1
- package/components/ExperimentSign.svelte +1 -1
- package/components/Notification.svelte +1 -1
- package/components/Popup.svelte +237 -16
- package/components/RoomCodeInput.svelte +1 -1
- package/index.js +4 -4
- package/package.json +3 -6
- package/utils/clamp.js +1 -0
- package/utils/cubic-bezier.js +12 -0
- package/utils/scroll-check.js +20 -0
- /package/{confirmPopup.svelte.js → utils/confirm-popup.svelte.js} +0 -0
- /package/{errorPopup.svelte.js → utils/error-popup.svelte.js} +0 -0
- /package/{notifications.svelte.js → utils/notifications.svelte.js} +0 -0
- /package/{tooltip.svelte.js → utils/tooltip.svelte.js} +0 -0
package/README.md
CHANGED
|
@@ -249,6 +249,8 @@ The popup component is used for dialogs. It takes the following props:
|
|
|
249
249
|
- `frameless`: Reduces the padding, e.g. for iframes
|
|
250
250
|
- `onuserclose`: Called when the user closes the popup
|
|
251
251
|
|
|
252
|
+
On mobile devices (portrait orientation, and narrow in terms of width), the popup transforms into a drawer.
|
|
253
|
+
|
|
252
254
|
A common design pattern is to include a heading at the top of a popup. For this, the built-in `popup-title` class can be used:
|
|
253
255
|
|
|
254
256
|
```html
|
package/base.css
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
html {
|
|
15
15
|
/* This UI library is for games, not sites, double-tap to zoom or drag down to refresh are not wanted */
|
|
16
16
|
overscroll-behavior: none;
|
|
17
|
-
|
|
17
|
+
/* Block double-tap or pinch to zoom */
|
|
18
|
+
touch-action: pan-x pan-y;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
html * {
|
|
@@ -23,8 +24,8 @@ html * {
|
|
|
23
24
|
|
|
24
25
|
body {
|
|
25
26
|
font-family: Barlow, sans-serif;
|
|
26
|
-
/*
|
|
27
|
-
touch-action:
|
|
27
|
+
/* Set touch action again since body could be a scroll container */
|
|
28
|
+
touch-action: pan-x pan-y;
|
|
28
29
|
background-color: var(--background-color);
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { notification, hideNotification } from "../notifications.svelte.js";
|
|
2
|
+
import { notification, hideNotification } from "../utils/notifications.svelte.js";
|
|
3
3
|
import { Check, SquareArrowOutUpRight, X } from "@lucide/svelte";
|
|
4
4
|
import { fly } from "svelte/transition";
|
|
5
5
|
|
package/components/Popup.svelte
CHANGED
|
@@ -5,13 +5,145 @@
|
|
|
5
5
|
<script>
|
|
6
6
|
import { X } from "@lucide/svelte";
|
|
7
7
|
import { untrack } from "svelte";
|
|
8
|
+
import { on } from "svelte/events";
|
|
9
|
+
import { cubicBezier } from "../utils/cubic-bezier.js";
|
|
10
|
+
import { clamp } from "../utils/clamp.js";
|
|
11
|
+
import { isScrolledDown, canScrollDown } from "../utils/scroll-check.js";
|
|
12
|
+
import { scale, fade } from "svelte/transition";
|
|
8
13
|
import { backOut } from "svelte/easing";
|
|
9
|
-
import {
|
|
10
|
-
|
|
14
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
11
15
|
let { children, frameless, slim, verySlim, ultraSlim, open = $bindable(false), onuserclose } = $props();
|
|
12
16
|
|
|
13
17
|
let zIndex = $state(0);
|
|
14
18
|
|
|
19
|
+
// Transitions ------------------------------------------------------------------------------
|
|
20
|
+
const DRAWER_TIMING = { duration: 500, easing: cubicBezier(0.32, 0.72, 0, 1) };
|
|
21
|
+
const DRAWER_HIDDEN_OVERSHOOT = 50; // Added negative closed y-pos offset to move the panel shadow off-screen (100px blur - 50px --layout-spacer)
|
|
22
|
+
const popupToDrawer = new MediaQuery("(orientation: portrait) and (max-width: 500px)");
|
|
23
|
+
|
|
24
|
+
const popupHolderTransition = (node) => (popupToDrawer.current ? { duration: 0 } : fade(node, { duration: 100 }));
|
|
25
|
+
|
|
26
|
+
// Replaced when dragged closed, so the close continues the swipe's speed
|
|
27
|
+
let closeTiming = DRAWER_TIMING;
|
|
28
|
+
|
|
29
|
+
function windowTransition(node, _params, { direction }) {
|
|
30
|
+
if (!popupToDrawer.current) return scale(node, { easing: backOut, duration: 250, start: 0.9 });
|
|
31
|
+
|
|
32
|
+
if (direction == "in") {
|
|
33
|
+
closeTiming = DRAWER_TIMING;
|
|
34
|
+
drawerVerticalOffset = node.offsetHeight + DRAWER_HIDDEN_OVERSHOOT;
|
|
35
|
+
animateDrawerOpen(); // Opening is animated by this function, so a drag can take over at any point
|
|
36
|
+
return { duration: 0 };
|
|
37
|
+
} else {
|
|
38
|
+
// Close from wherever the drawer currently is
|
|
39
|
+
cancelAnimationFrame(openAnimationFrame);
|
|
40
|
+
const start = drawerVerticalOffset;
|
|
41
|
+
return {
|
|
42
|
+
...closeTiming,
|
|
43
|
+
css: (t) => `transform: translate3d(0, ${closedOffset + (start - closedOffset) * t}px, 0)`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function drawerOverlayOutTransition() {
|
|
49
|
+
if (!popupToDrawer.current) return { duration: 0 };
|
|
50
|
+
const start = overlayProgress; // Current strength
|
|
51
|
+
return {
|
|
52
|
+
...closeTiming,
|
|
53
|
+
// Fade color opacity and blur strength relative to time factor t (0-1)
|
|
54
|
+
css: (t) =>
|
|
55
|
+
`background-color: color-mix(in srgb, var(--overlay-color) ${start * t * 100}%, transparent); backdrop-filter: blur(calc(var(--weak-blur) * ${start * t}));`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Drawer logic ------------------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
// Bindings
|
|
62
|
+
let contentEl = $state();
|
|
63
|
+
let uiWindowHeight = $state();
|
|
64
|
+
|
|
65
|
+
// Drawer state
|
|
66
|
+
let dragStartY = $state();
|
|
67
|
+
let dragStartVerticalOffset;
|
|
68
|
+
let isDragging = $state(false);
|
|
69
|
+
let drawerVerticalOffset = $state(0);
|
|
70
|
+
let velocitySamples = $state([]); // Y positions with timestamps
|
|
71
|
+
let openAnimationFrame; // Latest open animation frame for cancelling it
|
|
72
|
+
|
|
73
|
+
const closedOffset = $derived(uiWindowHeight + DRAWER_HIDDEN_OVERSHOOT);
|
|
74
|
+
const overlayProgress = $derived(Math.min(1, 1 - drawerVerticalOffset / closedOffset));
|
|
75
|
+
|
|
76
|
+
// Animate drawer to fully open
|
|
77
|
+
const animateDrawerOpen = () => {
|
|
78
|
+
if (drawerVerticalOffset == 0) return; // Skip when the drawer is already fully open
|
|
79
|
+
const start = drawerVerticalOffset;
|
|
80
|
+
const startTime = performance.now();
|
|
81
|
+
const step = (now) => {
|
|
82
|
+
const progress = Math.min(1, (now - startTime) / DRAWER_TIMING.duration); // Progress (0-1)
|
|
83
|
+
drawerVerticalOffset = start * (1 - DRAWER_TIMING.easing(progress));
|
|
84
|
+
if (progress < 1) openAnimationFrame = requestAnimationFrame(step);
|
|
85
|
+
};
|
|
86
|
+
openAnimationFrame = requestAnimationFrame(step);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const dragStart = (event) => {
|
|
90
|
+
// Abort if not a drawer, or if content should be scrolled by the "finger downwards" movement because it isn't scrolled all the way up
|
|
91
|
+
if (!popupToDrawer.current || isScrolledDown(event.target, contentEl)) return;
|
|
92
|
+
|
|
93
|
+
// Catch the drawer where it is (could still be animating open)
|
|
94
|
+
cancelAnimationFrame(openAnimationFrame);
|
|
95
|
+
dragStartVerticalOffset = drawerVerticalOffset;
|
|
96
|
+
dragStartY = event.touches[0].clientY;
|
|
97
|
+
velocitySamples = [{ y: dragStartY, time: performance.now() }];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const drag = (event) => {
|
|
101
|
+
if (dragStartY == null) return;
|
|
102
|
+
|
|
103
|
+
const eventY = event.touches[0].clientY;
|
|
104
|
+
|
|
105
|
+
// Cancel if dragging finger upwards while the touched element (or an ancestor up to the content) can still scroll down
|
|
106
|
+
// Mainly relevant for the slow stretch animation when the drwaer is dragged upwards (which shouldn't show when content is scrolled down)
|
|
107
|
+
if (!isDragging && eventY <= dragStartY && canScrollDown(event.target, contentEl)) {
|
|
108
|
+
dragStartY = null;
|
|
109
|
+
return animateDrawerOpen(); // Touch could've stopped the drawer during open, since it's a content scroll not a drag, resume
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
event.preventDefault(); // Prevent page scroll
|
|
113
|
+
isDragging = true;
|
|
114
|
+
|
|
115
|
+
// Collect samples for velocity calc
|
|
116
|
+
const now = performance.now();
|
|
117
|
+
velocitySamples.push({ y: eventY, time: now });
|
|
118
|
+
while (velocitySamples.length > 2 && now - velocitySamples[0].time > 80) velocitySamples.shift(); // Keep the last 80ms
|
|
119
|
+
|
|
120
|
+
// Apply offset
|
|
121
|
+
const delta = eventY - dragStartY + dragStartVerticalOffset;
|
|
122
|
+
drawerVerticalOffset = delta > 0 ? delta : delta * 0.05; // Allow dragging the drawer upwards, but make it less effective (that's what the x0.05 is for)
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const dragEnd = () => {
|
|
126
|
+
if (dragStartY == null) return;
|
|
127
|
+
dragStartY = null;
|
|
128
|
+
|
|
129
|
+
if (!isDragging) return animateDrawerOpen(); // Single tap without drag, resume opening if it was caught mid-animation
|
|
130
|
+
isDragging = false;
|
|
131
|
+
|
|
132
|
+
// Calculate velocity
|
|
133
|
+
const first = velocitySamples[0];
|
|
134
|
+
const last = velocitySamples.at(-1);
|
|
135
|
+
const velocity = last.time > first.time ? (last.y - first.y) / (last.time - first.time) : 0; // Pixels per ms (time is ms)
|
|
136
|
+
|
|
137
|
+
// If swiped with enough velocity or past 35%, animate it shut, otherwise open fully again
|
|
138
|
+
if (velocity > 0.4 || drawerVerticalOffset > uiWindowHeight * 0.35) {
|
|
139
|
+
const duration = clamp((closedOffset - drawerVerticalOffset) / Math.max(velocity, 1.5), 180, 420); // Faster drag velocity means lower duration
|
|
140
|
+
closeTiming = { duration, easing: cubicBezier(0.45, 0.55, 0.4, 0.92) };
|
|
141
|
+
close();
|
|
142
|
+
} else animateDrawerOpen(); // Too little drag, snap back to fully open state
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Automatic z-indexing ---------------------------------------------------------------------
|
|
146
|
+
|
|
15
147
|
// Helper to remove the current popup's z-index
|
|
16
148
|
const removeZIndex = () => {
|
|
17
149
|
const index = openZIndexes.indexOf(zIndex);
|
|
@@ -33,6 +165,7 @@
|
|
|
33
165
|
}
|
|
34
166
|
});
|
|
35
167
|
|
|
168
|
+
// Close function ----------------------------------------------------------------------------
|
|
36
169
|
function close() {
|
|
37
170
|
onuserclose?.();
|
|
38
171
|
open = false;
|
|
@@ -53,24 +186,62 @@
|
|
|
53
186
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
54
187
|
<div
|
|
55
188
|
class="popup-holder"
|
|
189
|
+
class:drawer-holder={popupToDrawer.current}
|
|
56
190
|
style:z-index={zIndex}
|
|
57
|
-
|
|
191
|
+
in:popupHolderTransition
|
|
192
|
+
out:popupHolderTransition
|
|
58
193
|
onclick={(e) => {
|
|
59
194
|
if (e.target == e.currentTarget) close();
|
|
60
195
|
}}
|
|
61
196
|
role="dialog"
|
|
62
197
|
tabindex={0}
|
|
63
198
|
>
|
|
199
|
+
<!-- Separate from the holder, a backdrop filter on an ancestor wouldn't work -->
|
|
200
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
201
|
+
<div
|
|
202
|
+
class="drawer-overlay"
|
|
203
|
+
out:drawerOverlayOutTransition
|
|
204
|
+
onclick={close}
|
|
205
|
+
style:background-color="color-mix(in srgb, var(--overlay-color) {overlayProgress * 100}%, transparent)"
|
|
206
|
+
style:backdrop-filter="blur(calc(var(--weak-blur) * {overlayProgress}))"
|
|
207
|
+
></div>
|
|
64
208
|
<div
|
|
65
209
|
class="ui-window"
|
|
66
|
-
|
|
210
|
+
role="menu"
|
|
211
|
+
tabindex="0"
|
|
212
|
+
class:drawer={popupToDrawer.current}
|
|
213
|
+
in:windowTransition
|
|
214
|
+
out:windowTransition
|
|
67
215
|
class:frame-less={frameless}
|
|
68
|
-
style:max-width={
|
|
216
|
+
style:max-width={popupToDrawer.current
|
|
217
|
+
? "none"
|
|
218
|
+
: ultraSlim
|
|
219
|
+
? "450px"
|
|
220
|
+
: verySlim
|
|
221
|
+
? "600px"
|
|
222
|
+
: slim
|
|
223
|
+
? "750px"
|
|
224
|
+
: "850px"}
|
|
225
|
+
style:transform={popupToDrawer.current && drawerVerticalOffset
|
|
226
|
+
? `translate3d(0, ${drawerVerticalOffset}px, 0)`
|
|
227
|
+
: ""}
|
|
228
|
+
bind:offsetHeight={uiWindowHeight}
|
|
229
|
+
{@attach (node) => {
|
|
230
|
+
// Needed since regular Svelte's touchmove is passive
|
|
231
|
+
const cleanup = on(node, "touchmove", drag, { passive: false });
|
|
232
|
+
return cleanup;
|
|
233
|
+
}}
|
|
234
|
+
ontouchstart={dragStart}
|
|
235
|
+
ontouchend={dragEnd}
|
|
236
|
+
ontouchcancel={dragEnd}
|
|
69
237
|
>
|
|
238
|
+
{#if popupToDrawer.current && !frameless}
|
|
239
|
+
<div class="drawer-handle"></div>
|
|
240
|
+
{/if}
|
|
70
241
|
<button class="close-popup-button standard-button" aria-label="close" onclick={close}
|
|
71
242
|
><X strokeWidth={2.25} /></button
|
|
72
243
|
>
|
|
73
|
-
<div class="content of-top of-bottom" class:content-frameless={frameless}>
|
|
244
|
+
<div class="content of-top of-bottom" class:content-frameless={frameless} bind:this={contentEl}>
|
|
74
245
|
{@render children?.()}
|
|
75
246
|
</div>
|
|
76
247
|
</div>
|
|
@@ -90,6 +261,7 @@
|
|
|
90
261
|
background-color: var(--overlay-color);
|
|
91
262
|
pointer-events: auto;
|
|
92
263
|
backdrop-filter: blur(var(--weak-blur));
|
|
264
|
+
--window-padding: calc(var(--layout-margin) + var(--content-margin));
|
|
93
265
|
}
|
|
94
266
|
|
|
95
267
|
.close-popup-button {
|
|
@@ -102,10 +274,6 @@
|
|
|
102
274
|
z-index: 10;
|
|
103
275
|
}
|
|
104
276
|
|
|
105
|
-
.frame-less {
|
|
106
|
-
padding: var(--content-margin) !important;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
277
|
.content {
|
|
110
278
|
position: relative;
|
|
111
279
|
overflow-y: auto;
|
|
@@ -119,18 +287,18 @@
|
|
|
119
287
|
);
|
|
120
288
|
width: 100%;
|
|
121
289
|
scrollbar-width: none;
|
|
290
|
+
overscroll-behavior: contain;
|
|
122
291
|
border-radius: var(--box-margin);
|
|
123
292
|
}
|
|
124
293
|
|
|
125
294
|
.content-frameless {
|
|
126
295
|
max-height: calc(100dvh - var(--layout-spacer) - env(safe-area-inset-top) - env(safe-area-inset-bottom));
|
|
127
|
-
margin-bottom: -4px;
|
|
128
296
|
border-radius: 0;
|
|
129
297
|
}
|
|
130
298
|
|
|
131
299
|
.ui-window {
|
|
132
300
|
width: calc(100% - var(--layout-margin) * 2);
|
|
133
|
-
padding:
|
|
301
|
+
padding: var(--window-padding);
|
|
134
302
|
border-radius: calc(var(--layout-margin) + var(--box-margin));
|
|
135
303
|
text-align: left;
|
|
136
304
|
position: relative;
|
|
@@ -139,11 +307,64 @@
|
|
|
139
307
|
box-shadow:
|
|
140
308
|
0 2px 100px rgba(5, 6, 10, 0.5),
|
|
141
309
|
inset 0 0.05rem 1px hsla(0, 0%, 100%, 0.2),
|
|
142
|
-
inset 0 -0.2rem 1px rgba(0, 0, 2, 0.2)
|
|
310
|
+
inset 0 -0.2rem 1px rgba(0, 0, 2, 0.2);
|
|
143
311
|
color: white;
|
|
144
312
|
pointer-events: auto;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.frame-less {
|
|
316
|
+
padding: var(--content-margin);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.drawer-overlay {
|
|
320
|
+
display: none;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.drawer-holder {
|
|
324
|
+
align-items: flex-end;
|
|
325
|
+
background-color: transparent;
|
|
326
|
+
backdrop-filter: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.drawer-holder .drawer-overlay {
|
|
330
|
+
display: block;
|
|
331
|
+
position: absolute;
|
|
332
|
+
inset: 0;
|
|
333
|
+
background-color: var(--overlay-color);
|
|
334
|
+
backdrop-filter: blur(var(--weak-blur));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.drawer {
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
width: 100%;
|
|
341
|
+
/* Max height is also shorter by the negative --layout-spacer margin bottom */
|
|
342
|
+
max-height: calc(100dvh - env(safe-area-inset-top));
|
|
343
|
+
padding-bottom: calc(var(--window-padding) + env(safe-area-inset-bottom) + var(--layout-spacer));
|
|
344
|
+
margin-bottom: calc(-1 * var(--layout-spacer));
|
|
345
|
+
border-bottom-left-radius: 0;
|
|
346
|
+
border-bottom-right-radius: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.drawer.frame-less {
|
|
350
|
+
/* Add --layout-spacer since we have the negative --layout-spacer margin bottom */
|
|
351
|
+
padding-bottom: calc(max(var(--content-margin), env(safe-area-inset-bottom)) + var(--layout-spacer));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.drawer .content {
|
|
355
|
+
flex: 1;
|
|
356
|
+
min-height: 0;
|
|
357
|
+
max-height: none;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.drawer-handle {
|
|
361
|
+
position: absolute;
|
|
362
|
+
top: var(--box-margin);
|
|
363
|
+
border-radius: 4px;
|
|
364
|
+
left: 50%;
|
|
365
|
+
translate: -50% 0;
|
|
366
|
+
width: 40px;
|
|
367
|
+
height: 4px;
|
|
368
|
+
background-color: var(--line-color);
|
|
148
369
|
}
|
|
149
370
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { ArrowRight } from "@lucide/svelte";
|
|
3
|
-
import { tooltip } from "../tooltip.svelte.js";
|
|
3
|
+
import { tooltip } from "../utils/tooltip.svelte.js";
|
|
4
4
|
|
|
5
5
|
let { segments = $bindable(["", "", "", "", "", ""]), inPage = true, onsubmit } = $props(); // 6 segments for XXX-XXX
|
|
6
6
|
|
package/index.js
CHANGED
|
@@ -11,10 +11,10 @@ export { default as Tabs } from "./components/Tabs.svelte";
|
|
|
11
11
|
export { default as Ticker } from "./components/Ticker.svelte";
|
|
12
12
|
export { default as TitleSeparator } from "./components/TitleSeparator.svelte";
|
|
13
13
|
export { default as ExperimentSign } from "./components/ExperimentSign.svelte";
|
|
14
|
-
export { tooltip } from "./tooltip.svelte.js";
|
|
15
|
-
export { showNotification } from "./notifications.svelte.js";
|
|
16
|
-
export { showConfirmPopup } from "./
|
|
17
|
-
export { showErrorPopup } from "./
|
|
14
|
+
export { tooltip } from "./utils/tooltip.svelte.js";
|
|
15
|
+
export { showNotification } from "./utils/notifications.svelte.js";
|
|
16
|
+
export { showConfirmPopup } from "./utils/confirm-popup.svelte.js";
|
|
17
|
+
export { showErrorPopup } from "./utils/error-popup.svelte.js";
|
|
18
18
|
|
|
19
19
|
// Sounds
|
|
20
20
|
import basic_button from "./sound-effects/basic_button.ogg";
|
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openguessr-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite example"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"index.js",
|
|
10
|
-
"
|
|
11
|
-
"notifications.svelte.js",
|
|
12
|
-
"confirmPopup.svelte.js",
|
|
13
|
-
"errorPopup.svelte.js",
|
|
10
|
+
"utils",
|
|
14
11
|
"components",
|
|
15
12
|
"fonts",
|
|
16
13
|
"vector-graphics",
|
|
@@ -34,7 +31,7 @@
|
|
|
34
31
|
"peerDependencies": {
|
|
35
32
|
"@lucide/svelte": "^1.0.0",
|
|
36
33
|
"overfade": "^0.0.11",
|
|
37
|
-
"svelte": "^5.
|
|
34
|
+
"svelte": "^5.7.0"
|
|
38
35
|
},
|
|
39
36
|
"dependencies": {
|
|
40
37
|
"@floating-ui/dom": "^1.8.0"
|
package/utils/clamp.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function cubicBezier(x1, y1, x2, y2) {
|
|
2
|
+
const curve = (s, c1, c2) => (1 - 3 * c2 + 3 * c1) * s ** 3 + (3 * c2 - 6 * c1) * s ** 2 + 3 * c1 * s;
|
|
3
|
+
return (t) => {
|
|
4
|
+
let s = t;
|
|
5
|
+
for (let i = 0; i < 5; i++) {
|
|
6
|
+
const slope = 3 * (1 - 3 * x2 + 3 * x1) * s ** 2 + 2 * (3 * x2 - 6 * x1) * s + 3 * x1;
|
|
7
|
+
if (Math.abs(slope) < 1e-6) break;
|
|
8
|
+
s -= (curve(s, x1, x2) - t) / slope;
|
|
9
|
+
}
|
|
10
|
+
return curve(s, y1, y2);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Whether the element or any ancestor up to root is scrolled down
|
|
2
|
+
export function isScrolledDown(el, root) {
|
|
3
|
+
while (el) {
|
|
4
|
+
if (el.scrollTop > 0) return true; // Return true when a scrollable element was found
|
|
5
|
+
if (el === root) return false;
|
|
6
|
+
el = el.parentElement;
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Whether the element or any ancestor up to root can still scroll further down
|
|
12
|
+
export function canScrollDown(el, root) {
|
|
13
|
+
while (el) {
|
|
14
|
+
const scrollRemaining = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
15
|
+
if (scrollRemaining > 1) return true; // scrollTop can be fractional while scrollHeight and clientHeight are rounded
|
|
16
|
+
if (el === root) return false;
|
|
17
|
+
el = el.parentElement;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|