libraries-dev-svelte 0.1.0 → 0.2.0
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 +26 -5
- package/dist/bot-avatars/BotAvatar.svelte +67 -11
- package/dist/bot-avatars/ticker.d.ts +1 -0
- package/dist/bot-avatars/ticker.js +6 -2
- package/dist/bot-avatars/types.d.ts +4 -0
- package/dist/gooey-radial-menu/GooeyRadialMenu.svelte +74 -73
- package/dist/gooey-radial-menu/shadow.d.ts +12 -0
- package/dist/gooey-radial-menu/shadow.js +12 -0
- package/dist/gooey-radial-menu/types.d.ts +3 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Svelte 5 ports of the Libraries.dev Bot Avatars and Gooey radial plus menu. The
|
|
|
13
13
|
<BotAvatar type="clover" {state} size={64} />
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The port retains all 18 shapes; idle, working and sleeping animation states; eyes and optional state-aware mouth; pointer following and click jumps; `plastic`, `crisp`, `smooth` and `flat` shading; lighting, whirl and jump controls; reduced-motion and paused poses; seed synchronization; accessible labels; and the canvas drawing exports. Canvas properties and native click handlers pass through.
|
|
16
|
+
The port retains all 18 shapes; idle, working and sleeping animation states; eyes and optional state-aware mouth; pointer following and click jumps; `plastic`, `crisp`, `smooth` and `flat` shading; lighting, whirl and jump controls; reduced-motion and paused poses; seed synchronization; accessible labels; and the canvas drawing exports. Canvas properties and native click handlers pass through. It updates at 30 fps with a maximum device pixel ratio of 1.5 by default to reduce continuous drawing and backing-store memory on high-density screens; set `fps={60}` and `maxDpr={2}` when maximum smoothness and resolution matter more.
|
|
17
17
|
|
|
18
18
|
Types, presets, palette, shape paths, simulation, renderer, and plastic-material helpers are exported from the package entry point. See `packages/bot-avatars/README.md` in the source clone for the complete original prop descriptions.
|
|
19
19
|
|
|
@@ -21,15 +21,36 @@ Types, presets, palette, shape paths, simulation, renderer, and plastic-material
|
|
|
21
21
|
|
|
22
22
|
```svelte
|
|
23
23
|
<script lang="ts">
|
|
24
|
-
import { GooeyRadialMenu } from 'libraries-dev-svelte';
|
|
24
|
+
import { GooeyRadialMenu, gooeyShadowPresets } from 'libraries-dev-svelte';
|
|
25
25
|
</script>
|
|
26
26
|
|
|
27
|
-
<GooeyRadialMenu blur={6} contrast={18} shadow=
|
|
27
|
+
<GooeyRadialMenu bare blur={6} contrast={18} shadow={gooeyShadowPresets.light.soft} />
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
This ports the source `PlusMenu.tsx` demo and the liquid-gooey behavior it uses: filtered merged silhouettes and shadows,
|
|
30
|
+
This ports the source `PlusMenu.tsx` demo and the liquid-gooey behavior it uses: filtered merged silhouettes and shadows, synchronized CSS transform transitions for each button and its SVG blob, stagger and easing controls, close anticipation, theme-aware fill, and the full tuning panel. The normal playground exposes duration and stagger; add `?pro` on `/gooey` for the full controls, copy-values output, and reset.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### Shadow styles
|
|
33
|
+
|
|
34
|
+
`shadow` styles one silhouette behind the menu. When the buttons join, their shadow joins into one continuous outline; it is not a separate shadow on each button. The presets are split by surface theme:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
gooeyShadowPresets.light.soft
|
|
38
|
+
gooeyShadowPresets.light.floating
|
|
39
|
+
gooeyShadowPresets.dark.soft
|
|
40
|
+
gooeyShadowPresets.dark.floating
|
|
41
|
+
gooeyShadowPresets.none
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can also pass CSS `box-shadow` layers directly. Layer order is horizontal offset, vertical offset, blur, optional spread, then color. Separate layers with commas; add `inset` for an inner ring or highlight. Lengths must be numbers in pixels (`px`) or unitless values.
|
|
45
|
+
|
|
46
|
+
```svelte
|
|
47
|
+
<GooeyRadialMenu
|
|
48
|
+
bare
|
|
49
|
+
shadow="0 0 0 1px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .12)"
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Props: `blur` (6), `contrast` (18), `fill` (`var(--modal-bg, #fff)`), `shadow` (empty), `onAction` (optional callback for `'new-file'`, `'add-image'`, or `'new-folder'`), `pro` (false), and `bare` (false). `bare` renders only the menu stage and is the recommended mode when embedding it in an application; without it, the tuning panel is included. Selecting a satellite calls `onAction` when supplied and closes the menu. The host can set `--modal-bg`, `--btn-text`, `--stage-bg`, `--accent`, and `--hover-veil` to match its theme.
|
|
33
54
|
|
|
34
55
|
## Demo routes
|
|
35
56
|
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
face,
|
|
16
16
|
state: avatarState = 'default',
|
|
17
17
|
size = 64,
|
|
18
|
+
fps = 30,
|
|
19
|
+
maxDpr = 1.5,
|
|
18
20
|
color,
|
|
19
21
|
ink,
|
|
20
22
|
brightness = 1,
|
|
@@ -69,6 +71,8 @@
|
|
|
69
71
|
let sim: Sim | null = null;
|
|
70
72
|
let cfg: DrawConfig | null = null;
|
|
71
73
|
let cssSize = 0;
|
|
74
|
+
let drawContext: CanvasRenderingContext2D | null = null;
|
|
75
|
+
let bounds: DOMRect | null = null;
|
|
72
76
|
let instanceSeed = 0;
|
|
73
77
|
let speedValue = 1;
|
|
74
78
|
let interactiveValue = true;
|
|
@@ -117,14 +121,18 @@
|
|
|
117
121
|
/* A hidden ancestor measures 0: keep the last size rather than wiping the backing store. */
|
|
118
122
|
const px = el.clientWidth / OVERSCAN || cssSize || (typeof size === 'number' ? size : 64);
|
|
119
123
|
if (!px) return;
|
|
120
|
-
const dpr = Math.min(
|
|
124
|
+
const dpr = Math.min(
|
|
125
|
+
2,
|
|
126
|
+
clamp(maxDpr, 1, 2),
|
|
127
|
+
(typeof devicePixelRatio === 'number' && devicePixelRatio) || 1,
|
|
128
|
+
);
|
|
121
129
|
const wanted = Math.round(px * OVERSCAN * dpr);
|
|
122
130
|
if (el.width !== wanted || el.height !== wanted) {
|
|
123
131
|
el.width = wanted;
|
|
124
132
|
el.height = wanted;
|
|
125
133
|
}
|
|
126
134
|
cssSize = px;
|
|
127
|
-
const ctx = el.getContext('2d');
|
|
135
|
+
const ctx = drawContext ?? (drawContext = el.getContext('2d'));
|
|
128
136
|
if (!ctx) return;
|
|
129
137
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
130
138
|
current.dpr = dpr;
|
|
@@ -164,6 +172,34 @@
|
|
|
164
172
|
};
|
|
165
173
|
media?.addEventListener?.('change', updateMotion);
|
|
166
174
|
|
|
175
|
+
const el = canvas;
|
|
176
|
+
if (el) {
|
|
177
|
+
const measure = () => {
|
|
178
|
+
const measuredSize = el.clientWidth / OVERSCAN;
|
|
179
|
+
if (measuredSize > 0) cssSize = measuredSize;
|
|
180
|
+
bounds = el.getBoundingClientRect();
|
|
181
|
+
paint();
|
|
182
|
+
};
|
|
183
|
+
const updateBounds = () => {
|
|
184
|
+
bounds = el.getBoundingClientRect();
|
|
185
|
+
};
|
|
186
|
+
const observer = typeof ResizeObserver === 'function'
|
|
187
|
+
? new ResizeObserver(measure)
|
|
188
|
+
: null;
|
|
189
|
+
observer?.observe(el);
|
|
190
|
+
window.addEventListener('scroll', updateBounds, { capture: true, passive: true });
|
|
191
|
+
window.addEventListener('resize', measure, { passive: true });
|
|
192
|
+
mounted = true;
|
|
193
|
+
measure();
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
media?.removeEventListener?.('change', updateMotion);
|
|
197
|
+
observer?.disconnect();
|
|
198
|
+
window.removeEventListener('scroll', updateBounds, true);
|
|
199
|
+
window.removeEventListener('resize', measure);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
mounted = true;
|
|
168
204
|
|
|
169
205
|
return () => {
|
|
@@ -228,7 +264,11 @@
|
|
|
228
264
|
$effect(() => {
|
|
229
265
|
if (!mounted || shadingMode !== 'plastic' || typeof Path2D === 'undefined') return;
|
|
230
266
|
const path = bodyPath(SHAPE_PATHS[type] ?? SHAPE_PATHS.clover);
|
|
231
|
-
const dpr = Math.min(
|
|
267
|
+
const dpr = Math.min(
|
|
268
|
+
2,
|
|
269
|
+
clamp(maxDpr, 1, 2),
|
|
270
|
+
(typeof devicePixelRatio === 'number' && devicePixelRatio) || 1,
|
|
271
|
+
);
|
|
232
272
|
const devicePx = (typeof size === 'number' ? size : 64) * dpr;
|
|
233
273
|
const runtime = globalThis as typeof globalThis & {
|
|
234
274
|
requestIdleCallback?: (callback: () => void) => number;
|
|
@@ -247,20 +287,37 @@
|
|
|
247
287
|
const el = canvas;
|
|
248
288
|
const current = sim;
|
|
249
289
|
if (!mounted || !el || !current || frozen || motionReduced) return;
|
|
250
|
-
let onScreen = true;
|
|
251
290
|
let unsubscribe: (() => void) | null = null;
|
|
252
291
|
const reach = 3;
|
|
292
|
+
const frameInterval = 1 / clamp(fps, 10, 60);
|
|
293
|
+
let frameElapsed = 0;
|
|
294
|
+
let lastPointerVersion = -1;
|
|
253
295
|
const tick = (dt: number) => {
|
|
296
|
+
frameElapsed += dt;
|
|
297
|
+
if (frameElapsed < frameInterval) return;
|
|
298
|
+
const frameDt = frameElapsed;
|
|
299
|
+
frameElapsed = 0;
|
|
300
|
+
|
|
254
301
|
if (interactiveValue && !Number.isNaN(pointer.x)) {
|
|
255
|
-
|
|
302
|
+
if (pointer.version !== lastPointerVersion) {
|
|
303
|
+
bounds = el.getBoundingClientRect();
|
|
304
|
+
lastPointerVersion = pointer.version;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (interactiveValue && !Number.isNaN(pointer.x) && bounds) {
|
|
309
|
+
const rect = bounds;
|
|
256
310
|
const box = rect.width / OVERSCAN || 1;
|
|
257
311
|
const dx = (pointer.x - (rect.left + rect.width / 2)) / box;
|
|
258
312
|
const dy = (pointer.y - (rect.top + rect.height / 2 + RISE * box)) / box;
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
313
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
314
|
+
if (distanceSquared < 1) current.setPointer(dx, dy, 1);
|
|
315
|
+
else if (distanceSquared < reach * reach) {
|
|
316
|
+
const distance = Math.sqrt(distanceSquared);
|
|
317
|
+
current.setPointer(dx / distance, dy / distance, 1 - (distance - 1) / (reach - 1));
|
|
318
|
+
} else current.setPointer(0, 0, 0);
|
|
262
319
|
} else current.setPointer(0, 0, 0);
|
|
263
|
-
current.update(
|
|
320
|
+
current.update(frameDt * speedValue);
|
|
264
321
|
paint();
|
|
265
322
|
};
|
|
266
323
|
const run = () => {
|
|
@@ -273,8 +330,7 @@
|
|
|
273
330
|
let observer: IntersectionObserver | null = null;
|
|
274
331
|
if (typeof IntersectionObserver === 'function') {
|
|
275
332
|
observer = new IntersectionObserver((entries) => {
|
|
276
|
-
|
|
277
|
-
if (onScreen) run();
|
|
333
|
+
if (entries[0]?.isIntersecting ?? true) run();
|
|
278
334
|
else stop();
|
|
279
335
|
});
|
|
280
336
|
observer.observe(el);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
the seconds since their last tick; the loop stops while the tab is
|
|
3
3
|
hidden and while nobody is subscribed. */
|
|
4
4
|
/** The last pointer position on the page, NaN while it is away. */
|
|
5
|
-
export const pointer = { x: NaN, y: NaN };
|
|
5
|
+
export const pointer = { x: NaN, y: NaN, version: 0 };
|
|
6
6
|
const subs = new Set();
|
|
7
7
|
let raf = 0;
|
|
8
8
|
let last = 0;
|
|
@@ -25,7 +25,11 @@ function wire() {
|
|
|
25
25
|
if (wired || typeof document === 'undefined')
|
|
26
26
|
return;
|
|
27
27
|
wired = true;
|
|
28
|
-
document.addEventListener('pointermove', (e) => {
|
|
28
|
+
document.addEventListener('pointermove', (e) => {
|
|
29
|
+
pointer.x = e.clientX;
|
|
30
|
+
pointer.y = e.clientY;
|
|
31
|
+
pointer.version++;
|
|
32
|
+
}, { passive: true });
|
|
29
33
|
document.addEventListener('pointerleave', () => { pointer.x = NaN; pointer.y = NaN; });
|
|
30
34
|
window.addEventListener('blur', () => { pointer.x = NaN; pointer.y = NaN; });
|
|
31
35
|
document.addEventListener('visibilitychange', () => {
|
|
@@ -64,6 +64,10 @@ export interface BotAvatarProps extends Omit<HTMLCanvasAttributes, 'color' | 'st
|
|
|
64
64
|
saturation?: number;
|
|
65
65
|
/** Multiplier on every animation's speed. Default `1`. */
|
|
66
66
|
speed?: number;
|
|
67
|
+
/** Maximum animation updates per second, clamped to 10–60. Default `30`. */
|
|
68
|
+
fps?: number;
|
|
69
|
+
/** Maximum backing-store device pixel ratio, clamped to 1–2. Default `1.5`. */
|
|
70
|
+
maxDpr?: number;
|
|
67
71
|
/** Freeze every animation on its current frame. */
|
|
68
72
|
paused?: boolean;
|
|
69
73
|
/**
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy, onMount } from 'svelte';
|
|
3
|
-
import { easingFunction } from './motion';
|
|
4
3
|
import { parseShadow, type ShadowLayer } from './shadow';
|
|
5
|
-
import type { GooeyMenuSettings, GooeyRadialMenuProps } from './types';
|
|
4
|
+
import type { GooeyMenuAction, GooeyMenuSettings, GooeyRadialMenuProps } from './types';
|
|
6
5
|
|
|
7
6
|
let {
|
|
8
7
|
blur = 6,
|
|
9
8
|
contrast = 18,
|
|
10
9
|
fill = 'var(--modal-bg, #fff)',
|
|
11
10
|
shadow = '',
|
|
11
|
+
onAction,
|
|
12
12
|
pro = false,
|
|
13
13
|
bare = false,
|
|
14
14
|
}: GooeyRadialMenuProps & { fill?: string } = $props();
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
const instanceId = $props.id();
|
|
17
17
|
const filterId = `gooey-${instanceId.replace(/[^a-zA-Z0-9_-]/g, '')}`;
|
|
18
18
|
const satellites = [
|
|
19
|
-
{ label: 'New file', x: -54, y: -34, icon: 'file' },
|
|
20
|
-
{ label: 'Add image', x: 0, y: -64, icon: 'image' },
|
|
21
|
-
{ label: 'New folder', x: 54, y: -34, icon: 'folder' },
|
|
19
|
+
{ label: 'New file', action: 'new-file', x: -54, y: -34, icon: 'file' },
|
|
20
|
+
{ label: 'Add image', action: 'add-image', x: 0, y: -64, icon: 'image' },
|
|
21
|
+
{ label: 'New folder', action: 'new-folder', x: 54, y: -34, icon: 'folder' },
|
|
22
22
|
] as const;
|
|
23
23
|
const defaults: GooeyMenuSettings = {
|
|
24
24
|
openDur: 550,
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
let menuSettings = $state<GooeyMenuSettings>({ ...defaults });
|
|
59
59
|
let copied = $state(false);
|
|
60
60
|
let reducedMotion = $state(false);
|
|
61
|
-
let mounted = $state(false);
|
|
62
61
|
let groupElement: HTMLDivElement;
|
|
63
62
|
let itemWrappers: Array<HTMLDivElement | undefined> = [];
|
|
64
|
-
let
|
|
63
|
+
let satelliteButtons: Array<HTMLButtonElement | undefined> = [];
|
|
64
|
+
let mainButton: HTMLButtonElement;
|
|
65
65
|
let itemBoxes = $state<Array<BlobBox | null>>(initialBoxes);
|
|
66
66
|
let groupSize = $state({ w: 0, h: 0 });
|
|
67
67
|
|
|
@@ -104,10 +104,10 @@
|
|
|
104
104
|
},
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
-
let positions = allItems.map(() => ({ x: 0, y: 0 }));
|
|
108
|
-
let frameIds = allItems.map(() => 0);
|
|
109
107
|
let anticipationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
110
108
|
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
109
|
+
let anticipationFrame = 0;
|
|
110
|
+
let focusFrame = 0;
|
|
111
111
|
|
|
112
112
|
function parseCorner(value: string, size: number): number {
|
|
113
113
|
const first = value.split(' ')[0];
|
|
@@ -165,67 +165,46 @@
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
positions[index] = { x, y };
|
|
168
|
+
function transformFor(index: number) {
|
|
169
|
+
const item = allItems[index];
|
|
170
|
+
if (!menuOpen || !item || (!item.x && !item.y)) return 'none';
|
|
171
|
+
return `translate(${item.x * menuSettings.spread}px, ${item.y * menuSettings.spread}px)`;
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
function animateItem(index: number, x: number, y: number, duration: number, ease: string, delay: number) {
|
|
176
|
-
if (frameIds[index]) cancelAnimationFrame(frameIds[index]);
|
|
177
|
-
const from = positions[index];
|
|
178
|
-
if (duration <= 0 || (from.x === x && from.y === y)) {
|
|
179
|
-
writeTransform(index, x, y);
|
|
180
|
-
frameIds[index] = 0;
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const startPosition = { ...from };
|
|
185
|
-
const evaluate = easingFunction(ease);
|
|
186
|
-
const start = performance.now() + delay;
|
|
187
|
-
const tick = (now: number) => {
|
|
188
|
-
const progress = Math.min(1, Math.max(0, (now - start) / duration));
|
|
189
|
-
const eased = evaluate(progress);
|
|
190
|
-
writeTransform(
|
|
191
|
-
index,
|
|
192
|
-
startPosition.x + (x - startPosition.x) * eased,
|
|
193
|
-
startPosition.y + (y - startPosition.y) * eased,
|
|
194
|
-
);
|
|
195
|
-
if (progress < 1) frameIds[index] = requestAnimationFrame(tick);
|
|
196
|
-
else frameIds[index] = 0;
|
|
197
|
-
};
|
|
198
|
-
frameIds[index] = requestAnimationFrame(tick);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
$effect(() => {
|
|
202
|
-
const open = menuOpen;
|
|
203
|
-
const duration = reducedMotion ? 0 : phase.duration;
|
|
204
|
-
const ease = phase.ease;
|
|
205
|
-
const stagger = phase.stagger;
|
|
206
|
-
const spread = menuSettings.spread;
|
|
207
|
-
if (!mounted) return;
|
|
208
|
-
satellites.forEach((satellite, index) => {
|
|
209
|
-
animateItem(
|
|
210
|
-
index,
|
|
211
|
-
open ? satellite.x * spread : 0,
|
|
212
|
-
open ? satellite.y * spread : 0,
|
|
213
|
-
duration,
|
|
214
|
-
ease,
|
|
215
|
-
index * stagger,
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
animateItem(allItems.length - 1, 0, 0, duration, ease, 0);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
174
|
function toggle() {
|
|
175
|
+
const opening = !menuOpen;
|
|
222
176
|
if (menuOpen && menuSettings.anticipDist > 0) {
|
|
223
177
|
if (anticipationTimer) clearTimeout(anticipationTimer);
|
|
178
|
+
if (anticipationFrame) cancelAnimationFrame(anticipationFrame);
|
|
224
179
|
anticipating = false;
|
|
225
|
-
requestAnimationFrame(() =>
|
|
180
|
+
anticipationFrame = requestAnimationFrame(() => {
|
|
181
|
+
anticipationFrame = 0;
|
|
182
|
+
anticipating = true;
|
|
183
|
+
});
|
|
226
184
|
anticipationTimer = setTimeout(() => (anticipating = false), menuSettings.anticipDur);
|
|
227
185
|
}
|
|
228
|
-
menuOpen =
|
|
186
|
+
menuOpen = opening;
|
|
187
|
+
if (focusFrame) cancelAnimationFrame(focusFrame);
|
|
188
|
+
if (opening) {
|
|
189
|
+
focusFrame = requestAnimationFrame(() => {
|
|
190
|
+
focusFrame = 0;
|
|
191
|
+
if (menuOpen) satelliteButtons[0]?.focus();
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
mainButton?.focus();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function activateSatellite(action: GooeyMenuAction) {
|
|
199
|
+
toggle();
|
|
200
|
+
onAction?.(action);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleEscape(event: KeyboardEvent) {
|
|
204
|
+
if (event.key === 'Escape' && menuOpen) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
toggle();
|
|
207
|
+
}
|
|
229
208
|
}
|
|
230
209
|
|
|
231
210
|
function updateSetting<K extends keyof GooeyMenuSettings>(key: K, value: GooeyMenuSettings[K]) {
|
|
@@ -260,8 +239,6 @@
|
|
|
260
239
|
reducedMotion = motionQuery.matches;
|
|
261
240
|
const updateMotionPreference = (event: MediaQueryListEvent) => (reducedMotion = event.matches);
|
|
262
241
|
motionQuery.addEventListener('change', updateMotionPreference);
|
|
263
|
-
mounted = true;
|
|
264
|
-
|
|
265
242
|
return () => {
|
|
266
243
|
observer.disconnect();
|
|
267
244
|
motionQuery.removeEventListener('change', updateMotionPreference);
|
|
@@ -271,7 +248,8 @@
|
|
|
271
248
|
onDestroy(() => {
|
|
272
249
|
if (anticipationTimer) clearTimeout(anticipationTimer);
|
|
273
250
|
if (copyTimer) clearTimeout(copyTimer);
|
|
274
|
-
|
|
251
|
+
if (anticipationFrame) cancelAnimationFrame(anticipationFrame);
|
|
252
|
+
if (focusFrame) cancelAnimationFrame(focusFrame);
|
|
275
253
|
});
|
|
276
254
|
</script>
|
|
277
255
|
|
|
@@ -279,13 +257,13 @@
|
|
|
279
257
|
<div
|
|
280
258
|
class={`pm${menuOpen ? ' pm-open' : ''}${anticipating ? ' pm-anticipating' : ''}`}
|
|
281
259
|
bind:this={groupElement}
|
|
282
|
-
style={`--pm-anticip:${menuSettings.anticipDist}px;--pm-anticip-dur:${menuSettings.anticipDur}ms;--pm-icon-dur:${menuSettings.iconDur}ms;position:relative;isolation:isolate;`}
|
|
260
|
+
style={`--pm-anticip:${menuSettings.anticipDist}px;--pm-anticip-dur:${menuSettings.anticipDur}ms;--pm-icon-dur:${menuSettings.iconDur}ms;--pm-transition-duration:${reducedMotion ? 0 : phase.duration}ms;--pm-transition-ease:${phase.ease};position:relative;isolation:isolate;`}
|
|
283
261
|
>
|
|
284
262
|
<svg
|
|
285
263
|
aria-hidden="true"
|
|
286
264
|
focusable="false"
|
|
287
265
|
data-gooey-svg=""
|
|
288
|
-
style={`position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:-1
|
|
266
|
+
style={`position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:-1;${cssShadowFilter ? `filter:${cssShadowFilter};` : ''}`}
|
|
289
267
|
>
|
|
290
268
|
<defs>
|
|
291
269
|
<filter
|
|
@@ -409,11 +387,10 @@
|
|
|
409
387
|
{/if}
|
|
410
388
|
</filter>
|
|
411
389
|
</defs>
|
|
412
|
-
<g id={`${filterId}-sil`} filter={`url(#${filterId})`} style={`fill:${fill};`}>
|
|
390
|
+
<g id={`${filterId}-sil`} class="pm-blobs" filter={`url(#${filterId})`} style={`fill:${fill};`}>
|
|
413
391
|
{#each itemBoxes as box, index (index)}
|
|
414
392
|
{#if box}
|
|
415
393
|
<rect
|
|
416
|
-
bind:this={blobElements[index]}
|
|
417
394
|
x={box.x}
|
|
418
395
|
y={box.y}
|
|
419
396
|
width={box.w}
|
|
@@ -421,7 +398,7 @@
|
|
|
421
398
|
rx={box.r[0] === box.r[1] && box.r[1] === box.r[2] && box.r[2] === box.r[3]
|
|
422
399
|
? Math.max(0, Math.min(box.r[0], Math.min(box.w, box.h) / 2))
|
|
423
400
|
: undefined}
|
|
424
|
-
style={
|
|
401
|
+
style={`--pm-transition-delay:${reducedMotion || index >= satellites.length ? 0 : index * phase.stagger}ms;transform:${transformFor(index)};`}
|
|
425
402
|
/>
|
|
426
403
|
{/if}
|
|
427
404
|
{/each}
|
|
@@ -429,13 +406,16 @@
|
|
|
429
406
|
</svg>
|
|
430
407
|
|
|
431
408
|
{#each satellites as satellite, index (satellite.label)}
|
|
432
|
-
<div bind:this={itemWrappers[index]} class="pm-slot" style=
|
|
409
|
+
<div bind:this={itemWrappers[index]} class="pm-slot" style={`--pm-transition-delay:${reducedMotion ? 0 : index * phase.stagger}ms;transform:${transformFor(index)};`}>
|
|
433
410
|
<button
|
|
434
411
|
type="button"
|
|
435
412
|
class="pm-btn pm-sat"
|
|
436
413
|
aria-label={satellite.label}
|
|
437
414
|
tabindex={menuOpen ? 0 : -1}
|
|
438
|
-
|
|
415
|
+
inert={!menuOpen}
|
|
416
|
+
bind:this={satelliteButtons[index]}
|
|
417
|
+
onclick={() => activateSatellite(satellite.action)}
|
|
418
|
+
onkeydown={handleEscape}
|
|
439
419
|
>
|
|
440
420
|
<span
|
|
441
421
|
class="pm-sat-icon"
|
|
@@ -464,13 +444,15 @@
|
|
|
464
444
|
</div>
|
|
465
445
|
{/each}
|
|
466
446
|
|
|
467
|
-
<div bind:this={itemWrappers[satellites.length]} class="pm-slot" style=
|
|
447
|
+
<div bind:this={itemWrappers[satellites.length]} class="pm-slot" style={`transform:${transformFor(satellites.length)};`}>
|
|
468
448
|
<button
|
|
449
|
+
bind:this={mainButton}
|
|
469
450
|
type="button"
|
|
470
451
|
class="pm-btn pm-main"
|
|
471
452
|
aria-expanded={menuOpen}
|
|
472
453
|
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
|
473
454
|
onclick={toggle}
|
|
455
|
+
onkeydown={handleEscape}
|
|
474
456
|
>
|
|
475
457
|
<span class="pm-plus">
|
|
476
458
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round">
|
|
@@ -597,9 +579,20 @@
|
|
|
597
579
|
position: absolute;
|
|
598
580
|
left: 80px;
|
|
599
581
|
top: 80px;
|
|
582
|
+
display: inline-block;
|
|
583
|
+
transition: transform var(--pm-transition-duration, 0ms)
|
|
584
|
+
var(--pm-transition-ease, ease) var(--pm-transition-delay, 0ms);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.pm-blobs rect {
|
|
588
|
+
transform-box: fill-box;
|
|
589
|
+
transform-origin: center;
|
|
590
|
+
transition: transform var(--pm-transition-duration, 0ms)
|
|
591
|
+
var(--pm-transition-ease, ease) var(--pm-transition-delay, 0ms);
|
|
600
592
|
}
|
|
601
593
|
|
|
602
594
|
.pm-btn {
|
|
595
|
+
position: relative;
|
|
603
596
|
width: 40px;
|
|
604
597
|
height: 40px;
|
|
605
598
|
border: 0;
|
|
@@ -611,6 +604,14 @@
|
|
|
611
604
|
cursor: pointer;
|
|
612
605
|
color: var(--btn-text, #17181c);
|
|
613
606
|
-webkit-tap-highlight-color: transparent;
|
|
607
|
+
touch-action: manipulation;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.pm-btn::before {
|
|
611
|
+
content: '';
|
|
612
|
+
position: absolute;
|
|
613
|
+
inset: -2px;
|
|
614
|
+
border-radius: inherit;
|
|
614
615
|
}
|
|
615
616
|
|
|
616
617
|
.pm-btn:focus-visible {
|
|
@@ -7,4 +7,16 @@ export interface ShadowLayer {
|
|
|
7
7
|
color: string;
|
|
8
8
|
inset: boolean;
|
|
9
9
|
}
|
|
10
|
+
/** Ready-to-use box-shadow values for the Gooey menu's merged liquid silhouette. */
|
|
11
|
+
export declare const gooeyShadowPresets: {
|
|
12
|
+
readonly light: {
|
|
13
|
+
readonly soft: "0 0 0 1px rgba(0, 0, 0, 0.06), 0 2px 6px rgba(0, 0, 0, 0.05), 0 4px 42px rgba(0, 0, 0, 0.06)";
|
|
14
|
+
readonly floating: "0 2px 6px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.18)";
|
|
15
|
+
};
|
|
16
|
+
readonly dark: {
|
|
17
|
+
readonly soft: "0 0 0 1px rgba(255, 255, 255, 0.08) inset, 0 1px 0 rgba(255, 255, 255, 0.06) inset";
|
|
18
|
+
readonly floating: "0 2px 6px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.55)";
|
|
19
|
+
};
|
|
20
|
+
readonly none: "";
|
|
21
|
+
};
|
|
10
22
|
export declare function parseShadow(input?: string | null): ShadowLayer[];
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** Ready-to-use box-shadow values for the Gooey menu's merged liquid silhouette. */
|
|
2
|
+
export const gooeyShadowPresets = {
|
|
3
|
+
light: {
|
|
4
|
+
soft: '0 0 0 1px rgba(0, 0, 0, 0.06), 0 2px 6px rgba(0, 0, 0, 0.05), 0 4px 42px rgba(0, 0, 0, 0.06)',
|
|
5
|
+
floating: '0 2px 6px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.18)',
|
|
6
|
+
},
|
|
7
|
+
dark: {
|
|
8
|
+
soft: '0 0 0 1px rgba(255, 255, 255, 0.08) inset, 0 1px 0 rgba(255, 255, 255, 0.06) inset',
|
|
9
|
+
floating: '0 2px 6px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.55)',
|
|
10
|
+
},
|
|
11
|
+
none: '',
|
|
12
|
+
};
|
|
1
13
|
function splitTop(value, separator) {
|
|
2
14
|
const parts = [];
|
|
3
15
|
let depth = 0;
|
|
@@ -6,6 +6,8 @@ export interface GooeyRadialMenuProps {
|
|
|
6
6
|
contrast?: number;
|
|
7
7
|
/** CSS box-shadow layers drawn on the merged liquid silhouette. */
|
|
8
8
|
shadow?: string;
|
|
9
|
+
/** Called when a sample action is selected. The menu closes after activation. */
|
|
10
|
+
onAction?: (action: GooeyMenuAction) => void;
|
|
9
11
|
/** Surface color for the merged liquid. Defaults to the host theme's modal color. */
|
|
10
12
|
fill?: string;
|
|
11
13
|
/** Show all raw tuning controls, export, and reset controls. */
|
|
@@ -13,6 +15,7 @@ export interface GooeyRadialMenuProps {
|
|
|
13
15
|
/** Render only the menu stage for embedding in a host layout. */
|
|
14
16
|
bare?: boolean;
|
|
15
17
|
}
|
|
18
|
+
export type GooeyMenuAction = 'new-file' | 'add-image' | 'new-folder';
|
|
16
19
|
export interface GooeyMenuSettings {
|
|
17
20
|
openDur: number;
|
|
18
21
|
openEase: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { default as BotAvatar } from './bot-avatars/BotAvatar.svelte';
|
|
2
2
|
export { default } from './bot-avatars/BotAvatar.svelte';
|
|
3
3
|
export { default as GooeyRadialMenu } from './gooey-radial-menu/GooeyRadialMenu.svelte';
|
|
4
|
-
export
|
|
4
|
+
export { gooeyShadowPresets } from './gooey-radial-menu/shadow';
|
|
5
|
+
export type { GooeyMenuAction, GooeyMenuSettings, GooeyRadialMenuProps } from './gooey-radial-menu/types';
|
|
5
6
|
export { botAvatarPresets, botAvatarPalette, botAvatarTypes, botAvatarFaces, botAvatarStates, } from './bot-avatars/presets';
|
|
6
7
|
export { SHAPE_PATHS as botAvatarShapes, SHAPE_PARTS as botAvatarParts } from './bot-avatars/shapes';
|
|
7
8
|
export { autoInk, luminance, parseColor, shade } from './bot-avatars/color';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as BotAvatar } from './bot-avatars/BotAvatar.svelte';
|
|
2
2
|
export { default } from './bot-avatars/BotAvatar.svelte';
|
|
3
3
|
export { default as GooeyRadialMenu } from './gooey-radial-menu/GooeyRadialMenu.svelte';
|
|
4
|
+
export { gooeyShadowPresets } from './gooey-radial-menu/shadow';
|
|
4
5
|
export { botAvatarPresets, botAvatarPalette, botAvatarTypes, botAvatarFaces, botAvatarStates, } from './bot-avatars/presets';
|
|
5
6
|
export { SHAPE_PATHS as botAvatarShapes, SHAPE_PARTS as botAvatarParts } from './bot-avatars/shapes';
|
|
6
7
|
export { autoInk, luminance, parseColor, shade } from './bot-avatars/color';
|