@le-space/ui 0.9.1 → 0.9.2
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/package.json +3 -3
- package/react/index.d.ts +24 -0
- package/react/index.js +161 -11
- package/shared/index.d.ts +85 -2
- package/shared/index.js +65 -2
- package/svelte/SponsorRelayFab.svelte +97 -4
- package/svelte/components/LauncherButton.svelte +65 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Floating Aleph Relay Button UI components for Svelte and React.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"url": "https://github.com/NiKrause/relay-button/issues"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@le-space/browser": "0.9.
|
|
48
|
-
"@le-space/core": "0.9.
|
|
47
|
+
"@le-space/browser": "0.9.2",
|
|
48
|
+
"@le-space/core": "0.9.2",
|
|
49
49
|
"viem": "^2.53.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
package/react/index.d.ts
CHANGED
|
@@ -199,6 +199,16 @@ interface DeploymentProgressEvent {
|
|
|
199
199
|
}
|
|
200
200
|
type DeploymentProgressListener = (event: DeploymentProgressEvent) => void;
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Geometry for a launcher the consumer can place and the user can drag.
|
|
204
|
+
*
|
|
205
|
+
* Kept free of DOM access on purpose: the framework layers read rects and write
|
|
206
|
+
* styles, this module only does the arithmetic, which is what makes the awkward
|
|
207
|
+
* parts — the tap/drag threshold, staying on screen after a rotation — testable
|
|
208
|
+
* without a browser.
|
|
209
|
+
*/
|
|
210
|
+
type FabCorner = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
211
|
+
|
|
202
212
|
type SponsorRelayHealthTone = 'ok' | 'caution' | 'error' | 'idle';
|
|
203
213
|
interface SponsorRelayUcanStoreBootstrapInput {
|
|
204
214
|
adminDid: string;
|
|
@@ -233,6 +243,20 @@ interface SponsorRelayProps {
|
|
|
233
243
|
crnSource?: CrnSourcePreference;
|
|
234
244
|
schedulerApiHost?: string;
|
|
235
245
|
twoN6ApiHost?: string;
|
|
246
|
+
/**
|
|
247
|
+
* Which corner the floating launcher starts in. Consumers could not place it
|
|
248
|
+
* at all before: the launcher is `position: fixed`, so a wrapper around it
|
|
249
|
+
* collapses to 0x0 and has no box to position or drag.
|
|
250
|
+
*/
|
|
251
|
+
position?: FabCorner;
|
|
252
|
+
/** Let the user drag the launcher. The panel follows it. */
|
|
253
|
+
draggable?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* localStorage key for the dragged position. Omit it and nothing is
|
|
256
|
+
* persisted -- a library that writes to storage unasked collides with its
|
|
257
|
+
* host.
|
|
258
|
+
*/
|
|
259
|
+
positionStorageKey?: string;
|
|
236
260
|
}
|
|
237
261
|
interface SponsorRelayWalletState {
|
|
238
262
|
connected: boolean;
|
package/react/index.js
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
// src/react/SponsorRelayFab.tsx
|
|
2
2
|
import React2 from "react";
|
|
3
3
|
|
|
4
|
+
// src/shared/fab-position.ts
|
|
5
|
+
var FAB_EDGE_MARGIN = 22;
|
|
6
|
+
var FAB_DRAG_THRESHOLD = 6;
|
|
7
|
+
function dragExceedsThreshold(start, current, threshold = FAB_DRAG_THRESHOLD) {
|
|
8
|
+
const dx = current.x - start.x;
|
|
9
|
+
const dy = current.y - start.y;
|
|
10
|
+
return Math.hypot(dx, dy) >= threshold;
|
|
11
|
+
}
|
|
12
|
+
function resolveCornerPlacement(corner, size, viewport, margin = FAB_EDGE_MARGIN) {
|
|
13
|
+
const left = corner.endsWith("left") ? margin : viewport.width - size.width - margin;
|
|
14
|
+
const top = corner.startsWith("top") ? margin : viewport.height - size.height - margin;
|
|
15
|
+
return clampToViewport({ left, top }, size, viewport, margin);
|
|
16
|
+
}
|
|
17
|
+
function clampToViewport(placement, size, viewport, margin = FAB_EDGE_MARGIN) {
|
|
18
|
+
const maxLeft = Math.max(margin, viewport.width - size.width - margin);
|
|
19
|
+
const maxTop = Math.max(margin, viewport.height - size.height - margin);
|
|
20
|
+
return {
|
|
21
|
+
left: Math.min(Math.max(placement.left, margin), maxLeft),
|
|
22
|
+
top: Math.min(Math.max(placement.top, margin), maxTop)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function anchorPanelToLauncher(launcher, panel, viewport, gap = 12, margin = FAB_EDGE_MARGIN) {
|
|
26
|
+
const launcherCentre = launcher.top + launcher.height / 2;
|
|
27
|
+
const side = launcherCentre < viewport.height / 2 ? "below" : "above";
|
|
28
|
+
const top = side === "below" ? launcher.top + launcher.height + gap : launcher.top - gap - panel.height;
|
|
29
|
+
const preferredLeft = launcher.left + launcher.width / 2 < viewport.width / 2 ? launcher.left : launcher.left + launcher.width - panel.width;
|
|
30
|
+
const clamped = clampToViewport({ left: preferredLeft, top }, panel, viewport, margin);
|
|
31
|
+
const available = side === "below" ? viewport.height - clamped.top - margin : launcher.top - gap - margin;
|
|
32
|
+
return {
|
|
33
|
+
...clamped,
|
|
34
|
+
side,
|
|
35
|
+
maxHeight: Math.max(0, available)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function readStoredPosition(key) {
|
|
39
|
+
if (!key) return null;
|
|
40
|
+
try {
|
|
41
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
42
|
+
if (!raw) return null;
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
return Number.isFinite(parsed?.left) && Number.isFinite(parsed?.top) ? parsed : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function writeStoredPosition(key, placement, viewport) {
|
|
50
|
+
if (!key) return;
|
|
51
|
+
try {
|
|
52
|
+
globalThis.localStorage?.setItem(key, JSON.stringify({ ...placement, viewport }));
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
4
57
|
// src/shared/constants.ts
|
|
5
58
|
var DEFAULT_INSTANCE_NAME = "sponsor-relay";
|
|
6
59
|
var DEFAULT_MANIFEST_URL = "./rootfs-manifest.json";
|
|
@@ -43,7 +96,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
|
|
|
43
96
|
var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
|
|
44
97
|
|
|
45
98
|
// src/shared/package-version.ts
|
|
46
|
-
var UI_PACKAGE_VERSION = "0.9.
|
|
99
|
+
var UI_PACKAGE_VERSION = "0.9.2";
|
|
47
100
|
|
|
48
101
|
// src/shared/format.ts
|
|
49
102
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -7169,6 +7222,14 @@ function SponsorRelayFab(props) {
|
|
|
7169
7222
|
}
|
|
7170
7223
|
};
|
|
7171
7224
|
const launcherRef = React2.useRef(null);
|
|
7225
|
+
const panelRef = React2.useRef(null);
|
|
7226
|
+
const draggable = props.draggable ?? false;
|
|
7227
|
+
const corner = props.position ?? "bottom-right";
|
|
7228
|
+
const storageKey = props.positionStorageKey;
|
|
7229
|
+
const [placement, setPlacement] = React2.useState(null);
|
|
7230
|
+
const dragOrigin = React2.useRef(null);
|
|
7231
|
+
const dragOffset = React2.useRef(null);
|
|
7232
|
+
const draggedRef = React2.useRef(false);
|
|
7172
7233
|
const [inlinePanelStyle, setInlinePanelStyle] = React2.useState(null);
|
|
7173
7234
|
const [compactInlineLabel, setCompactInlineLabel] = React2.useState(false);
|
|
7174
7235
|
React2.useEffect(() => {
|
|
@@ -7198,6 +7259,67 @@ function SponsorRelayFab(props) {
|
|
|
7198
7259
|
window.clearTimeout(timeout);
|
|
7199
7260
|
};
|
|
7200
7261
|
}, [state.deploymentProgress.stage, state.deploymentProgress.timestamp]);
|
|
7262
|
+
const viewportSize = () => ({
|
|
7263
|
+
width: window.innerWidth,
|
|
7264
|
+
height: window.innerHeight
|
|
7265
|
+
});
|
|
7266
|
+
const launcherSize = () => {
|
|
7267
|
+
const rect = launcherRef.current?.getBoundingClientRect();
|
|
7268
|
+
return { width: rect?.width ?? 180, height: rect?.height ?? 44 };
|
|
7269
|
+
};
|
|
7270
|
+
React2.useEffect(() => {
|
|
7271
|
+
if (launcherMode !== "floating") {
|
|
7272
|
+
setPlacement(null);
|
|
7273
|
+
return;
|
|
7274
|
+
}
|
|
7275
|
+
const stored = readStoredPosition(storageKey);
|
|
7276
|
+
setPlacement(
|
|
7277
|
+
stored ? clampToViewport(stored, launcherSize(), viewportSize()) : resolveCornerPlacement(corner, launcherSize(), viewportSize())
|
|
7278
|
+
);
|
|
7279
|
+
const onViewportChange = () => setPlacement(
|
|
7280
|
+
(current) => current ? clampToViewport(current, launcherSize(), viewportSize()) : current
|
|
7281
|
+
);
|
|
7282
|
+
window.addEventListener("resize", onViewportChange);
|
|
7283
|
+
window.addEventListener("orientationchange", onViewportChange);
|
|
7284
|
+
return () => {
|
|
7285
|
+
window.removeEventListener("resize", onViewportChange);
|
|
7286
|
+
window.removeEventListener("orientationchange", onViewportChange);
|
|
7287
|
+
};
|
|
7288
|
+
}, [launcherMode, corner, storageKey]);
|
|
7289
|
+
const handlePointerDown = (event) => {
|
|
7290
|
+
if (!draggable || launcherMode !== "floating" || event.button > 0 || !placement) return;
|
|
7291
|
+
dragOrigin.current = { x: event.clientX, y: event.clientY };
|
|
7292
|
+
dragOffset.current = { x: event.clientX - placement.left, y: event.clientY - placement.top };
|
|
7293
|
+
draggedRef.current = false;
|
|
7294
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
7295
|
+
};
|
|
7296
|
+
const handlePointerMove = (event) => {
|
|
7297
|
+
const origin = dragOrigin.current;
|
|
7298
|
+
const offset = dragOffset.current;
|
|
7299
|
+
if (!origin || !offset) return;
|
|
7300
|
+
const point = { x: event.clientX, y: event.clientY };
|
|
7301
|
+
if (!draggedRef.current && !dragExceedsThreshold(origin, point)) return;
|
|
7302
|
+
draggedRef.current = true;
|
|
7303
|
+
setPlacement(
|
|
7304
|
+
clampToViewport(
|
|
7305
|
+
{ left: point.x - offset.x, top: point.y - offset.y },
|
|
7306
|
+
launcherSize(),
|
|
7307
|
+
viewportSize()
|
|
7308
|
+
)
|
|
7309
|
+
);
|
|
7310
|
+
};
|
|
7311
|
+
const handlePointerUp = (event) => {
|
|
7312
|
+
if (!dragOrigin.current) return;
|
|
7313
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
7314
|
+
dragOrigin.current = null;
|
|
7315
|
+
dragOffset.current = null;
|
|
7316
|
+
if (draggedRef.current && placement) {
|
|
7317
|
+
writeStoredPosition(storageKey, placement, viewportSize());
|
|
7318
|
+
} else {
|
|
7319
|
+
controller.toggleOpen();
|
|
7320
|
+
}
|
|
7321
|
+
draggedRef.current = false;
|
|
7322
|
+
};
|
|
7201
7323
|
React2.useEffect(() => {
|
|
7202
7324
|
if (launcherMode !== "inline" || !state.open) {
|
|
7203
7325
|
return;
|
|
@@ -7251,18 +7373,37 @@ function SponsorRelayFab(props) {
|
|
|
7251
7373
|
const inlineButtonBorder = `1px solid ${themedLauncherBorder}`;
|
|
7252
7374
|
const inlineBadgeBackground = themedLauncherBadgeBackground;
|
|
7253
7375
|
const inlineBadgeBorder = `1px solid ${themedLauncherBadgeBorder}`;
|
|
7376
|
+
const floatingPanelStyle = () => {
|
|
7377
|
+
if (!placement || typeof window === "undefined") {
|
|
7378
|
+
return {
|
|
7379
|
+
...basePanelStyle,
|
|
7380
|
+
right: "1.4rem",
|
|
7381
|
+
bottom: "11.5rem",
|
|
7382
|
+
maxHeight: "calc(100vh - 12.5rem)"
|
|
7383
|
+
};
|
|
7384
|
+
}
|
|
7385
|
+
const panelRect = panelRef.current?.getBoundingClientRect();
|
|
7386
|
+
const anchored = anchorPanelToLauncher(
|
|
7387
|
+
{ ...placement, ...launcherSize() },
|
|
7388
|
+
{ width: panelRect?.width ?? 448, height: panelRect?.height ?? 520 },
|
|
7389
|
+
viewportSize()
|
|
7390
|
+
);
|
|
7391
|
+
return {
|
|
7392
|
+
...basePanelStyle,
|
|
7393
|
+
left: anchored.left,
|
|
7394
|
+
top: anchored.top,
|
|
7395
|
+
right: "auto",
|
|
7396
|
+
bottom: "auto",
|
|
7397
|
+
maxHeight: anchored.maxHeight
|
|
7398
|
+
};
|
|
7399
|
+
};
|
|
7254
7400
|
const panelStyle = launcherMode === "inline" ? inlinePanelStyle ?? {
|
|
7255
7401
|
...basePanelStyle,
|
|
7256
7402
|
top: "4.75rem",
|
|
7257
7403
|
right: "1rem",
|
|
7258
7404
|
maxHeight: "calc(100vh - 6rem)",
|
|
7259
7405
|
width: "min(30rem, calc(100vw - 1.5rem))"
|
|
7260
|
-
} :
|
|
7261
|
-
...basePanelStyle,
|
|
7262
|
-
right: "1.4rem",
|
|
7263
|
-
bottom: "11.5rem",
|
|
7264
|
-
maxHeight: "calc(100vh - 12.5rem)"
|
|
7265
|
-
};
|
|
7406
|
+
} : floatingPanelStyle();
|
|
7266
7407
|
const launcherLabel = deploymentLauncherLabel(state, compactInlineLabel, launcherMode);
|
|
7267
7408
|
const rootfsBlocked = state.rootfsHealth.tone === "error";
|
|
7268
7409
|
const deployCallToAction = state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : rootfsBlocked ? "Deployment blocked \u2014 Rootfs unavailable" : deploymentButtonLabel(state) : "Connect MetaMask";
|
|
@@ -7273,7 +7414,11 @@ function SponsorRelayFab(props) {
|
|
|
7273
7414
|
{
|
|
7274
7415
|
ref: launcherRef,
|
|
7275
7416
|
type: "button",
|
|
7276
|
-
onClick: () => controller.toggleOpen(),
|
|
7417
|
+
onClick: draggable ? void 0 : () => controller.toggleOpen(),
|
|
7418
|
+
onPointerDown: handlePointerDown,
|
|
7419
|
+
onPointerMove: handlePointerMove,
|
|
7420
|
+
onPointerUp: handlePointerUp,
|
|
7421
|
+
onPointerCancel: handlePointerUp,
|
|
7277
7422
|
onMouseEnter: () => setHovered(true),
|
|
7278
7423
|
onMouseLeave: () => setHovered(false),
|
|
7279
7424
|
onFocus: () => setHovered(true),
|
|
@@ -7281,8 +7426,13 @@ function SponsorRelayFab(props) {
|
|
|
7281
7426
|
title: indicator.detail ?? deploymentPanelTitle(state),
|
|
7282
7427
|
style: {
|
|
7283
7428
|
position: launcherMode === "floating" ? "fixed" : "relative",
|
|
7284
|
-
|
|
7285
|
-
|
|
7429
|
+
left: launcherMode === "floating" && placement ? placement.left : void 0,
|
|
7430
|
+
top: launcherMode === "floating" && placement ? placement.top : void 0,
|
|
7431
|
+
right: launcherMode === "floating" && !placement ? "1.4rem" : void 0,
|
|
7432
|
+
bottom: launcherMode === "floating" && !placement ? "5.8rem" : void 0,
|
|
7433
|
+
// Without this the browser scrolls the page instead of moving the button.
|
|
7434
|
+
touchAction: draggable ? "none" : void 0,
|
|
7435
|
+
userSelect: draggable ? "none" : void 0,
|
|
7286
7436
|
zIndex: launcherMode === "floating" ? 1e4 : "auto",
|
|
7287
7437
|
borderRadius: "999px",
|
|
7288
7438
|
border: launcherMode === "floating" ? `1px solid ${themedLauncherBorder}` : inlineButtonBorder,
|
|
@@ -7412,7 +7562,7 @@ function SponsorRelayFab(props) {
|
|
|
7412
7562
|
}
|
|
7413
7563
|
}
|
|
7414
7564
|
),
|
|
7415
|
-
/* @__PURE__ */ jsxs("aside", { style: panelStyle, children: [
|
|
7565
|
+
/* @__PURE__ */ jsxs("aside", { ref: panelRef, style: panelStyle, children: [
|
|
7416
7566
|
/* @__PURE__ */ jsxs(
|
|
7417
7567
|
"div",
|
|
7418
7568
|
{
|
package/shared/index.d.ts
CHANGED
|
@@ -197,6 +197,75 @@ interface DeploymentProgressEvent {
|
|
|
197
197
|
}
|
|
198
198
|
type DeploymentProgressListener = (event: DeploymentProgressEvent) => void;
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Geometry for a launcher the consumer can place and the user can drag.
|
|
202
|
+
*
|
|
203
|
+
* Kept free of DOM access on purpose: the framework layers read rects and write
|
|
204
|
+
* styles, this module only does the arithmetic, which is what makes the awkward
|
|
205
|
+
* parts — the tap/drag threshold, staying on screen after a rotation — testable
|
|
206
|
+
* without a browser.
|
|
207
|
+
*/
|
|
208
|
+
type FabCorner = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
209
|
+
interface FabPoint {
|
|
210
|
+
x: number;
|
|
211
|
+
y: number;
|
|
212
|
+
}
|
|
213
|
+
interface FabSize {
|
|
214
|
+
width: number;
|
|
215
|
+
height: number;
|
|
216
|
+
}
|
|
217
|
+
interface FabViewport {
|
|
218
|
+
width: number;
|
|
219
|
+
height: number;
|
|
220
|
+
}
|
|
221
|
+
/** Absolute top-left of the launcher, in viewport coordinates. */
|
|
222
|
+
interface FabPlacement {
|
|
223
|
+
left: number;
|
|
224
|
+
top: number;
|
|
225
|
+
}
|
|
226
|
+
declare const DEFAULT_FAB_CORNER: FabCorner;
|
|
227
|
+
/** Distance from the viewport edge, matching the previous `1.4rem` inset. */
|
|
228
|
+
declare const FAB_EDGE_MARGIN = 22;
|
|
229
|
+
/**
|
|
230
|
+
* Pointer travel, in pixels, before a press becomes a drag.
|
|
231
|
+
*
|
|
232
|
+
* Not zero: fingers are not steady, so a strict zero turns every tap into a
|
|
233
|
+
* one-pixel drag and the click never fires.
|
|
234
|
+
*/
|
|
235
|
+
declare const FAB_DRAG_THRESHOLD = 6;
|
|
236
|
+
declare function dragExceedsThreshold(start: FabPoint, current: FabPoint, threshold?: number): boolean;
|
|
237
|
+
/** Where a named corner puts a launcher of this size in this viewport. */
|
|
238
|
+
declare function resolveCornerPlacement(corner: FabCorner, size: FabSize, viewport: FabViewport, margin?: number): FabPlacement;
|
|
239
|
+
/**
|
|
240
|
+
* Keep the launcher reachable. A viewport that shrinks — rotation, a desktop
|
|
241
|
+
* window dragged narrow, a mobile keyboard — would otherwise strand a dragged
|
|
242
|
+
* button off-screen with no way to bring it back.
|
|
243
|
+
*/
|
|
244
|
+
declare function clampToViewport(placement: FabPlacement, size: FabSize, viewport: FabViewport, margin?: number): FabPlacement;
|
|
245
|
+
/** Which way the panel should open, given where the launcher sits. */
|
|
246
|
+
type FabPanelSide = 'above' | 'below';
|
|
247
|
+
interface FabPanelPlacement extends FabPlacement {
|
|
248
|
+
side: FabPanelSide;
|
|
249
|
+
maxHeight: number;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Hang the panel off the launcher instead of off its own hardcoded corner.
|
|
253
|
+
*
|
|
254
|
+
* The panel opens away from the nearer edge, so a launcher dragged to the top
|
|
255
|
+
* of the screen drops its panel downward rather than off-screen upward.
|
|
256
|
+
*/
|
|
257
|
+
declare function anchorPanelToLauncher(launcher: FabPlacement & FabSize, panel: FabSize, viewport: FabViewport, gap?: number, margin?: number): FabPanelPlacement;
|
|
258
|
+
interface StoredPosition extends FabPlacement {
|
|
259
|
+
/** Viewport the position was captured in, so a resize can be detected. */
|
|
260
|
+
viewport?: FabViewport;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Persistence is opt-in through a consumer-supplied key: a library that writes
|
|
264
|
+
* to localStorage unasked is a library that collides with its host.
|
|
265
|
+
*/
|
|
266
|
+
declare function readStoredPosition(key: string | null | undefined): StoredPosition | null;
|
|
267
|
+
declare function writeStoredPosition(key: string | null | undefined, placement: FabPlacement, viewport?: FabViewport): void;
|
|
268
|
+
|
|
200
269
|
type SponsorRelayHealthTone = 'ok' | 'caution' | 'error' | 'idle';
|
|
201
270
|
interface SponsorRelayUcanStoreBootstrapInput {
|
|
202
271
|
adminDid: string;
|
|
@@ -231,6 +300,20 @@ interface SponsorRelayProps {
|
|
|
231
300
|
crnSource?: CrnSourcePreference;
|
|
232
301
|
schedulerApiHost?: string;
|
|
233
302
|
twoN6ApiHost?: string;
|
|
303
|
+
/**
|
|
304
|
+
* Which corner the floating launcher starts in. Consumers could not place it
|
|
305
|
+
* at all before: the launcher is `position: fixed`, so a wrapper around it
|
|
306
|
+
* collapses to 0x0 and has no box to position or drag.
|
|
307
|
+
*/
|
|
308
|
+
position?: FabCorner;
|
|
309
|
+
/** Let the user drag the launcher. The panel follows it. */
|
|
310
|
+
draggable?: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* localStorage key for the dragged position. Omit it and nothing is
|
|
313
|
+
* persisted -- a library that writes to storage unasked collides with its
|
|
314
|
+
* host.
|
|
315
|
+
*/
|
|
316
|
+
positionStorageKey?: string;
|
|
234
317
|
}
|
|
235
318
|
interface SponsorRelayWalletState {
|
|
236
319
|
connected: boolean;
|
|
@@ -397,7 +480,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
|
|
|
397
480
|
declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
|
|
398
481
|
declare const DEPLOYMENT_PENDING_WARNING_MS: number;
|
|
399
482
|
|
|
400
|
-
declare const UI_PACKAGE_VERSION = "0.9.
|
|
483
|
+
declare const UI_PACKAGE_VERSION = "0.9.2";
|
|
401
484
|
|
|
402
485
|
interface RootfsRequiredPortForward {
|
|
403
486
|
port: number;
|
|
@@ -520,4 +603,4 @@ interface DeploymentProgressEmitter {
|
|
|
520
603
|
}
|
|
521
604
|
declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
|
|
522
605
|
|
|
523
|
-
export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEFAULT_UCAN_STORE_ALLOWED_CAPABILITIES, DEFAULT_UCAN_STORE_MAX_DELEGATION_EXPIRATION_SECONDS, DEFAULT_UCAN_STORE_PWA_ORIGIN, DEFAULT_UCAN_STORE_SERVICE_DID, DEFAULT_UCAN_STORE_SERVICE_ORIGIN, DEFAULT_UCAN_STORE_USER_DELEGATION_EXPIRATION_SECONDS, DEPLOYMENT_PENDING_WARNING_MS, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, INSTANCE_RUNTIME_PENDING_REFRESH_MS, INSTANCE_RUNTIME_READY_REFRESH_MS, INSTANCE_RUNTIME_STALE_REFRESH_MS, MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS, RECENT_INSTANCE_RUNTIME_GRACE_MS, REFRESH_INTERVAL_MS, ROOTFS_MISSING_STATE, STALE_INSTANCE_ALLOCATION_COOLDOWN_MS, type SponsorRelayAddressGroup, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayInstanceAddresses, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayUcanStoreBootstrapInput, type SponsorRelayWalletState, UI_PACKAGE_VERSION, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, parseInstanceAddressPayload, personalSign, resolveManifestSource, rootfsHealth, shortHash, toChecksumAddress, watchWallet };
|
|
606
|
+
export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_FAB_CORNER, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEFAULT_UCAN_STORE_ALLOWED_CAPABILITIES, DEFAULT_UCAN_STORE_MAX_DELEGATION_EXPIRATION_SECONDS, DEFAULT_UCAN_STORE_PWA_ORIGIN, DEFAULT_UCAN_STORE_SERVICE_DID, DEFAULT_UCAN_STORE_SERVICE_ORIGIN, DEFAULT_UCAN_STORE_USER_DELEGATION_EXPIRATION_SECONDS, DEPLOYMENT_PENDING_WARNING_MS, type DeploymentProgressEmitter, type EthereumProviderLike, FAB_DRAG_THRESHOLD, FAB_EDGE_MARGIN, type FabCorner, type FabPanelPlacement, type FabPanelSide, type FabPlacement, type FabPoint, type FabSize, type FabViewport, IDLE_DEPLOYMENT_PROGRESS, INSTANCE_RUNTIME_PENDING_REFRESH_MS, INSTANCE_RUNTIME_READY_REFRESH_MS, INSTANCE_RUNTIME_STALE_REFRESH_MS, MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS, RECENT_INSTANCE_RUNTIME_GRACE_MS, REFRESH_INTERVAL_MS, ROOTFS_MISSING_STATE, STALE_INSTANCE_ALLOCATION_COOLDOWN_MS, type SponsorRelayAddressGroup, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayInstanceAddresses, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayUcanStoreBootstrapInput, type SponsorRelayWalletState, UI_PACKAGE_VERSION, anchorPanelToLauncher, buildSshCommand, clampToViewport, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, dragExceedsThreshold, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, parseInstanceAddressPayload, personalSign, readStoredPosition, resolveCornerPlacement, resolveManifestSource, rootfsHealth, shortHash, toChecksumAddress, watchWallet, writeStoredPosition };
|
package/shared/index.js
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
// src/shared/fab-position.ts
|
|
2
|
+
var DEFAULT_FAB_CORNER = "bottom-right";
|
|
3
|
+
var FAB_EDGE_MARGIN = 22;
|
|
4
|
+
var FAB_DRAG_THRESHOLD = 6;
|
|
5
|
+
function dragExceedsThreshold(start, current, threshold = FAB_DRAG_THRESHOLD) {
|
|
6
|
+
const dx = current.x - start.x;
|
|
7
|
+
const dy = current.y - start.y;
|
|
8
|
+
return Math.hypot(dx, dy) >= threshold;
|
|
9
|
+
}
|
|
10
|
+
function resolveCornerPlacement(corner, size, viewport, margin = FAB_EDGE_MARGIN) {
|
|
11
|
+
const left = corner.endsWith("left") ? margin : viewport.width - size.width - margin;
|
|
12
|
+
const top = corner.startsWith("top") ? margin : viewport.height - size.height - margin;
|
|
13
|
+
return clampToViewport({ left, top }, size, viewport, margin);
|
|
14
|
+
}
|
|
15
|
+
function clampToViewport(placement, size, viewport, margin = FAB_EDGE_MARGIN) {
|
|
16
|
+
const maxLeft = Math.max(margin, viewport.width - size.width - margin);
|
|
17
|
+
const maxTop = Math.max(margin, viewport.height - size.height - margin);
|
|
18
|
+
return {
|
|
19
|
+
left: Math.min(Math.max(placement.left, margin), maxLeft),
|
|
20
|
+
top: Math.min(Math.max(placement.top, margin), maxTop)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function anchorPanelToLauncher(launcher, panel, viewport, gap = 12, margin = FAB_EDGE_MARGIN) {
|
|
24
|
+
const launcherCentre = launcher.top + launcher.height / 2;
|
|
25
|
+
const side = launcherCentre < viewport.height / 2 ? "below" : "above";
|
|
26
|
+
const top = side === "below" ? launcher.top + launcher.height + gap : launcher.top - gap - panel.height;
|
|
27
|
+
const preferredLeft = launcher.left + launcher.width / 2 < viewport.width / 2 ? launcher.left : launcher.left + launcher.width - panel.width;
|
|
28
|
+
const clamped = clampToViewport({ left: preferredLeft, top }, panel, viewport, margin);
|
|
29
|
+
const available = side === "below" ? viewport.height - clamped.top - margin : launcher.top - gap - margin;
|
|
30
|
+
return {
|
|
31
|
+
...clamped,
|
|
32
|
+
side,
|
|
33
|
+
maxHeight: Math.max(0, available)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function readStoredPosition(key) {
|
|
37
|
+
if (!key) return null;
|
|
38
|
+
try {
|
|
39
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
return Number.isFinite(parsed?.left) && Number.isFinite(parsed?.top) ? parsed : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeStoredPosition(key, placement, viewport) {
|
|
48
|
+
if (!key) return;
|
|
49
|
+
try {
|
|
50
|
+
globalThis.localStorage?.setItem(key, JSON.stringify({ ...placement, viewport }));
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
1
55
|
// src/shared/constants.ts
|
|
2
56
|
var DEFAULT_INSTANCE_NAME = "sponsor-relay";
|
|
3
57
|
var DEFAULT_MANIFEST_URL = "./rootfs-manifest.json";
|
|
@@ -42,7 +96,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
|
|
|
42
96
|
var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
|
|
43
97
|
|
|
44
98
|
// src/shared/package-version.ts
|
|
45
|
-
var UI_PACKAGE_VERSION = "0.9.
|
|
99
|
+
var UI_PACKAGE_VERSION = "0.9.2";
|
|
46
100
|
|
|
47
101
|
// src/shared/format.ts
|
|
48
102
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -6724,6 +6778,7 @@ function createSponsorRelayController(props = {}) {
|
|
|
6724
6778
|
return new SponsorRelayController(props);
|
|
6725
6779
|
}
|
|
6726
6780
|
export {
|
|
6781
|
+
DEFAULT_FAB_CORNER,
|
|
6727
6782
|
DEFAULT_INSTANCE_NAME,
|
|
6728
6783
|
DEFAULT_MANIFEST_URL,
|
|
6729
6784
|
DEFAULT_TIER_ID,
|
|
@@ -6734,6 +6789,8 @@ export {
|
|
|
6734
6789
|
DEFAULT_UCAN_STORE_SERVICE_ORIGIN,
|
|
6735
6790
|
DEFAULT_UCAN_STORE_USER_DELEGATION_EXPIRATION_SECONDS,
|
|
6736
6791
|
DEPLOYMENT_PENDING_WARNING_MS,
|
|
6792
|
+
FAB_DRAG_THRESHOLD,
|
|
6793
|
+
FAB_EDGE_MARGIN,
|
|
6737
6794
|
IDLE_DEPLOYMENT_PROGRESS,
|
|
6738
6795
|
INSTANCE_RUNTIME_PENDING_REFRESH_MS,
|
|
6739
6796
|
INSTANCE_RUNTIME_READY_REFRESH_MS,
|
|
@@ -6745,10 +6802,13 @@ export {
|
|
|
6745
6802
|
STALE_INSTANCE_ALLOCATION_COOLDOWN_MS,
|
|
6746
6803
|
SponsorRelayController,
|
|
6747
6804
|
UI_PACKAGE_VERSION,
|
|
6805
|
+
anchorPanelToLauncher,
|
|
6748
6806
|
buildSshCommand,
|
|
6807
|
+
clampToViewport,
|
|
6749
6808
|
connectWallet,
|
|
6750
6809
|
createDeploymentProgressEmitter,
|
|
6751
6810
|
createSponsorRelayController,
|
|
6811
|
+
dragExceedsThreshold,
|
|
6752
6812
|
formatDateTime,
|
|
6753
6813
|
formatNumber,
|
|
6754
6814
|
formatTierSpecLabel,
|
|
@@ -6757,9 +6817,12 @@ export {
|
|
|
6757
6817
|
joinRequiredPortForwards,
|
|
6758
6818
|
parseInstanceAddressPayload,
|
|
6759
6819
|
personalSign,
|
|
6820
|
+
readStoredPosition,
|
|
6821
|
+
resolveCornerPlacement,
|
|
6760
6822
|
resolveManifestSource,
|
|
6761
6823
|
rootfsHealth,
|
|
6762
6824
|
shortHash,
|
|
6763
6825
|
toChecksumAddress,
|
|
6764
|
-
watchWallet
|
|
6826
|
+
watchWallet,
|
|
6827
|
+
writeStoredPosition
|
|
6765
6828
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { onDestroy, onMount } from 'svelte'
|
|
3
3
|
|
|
4
|
-
import { UI_PACKAGE_VERSION, createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, joinMappedPorts, joinRequiredPortForwards, shortHash } from '../shared/index'
|
|
4
|
+
import { UI_PACKAGE_VERSION, anchorPanelToLauncher, clampToViewport, createSponsorRelayController, readStoredPosition, resolveCornerPlacement, writeStoredPosition, formatDateTime, formatNumber, formatTierSpecLabel, joinMappedPorts, joinRequiredPortForwards, shortHash } from '../shared/index'
|
|
5
5
|
import AccordionSection from './components/AccordionSection.svelte'
|
|
6
6
|
import CopyButton from './components/CopyButton.svelte'
|
|
7
7
|
import LauncherButton from './components/LauncherButton.svelte'
|
|
@@ -23,6 +23,9 @@ export let apiHosts = undefined
|
|
|
23
23
|
export let crnSource = undefined
|
|
24
24
|
export let schedulerApiHost = undefined
|
|
25
25
|
export let twoN6ApiHost = undefined
|
|
26
|
+
export let position = 'bottom-right'
|
|
27
|
+
export let draggable = false
|
|
28
|
+
export let positionStorageKey = undefined
|
|
26
29
|
|
|
27
30
|
const controller = createSponsorRelayController({
|
|
28
31
|
manifestUrl,
|
|
@@ -131,13 +134,93 @@ export let apiHosts = undefined
|
|
|
131
134
|
.join('\n')
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// Placement lives here rather than in the launcher because the panel has to
|
|
138
|
+
// be anchored to it, and only this component renders both.
|
|
139
|
+
let launcherEl = null
|
|
140
|
+
let panelEl = null
|
|
141
|
+
let placement = null
|
|
142
|
+
let panelPlacement = null
|
|
143
|
+
let dragOffset = null
|
|
144
|
+
|
|
145
|
+
function viewport() {
|
|
146
|
+
return { width: window.innerWidth, height: window.innerHeight }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function launcherSize() {
|
|
150
|
+
const rect = launcherEl?.getBoundingClientRect()
|
|
151
|
+
return { width: rect?.width ?? 180, height: rect?.height ?? 44 }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function initPlacement() {
|
|
155
|
+
if (launcherMode !== 'floating' || typeof window === 'undefined') return
|
|
156
|
+
const stored = readStoredPosition(positionStorageKey)
|
|
157
|
+
placement = stored
|
|
158
|
+
? clampToViewport(stored, launcherSize(), viewport())
|
|
159
|
+
: resolveCornerPlacement(position, launcherSize(), viewport())
|
|
160
|
+
updatePanelPlacement()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updatePanelPlacement() {
|
|
164
|
+
if (!state.open || !placement || launcherMode !== 'floating') {
|
|
165
|
+
panelPlacement = null
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
const panelRect = panelEl?.getBoundingClientRect()
|
|
169
|
+
panelPlacement = anchorPanelToLauncher(
|
|
170
|
+
{ ...placement, ...launcherSize() },
|
|
171
|
+
{ width: panelRect?.width ?? 448, height: panelRect?.height ?? 520 },
|
|
172
|
+
viewport(),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleDragStart(point) {
|
|
177
|
+
if (!placement) return
|
|
178
|
+
// Remember where inside the button the finger landed, so it does not jump
|
|
179
|
+
// to the pointer on the first move.
|
|
180
|
+
dragOffset = { x: point.x - placement.left, y: point.y - placement.top }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleDragMove(point) {
|
|
184
|
+
if (!dragOffset) return
|
|
185
|
+
placement = clampToViewport(
|
|
186
|
+
{ left: point.x - dragOffset.x, top: point.y - dragOffset.y },
|
|
187
|
+
launcherSize(),
|
|
188
|
+
viewport(),
|
|
189
|
+
)
|
|
190
|
+
updatePanelPlacement()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleDragEnd(wasDragging) {
|
|
194
|
+
dragOffset = null
|
|
195
|
+
if (wasDragging && placement) writeStoredPosition(positionStorageKey, placement, viewport())
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleViewportChange() {
|
|
199
|
+
if (!placement) return
|
|
200
|
+
placement = clampToViewport(placement, launcherSize(), viewport())
|
|
201
|
+
updatePanelPlacement()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
$: if (state.open !== undefined) updatePanelPlacement()
|
|
205
|
+
|
|
206
|
+
$: panelStyle = panelPlacement
|
|
207
|
+
? `left:${panelPlacement.left}px;top:${panelPlacement.top}px;right:auto;bottom:auto;max-height:${panelPlacement.maxHeight}px;`
|
|
208
|
+
: ''
|
|
209
|
+
|
|
134
210
|
onMount(async () => {
|
|
135
211
|
const unsubscribe = controller.subscribe((next) => {
|
|
136
212
|
state = next
|
|
137
213
|
})
|
|
138
214
|
|
|
139
215
|
await controller.init()
|
|
140
|
-
|
|
216
|
+
initPlacement()
|
|
217
|
+
window.addEventListener('resize', handleViewportChange)
|
|
218
|
+
window.addEventListener('orientationchange', handleViewportChange)
|
|
219
|
+
return () => {
|
|
220
|
+
window.removeEventListener('resize', handleViewportChange)
|
|
221
|
+
window.removeEventListener('orientationchange', handleViewportChange)
|
|
222
|
+
unsubscribe()
|
|
223
|
+
}
|
|
141
224
|
})
|
|
142
225
|
|
|
143
226
|
onDestroy(() => {
|
|
@@ -145,14 +228,24 @@ export let apiHosts = undefined
|
|
|
145
228
|
})
|
|
146
229
|
</script>
|
|
147
230
|
|
|
148
|
-
<LauncherButton
|
|
231
|
+
<LauncherButton
|
|
232
|
+
bind:element={launcherEl}
|
|
233
|
+
open={state.open}
|
|
234
|
+
onToggle={() => controller.toggleOpen()}
|
|
235
|
+
mode={launcherMode}
|
|
236
|
+
{placement}
|
|
237
|
+
{draggable}
|
|
238
|
+
onDragStart={handleDragStart}
|
|
239
|
+
onDragMove={handleDragMove}
|
|
240
|
+
onDragEnd={handleDragEnd}
|
|
241
|
+
/>
|
|
149
242
|
|
|
150
243
|
{#if state.open}
|
|
151
244
|
<div class="backdrop" on:click={() => controller.setOpen(false)}></div>
|
|
152
245
|
{/if}
|
|
153
246
|
|
|
154
247
|
{#if state.open}
|
|
155
|
-
<aside class="panel">
|
|
248
|
+
<aside class="panel" bind:this={panelEl} style={panelStyle}>
|
|
156
249
|
<div class="panel-head">
|
|
157
250
|
<div>
|
|
158
251
|
<p class="eyebrow">
|
|
@@ -1,10 +1,68 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { dragExceedsThreshold } from '../../shared/index'
|
|
3
|
+
|
|
2
4
|
export let open = false
|
|
3
5
|
export let onToggle = () => {}
|
|
4
6
|
export let mode = 'floating'
|
|
7
|
+
/** Absolute placement from the parent, or null to use the CSS corner default. */
|
|
8
|
+
export let placement = null
|
|
9
|
+
export let draggable = false
|
|
10
|
+
/** Called with the pointer position while dragging; the parent owns the maths. */
|
|
11
|
+
export let onDragStart = () => {}
|
|
12
|
+
export let onDragMove = () => {}
|
|
13
|
+
export let onDragEnd = () => {}
|
|
14
|
+
/** Bound by the parent so it can measure the launcher to anchor the panel. */
|
|
15
|
+
export let element = null
|
|
16
|
+
|
|
17
|
+
let dragging = false
|
|
18
|
+
let pressOrigin = null
|
|
19
|
+
|
|
20
|
+
function handlePointerDown(event) {
|
|
21
|
+
if (!draggable || mode !== 'floating' || event.button > 0) return
|
|
22
|
+
pressOrigin = { x: event.clientX, y: event.clientY }
|
|
23
|
+
dragging = false
|
|
24
|
+
event.currentTarget.setPointerCapture?.(event.pointerId)
|
|
25
|
+
onDragStart(pressOrigin)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handlePointerMove(event) {
|
|
29
|
+
if (!pressOrigin) return
|
|
30
|
+
const point = { x: event.clientX, y: event.clientY }
|
|
31
|
+
// Only become a drag once the pointer has travelled far enough; below the
|
|
32
|
+
// threshold this is still a press that should open the panel.
|
|
33
|
+
if (!dragging && !dragExceedsThreshold(pressOrigin, point)) return
|
|
34
|
+
dragging = true
|
|
35
|
+
onDragMove(point)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handlePointerUp(event) {
|
|
39
|
+
if (!pressOrigin) return
|
|
40
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId)
|
|
41
|
+
const wasDragging = dragging
|
|
42
|
+
pressOrigin = null
|
|
43
|
+
dragging = false
|
|
44
|
+
onDragEnd(wasDragging)
|
|
45
|
+
// A drag must not also toggle the panel; a tap must.
|
|
46
|
+
if (!wasDragging) onToggle()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$: placementStyle =
|
|
50
|
+
placement && mode === 'floating'
|
|
51
|
+
? `left:${placement.left}px;top:${placement.top}px;right:auto;bottom:auto;`
|
|
52
|
+
: ''
|
|
5
53
|
</script>
|
|
6
54
|
|
|
7
|
-
<button
|
|
55
|
+
<button
|
|
56
|
+
bind:this={element}
|
|
57
|
+
class={`launcher ${mode} ${open ? 'open' : ''} ${draggable ? 'draggable' : ''}`}
|
|
58
|
+
style={placementStyle}
|
|
59
|
+
type="button"
|
|
60
|
+
on:pointerdown={handlePointerDown}
|
|
61
|
+
on:pointermove={handlePointerMove}
|
|
62
|
+
on:pointerup={handlePointerUp}
|
|
63
|
+
on:pointercancel={handlePointerUp}
|
|
64
|
+
on:click={draggable ? undefined : onToggle}
|
|
65
|
+
>
|
|
8
66
|
<svg class="mark" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
|
|
9
67
|
<line x1="17" y1="15.4" x2="19.6" y2="13" class="mark-link" stroke-width="2.4" stroke-linecap="round" />
|
|
10
68
|
<circle cx="11.5" cy="20.5" r="7" class="mark-node" />
|
|
@@ -53,6 +111,12 @@
|
|
|
53
111
|
z-index: 10000;
|
|
54
112
|
}
|
|
55
113
|
|
|
114
|
+
.launcher.draggable {
|
|
115
|
+
/* Without this the browser scrolls the page instead of moving the button. */
|
|
116
|
+
touch-action: none;
|
|
117
|
+
user-select: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
56
120
|
.launcher.inline {
|
|
57
121
|
position: relative;
|
|
58
122
|
z-index: auto;
|