@le-space/ui 0.9.1 → 0.9.4
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 +25 -0
- package/react/index.js +218 -25
- package/shared/index.d.ts +110 -2
- package/shared/index.js +124 -16
- package/svelte/SponsorRelayFab.svelte +108 -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.4",
|
|
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.4",
|
|
48
|
+
"@le-space/core": "0.9.4",
|
|
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;
|
|
@@ -400,6 +424,7 @@ declare class SponsorRelayController {
|
|
|
400
424
|
private emit;
|
|
401
425
|
private trace;
|
|
402
426
|
private patch;
|
|
427
|
+
private lastProgressLabel;
|
|
403
428
|
private emitProgress;
|
|
404
429
|
private configureRelayBootstrapRegistration;
|
|
405
430
|
private probeRelayReachableOverProxy;
|
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.4";
|
|
47
100
|
|
|
48
101
|
// src/shared/format.ts
|
|
49
102
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -2777,7 +2830,8 @@ function browserMultiaddrTransport(addr) {
|
|
|
2777
2830
|
}
|
|
2778
2831
|
function selectCompactRelayBootstrapMultiaddrs(addrs, limit = DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT) {
|
|
2779
2832
|
const entries = filterPublicMultiaddrs(addrs, {
|
|
2780
|
-
browserDialableOnly: true
|
|
2833
|
+
browserDialableOnly: true,
|
|
2834
|
+
allowInsecureWebSockets: true
|
|
2781
2835
|
}).map((addr, index) => ({
|
|
2782
2836
|
addr,
|
|
2783
2837
|
index,
|
|
@@ -2847,12 +2901,23 @@ function isLocalHostname(host) {
|
|
|
2847
2901
|
function hasPeerId(addr) {
|
|
2848
2902
|
return splitMultiaddr(addr).includes("p2p");
|
|
2849
2903
|
}
|
|
2850
|
-
function
|
|
2904
|
+
function isSecureWebSocketMultiaddr(addr) {
|
|
2905
|
+
const parts = splitMultiaddr(addr.toLowerCase());
|
|
2906
|
+
if (parts.includes("wss")) return true;
|
|
2907
|
+
const wsIndex = parts.indexOf("ws");
|
|
2908
|
+
return wsIndex > 0 && parts.lastIndexOf("tls", wsIndex) >= 0;
|
|
2909
|
+
}
|
|
2910
|
+
function isWebSocketMultiaddr(addr) {
|
|
2911
|
+
const parts = splitMultiaddr(addr.toLowerCase());
|
|
2912
|
+
return parts.includes("ws") || parts.includes("wss");
|
|
2913
|
+
}
|
|
2914
|
+
function isBrowserDialableMultiaddr(addr, options = {}) {
|
|
2851
2915
|
const normalized = addr.toLowerCase();
|
|
2852
2916
|
if (normalized.includes("/webtransport") || normalized.includes("/webrtc-direct")) {
|
|
2853
2917
|
return normalized.includes("/certhash/");
|
|
2854
2918
|
}
|
|
2855
|
-
|
|
2919
|
+
if (!isWebSocketMultiaddr(addr)) return false;
|
|
2920
|
+
return (options.allowInsecureWebSockets ?? false) || isSecureWebSocketMultiaddr(addr);
|
|
2856
2921
|
}
|
|
2857
2922
|
function dedupeMultiaddrs(addrs) {
|
|
2858
2923
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2886,7 +2951,9 @@ function filterPublicMultiaddrs(addrs, options = {}) {
|
|
|
2886
2951
|
return dedupeMultiaddrs(addrs).filter((addr) => {
|
|
2887
2952
|
if (!isPublicMultiaddr(addr)) return false;
|
|
2888
2953
|
if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
|
|
2889
|
-
if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr
|
|
2954
|
+
if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr, {
|
|
2955
|
+
allowInsecureWebSockets: options.allowInsecureWebSockets
|
|
2956
|
+
})) {
|
|
2890
2957
|
return false;
|
|
2891
2958
|
}
|
|
2892
2959
|
return true;
|
|
@@ -3001,7 +3068,8 @@ function buildRelayBootstrapPostContent(args) {
|
|
|
3001
3068
|
const updatedAt = args.now ?? Date.now();
|
|
3002
3069
|
const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
|
|
3003
3070
|
const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
|
|
3004
|
-
browserDialableOnly: true
|
|
3071
|
+
browserDialableOnly: true,
|
|
3072
|
+
allowInsecureWebSockets: true
|
|
3005
3073
|
}) : void 0;
|
|
3006
3074
|
const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
|
|
3007
3075
|
fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
|
|
@@ -3083,7 +3151,8 @@ async function signRelayBootstrapAuthorization(args) {
|
|
|
3083
3151
|
async function signRelayBootstrapProof(args) {
|
|
3084
3152
|
const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
|
|
3085
3153
|
const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
|
|
3086
|
-
browserDialableOnly: true
|
|
3154
|
+
browserDialableOnly: true,
|
|
3155
|
+
allowInsecureWebSockets: true
|
|
3087
3156
|
}) : void 0;
|
|
3088
3157
|
const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
|
|
3089
3158
|
fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
|
|
@@ -3885,12 +3954,30 @@ function validateUcanStoreBootstrapPackage(value) {
|
|
|
3885
3954
|
function deploymentNoun(profile) {
|
|
3886
3955
|
return profile === "ucan-store" ? "service" : "relay";
|
|
3887
3956
|
}
|
|
3957
|
+
function describeRequest(input, init) {
|
|
3958
|
+
const method = String(init?.method ?? "GET").toUpperCase();
|
|
3959
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof input?.url === "string" ? input.url : "<unknown url>";
|
|
3960
|
+
return `${method} ${url}`;
|
|
3961
|
+
}
|
|
3962
|
+
function deploymentFailureMessage(error, stage) {
|
|
3963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3964
|
+
const step = String(stage ?? "").trim();
|
|
3965
|
+
return step ? `${message} (while: ${step})` : message;
|
|
3966
|
+
}
|
|
3888
3967
|
function asJsonFetch(input, init) {
|
|
3889
|
-
return fetch(input, init).then(
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3968
|
+
return fetch(input, init).then(
|
|
3969
|
+
async (response) => ({
|
|
3970
|
+
ok: response.ok,
|
|
3971
|
+
status: response.status,
|
|
3972
|
+
json: async () => await response.json()
|
|
3973
|
+
}),
|
|
3974
|
+
(error) => {
|
|
3975
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3976
|
+
throw new Error(`${reason} (${describeRequest(input, init)})`, {
|
|
3977
|
+
cause: error
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
);
|
|
3894
3981
|
}
|
|
3895
3982
|
function isConfirmedGuestSetupResult(value) {
|
|
3896
3983
|
return Boolean(
|
|
@@ -4533,11 +4620,15 @@ var SponsorRelayController = class {
|
|
|
4533
4620
|
};
|
|
4534
4621
|
this.emit();
|
|
4535
4622
|
}
|
|
4623
|
+
lastProgressLabel = null;
|
|
4536
4624
|
emitProgress(event) {
|
|
4537
4625
|
const nextEvent = {
|
|
4538
4626
|
...event,
|
|
4539
4627
|
timestamp: Date.now()
|
|
4540
4628
|
};
|
|
4629
|
+
if (nextEvent.stage !== "error") {
|
|
4630
|
+
this.lastProgressLabel = nextEvent.label;
|
|
4631
|
+
}
|
|
4541
4632
|
this.trace(`progress:${nextEvent.stage}`, {
|
|
4542
4633
|
label: nextEvent.label,
|
|
4543
4634
|
progress: nextEvent.progress,
|
|
@@ -6454,10 +6545,15 @@ var SponsorRelayController = class {
|
|
|
6454
6545
|
attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
|
|
6455
6546
|
);
|
|
6456
6547
|
} catch (error) {
|
|
6457
|
-
|
|
6548
|
+
const message = deploymentFailureMessage(error, this.lastProgressLabel);
|
|
6549
|
+
this.trace("deploy:error", {
|
|
6550
|
+
message,
|
|
6551
|
+
stage: this.lastProgressLabel,
|
|
6552
|
+
error
|
|
6553
|
+
});
|
|
6458
6554
|
this.patch({
|
|
6459
6555
|
busy: { deploying: false },
|
|
6460
|
-
errorText:
|
|
6556
|
+
errorText: message,
|
|
6461
6557
|
statusText: "Deployment failed"
|
|
6462
6558
|
});
|
|
6463
6559
|
this.emitProgress({
|
|
@@ -6465,7 +6561,7 @@ var SponsorRelayController = class {
|
|
|
6465
6561
|
label: "Deployment failed",
|
|
6466
6562
|
progress: 100,
|
|
6467
6563
|
status: "error",
|
|
6468
|
-
error:
|
|
6564
|
+
error: message
|
|
6469
6565
|
});
|
|
6470
6566
|
}
|
|
6471
6567
|
}
|
|
@@ -7169,6 +7265,14 @@ function SponsorRelayFab(props) {
|
|
|
7169
7265
|
}
|
|
7170
7266
|
};
|
|
7171
7267
|
const launcherRef = React2.useRef(null);
|
|
7268
|
+
const panelRef = React2.useRef(null);
|
|
7269
|
+
const draggable = props.draggable ?? false;
|
|
7270
|
+
const corner = props.position ?? "bottom-right";
|
|
7271
|
+
const storageKey = props.positionStorageKey;
|
|
7272
|
+
const [placement, setPlacement] = React2.useState(null);
|
|
7273
|
+
const dragOrigin = React2.useRef(null);
|
|
7274
|
+
const dragOffset = React2.useRef(null);
|
|
7275
|
+
const draggedRef = React2.useRef(false);
|
|
7172
7276
|
const [inlinePanelStyle, setInlinePanelStyle] = React2.useState(null);
|
|
7173
7277
|
const [compactInlineLabel, setCompactInlineLabel] = React2.useState(false);
|
|
7174
7278
|
React2.useEffect(() => {
|
|
@@ -7198,6 +7302,67 @@ function SponsorRelayFab(props) {
|
|
|
7198
7302
|
window.clearTimeout(timeout);
|
|
7199
7303
|
};
|
|
7200
7304
|
}, [state.deploymentProgress.stage, state.deploymentProgress.timestamp]);
|
|
7305
|
+
const viewportSize = () => ({
|
|
7306
|
+
width: window.innerWidth,
|
|
7307
|
+
height: window.innerHeight
|
|
7308
|
+
});
|
|
7309
|
+
const launcherSize = () => {
|
|
7310
|
+
const rect = launcherRef.current?.getBoundingClientRect();
|
|
7311
|
+
return { width: rect?.width ?? 180, height: rect?.height ?? 44 };
|
|
7312
|
+
};
|
|
7313
|
+
React2.useEffect(() => {
|
|
7314
|
+
if (launcherMode !== "floating") {
|
|
7315
|
+
setPlacement(null);
|
|
7316
|
+
return;
|
|
7317
|
+
}
|
|
7318
|
+
const stored = readStoredPosition(storageKey);
|
|
7319
|
+
setPlacement(
|
|
7320
|
+
stored ? clampToViewport(stored, launcherSize(), viewportSize()) : resolveCornerPlacement(corner, launcherSize(), viewportSize())
|
|
7321
|
+
);
|
|
7322
|
+
const onViewportChange = () => setPlacement(
|
|
7323
|
+
(current) => current ? clampToViewport(current, launcherSize(), viewportSize()) : current
|
|
7324
|
+
);
|
|
7325
|
+
window.addEventListener("resize", onViewportChange);
|
|
7326
|
+
window.addEventListener("orientationchange", onViewportChange);
|
|
7327
|
+
return () => {
|
|
7328
|
+
window.removeEventListener("resize", onViewportChange);
|
|
7329
|
+
window.removeEventListener("orientationchange", onViewportChange);
|
|
7330
|
+
};
|
|
7331
|
+
}, [launcherMode, corner, storageKey]);
|
|
7332
|
+
const handlePointerDown = (event) => {
|
|
7333
|
+
if (!draggable || launcherMode !== "floating" || event.button > 0 || !placement) return;
|
|
7334
|
+
dragOrigin.current = { x: event.clientX, y: event.clientY };
|
|
7335
|
+
dragOffset.current = { x: event.clientX - placement.left, y: event.clientY - placement.top };
|
|
7336
|
+
draggedRef.current = false;
|
|
7337
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
7338
|
+
};
|
|
7339
|
+
const handlePointerMove = (event) => {
|
|
7340
|
+
const origin = dragOrigin.current;
|
|
7341
|
+
const offset = dragOffset.current;
|
|
7342
|
+
if (!origin || !offset) return;
|
|
7343
|
+
const point = { x: event.clientX, y: event.clientY };
|
|
7344
|
+
if (!draggedRef.current && !dragExceedsThreshold(origin, point)) return;
|
|
7345
|
+
draggedRef.current = true;
|
|
7346
|
+
setPlacement(
|
|
7347
|
+
clampToViewport(
|
|
7348
|
+
{ left: point.x - offset.x, top: point.y - offset.y },
|
|
7349
|
+
launcherSize(),
|
|
7350
|
+
viewportSize()
|
|
7351
|
+
)
|
|
7352
|
+
);
|
|
7353
|
+
};
|
|
7354
|
+
const handlePointerUp = (event) => {
|
|
7355
|
+
if (!dragOrigin.current) return;
|
|
7356
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
7357
|
+
dragOrigin.current = null;
|
|
7358
|
+
dragOffset.current = null;
|
|
7359
|
+
if (draggedRef.current && placement) {
|
|
7360
|
+
writeStoredPosition(storageKey, placement, viewportSize());
|
|
7361
|
+
} else {
|
|
7362
|
+
controller.toggleOpen();
|
|
7363
|
+
}
|
|
7364
|
+
draggedRef.current = false;
|
|
7365
|
+
};
|
|
7201
7366
|
React2.useEffect(() => {
|
|
7202
7367
|
if (launcherMode !== "inline" || !state.open) {
|
|
7203
7368
|
return;
|
|
@@ -7251,18 +7416,37 @@ function SponsorRelayFab(props) {
|
|
|
7251
7416
|
const inlineButtonBorder = `1px solid ${themedLauncherBorder}`;
|
|
7252
7417
|
const inlineBadgeBackground = themedLauncherBadgeBackground;
|
|
7253
7418
|
const inlineBadgeBorder = `1px solid ${themedLauncherBadgeBorder}`;
|
|
7419
|
+
const floatingPanelStyle = () => {
|
|
7420
|
+
if (!placement || typeof window === "undefined") {
|
|
7421
|
+
return {
|
|
7422
|
+
...basePanelStyle,
|
|
7423
|
+
right: "1.4rem",
|
|
7424
|
+
bottom: "11.5rem",
|
|
7425
|
+
maxHeight: "calc(100vh - 12.5rem)"
|
|
7426
|
+
};
|
|
7427
|
+
}
|
|
7428
|
+
const panelRect = panelRef.current?.getBoundingClientRect();
|
|
7429
|
+
const anchored = anchorPanelToLauncher(
|
|
7430
|
+
{ ...placement, ...launcherSize() },
|
|
7431
|
+
{ width: panelRect?.width ?? 448, height: panelRect?.height ?? 520 },
|
|
7432
|
+
viewportSize()
|
|
7433
|
+
);
|
|
7434
|
+
return {
|
|
7435
|
+
...basePanelStyle,
|
|
7436
|
+
left: anchored.left,
|
|
7437
|
+
top: anchored.top,
|
|
7438
|
+
right: "auto",
|
|
7439
|
+
bottom: "auto",
|
|
7440
|
+
maxHeight: anchored.maxHeight
|
|
7441
|
+
};
|
|
7442
|
+
};
|
|
7254
7443
|
const panelStyle = launcherMode === "inline" ? inlinePanelStyle ?? {
|
|
7255
7444
|
...basePanelStyle,
|
|
7256
7445
|
top: "4.75rem",
|
|
7257
7446
|
right: "1rem",
|
|
7258
7447
|
maxHeight: "calc(100vh - 6rem)",
|
|
7259
7448
|
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
|
-
};
|
|
7449
|
+
} : floatingPanelStyle();
|
|
7266
7450
|
const launcherLabel = deploymentLauncherLabel(state, compactInlineLabel, launcherMode);
|
|
7267
7451
|
const rootfsBlocked = state.rootfsHealth.tone === "error";
|
|
7268
7452
|
const deployCallToAction = state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : rootfsBlocked ? "Deployment blocked \u2014 Rootfs unavailable" : deploymentButtonLabel(state) : "Connect MetaMask";
|
|
@@ -7273,7 +7457,11 @@ function SponsorRelayFab(props) {
|
|
|
7273
7457
|
{
|
|
7274
7458
|
ref: launcherRef,
|
|
7275
7459
|
type: "button",
|
|
7276
|
-
onClick: () => controller.toggleOpen(),
|
|
7460
|
+
onClick: draggable ? void 0 : () => controller.toggleOpen(),
|
|
7461
|
+
onPointerDown: handlePointerDown,
|
|
7462
|
+
onPointerMove: handlePointerMove,
|
|
7463
|
+
onPointerUp: handlePointerUp,
|
|
7464
|
+
onPointerCancel: handlePointerUp,
|
|
7277
7465
|
onMouseEnter: () => setHovered(true),
|
|
7278
7466
|
onMouseLeave: () => setHovered(false),
|
|
7279
7467
|
onFocus: () => setHovered(true),
|
|
@@ -7281,8 +7469,13 @@ function SponsorRelayFab(props) {
|
|
|
7281
7469
|
title: indicator.detail ?? deploymentPanelTitle(state),
|
|
7282
7470
|
style: {
|
|
7283
7471
|
position: launcherMode === "floating" ? "fixed" : "relative",
|
|
7284
|
-
|
|
7285
|
-
|
|
7472
|
+
left: launcherMode === "floating" && placement ? placement.left : void 0,
|
|
7473
|
+
top: launcherMode === "floating" && placement ? placement.top : void 0,
|
|
7474
|
+
right: launcherMode === "floating" && !placement ? "1.4rem" : void 0,
|
|
7475
|
+
bottom: launcherMode === "floating" && !placement ? "5.8rem" : void 0,
|
|
7476
|
+
// Without this the browser scrolls the page instead of moving the button.
|
|
7477
|
+
touchAction: draggable ? "none" : void 0,
|
|
7478
|
+
userSelect: draggable ? "none" : void 0,
|
|
7286
7479
|
zIndex: launcherMode === "floating" ? 1e4 : "auto",
|
|
7287
7480
|
borderRadius: "999px",
|
|
7288
7481
|
border: launcherMode === "floating" ? `1px solid ${themedLauncherBorder}` : inlineButtonBorder,
|
|
@@ -7412,7 +7605,7 @@ function SponsorRelayFab(props) {
|
|
|
7412
7605
|
}
|
|
7413
7606
|
}
|
|
7414
7607
|
),
|
|
7415
|
-
/* @__PURE__ */ jsxs("aside", { style: panelStyle, children: [
|
|
7608
|
+
/* @__PURE__ */ jsxs("aside", { ref: panelRef, style: panelStyle, children: [
|
|
7416
7609
|
/* @__PURE__ */ jsxs(
|
|
7417
7610
|
"div",
|
|
7418
7611
|
{
|
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.4";
|
|
401
484
|
|
|
402
485
|
interface RootfsRequiredPortForward {
|
|
403
486
|
port: number;
|
|
@@ -433,6 +516,30 @@ declare function connectWallet(provider?: EthereumProviderLike | null): Promise<
|
|
|
433
516
|
declare function personalSign(address: string, message: string, provider?: EthereumProviderLike | null): Promise<string>;
|
|
434
517
|
declare function watchWallet(onChange: () => void, provider?: EthereumProviderLike | null): () => void;
|
|
435
518
|
|
|
519
|
+
/**
|
|
520
|
+
* What a request was, for an error that otherwise says nothing.
|
|
521
|
+
*
|
|
522
|
+
* `fetch` rejects with a bare `TypeError: Failed to fetch` for every network
|
|
523
|
+
* failure — no URL, no method, no cause. Carried up through the deploy path
|
|
524
|
+
* that becomes "Relay Button deployment failed: Failed to fetch", which is
|
|
525
|
+
* where a run of this project ended with no way to tell which of a dozen calls
|
|
526
|
+
* had failed.
|
|
527
|
+
*
|
|
528
|
+
* Exported for its test; `init.method` is read defensively because a call site
|
|
529
|
+
* may pass none.
|
|
530
|
+
*/
|
|
531
|
+
declare function describeRequest(input: RequestInfo | URL, init?: RequestInit): string;
|
|
532
|
+
/**
|
|
533
|
+
* The failure message, with the step it happened in.
|
|
534
|
+
*
|
|
535
|
+
* A deployment walks two dozen calls across Aleph, a CRN and a guest VM. When
|
|
536
|
+
* one of them fails with a message that names none of them — `Failed to fetch`
|
|
537
|
+
* is the standard case — the step is the only thing that narrows it down, and
|
|
538
|
+
* it is already known.
|
|
539
|
+
*
|
|
540
|
+
* Exported for its test.
|
|
541
|
+
*/
|
|
542
|
+
declare function deploymentFailureMessage(error: unknown, stage: string | null | undefined): string;
|
|
436
543
|
/**
|
|
437
544
|
* Normalise the two address documents in the fleet into one list of groups.
|
|
438
545
|
*
|
|
@@ -467,6 +574,7 @@ declare class SponsorRelayController {
|
|
|
467
574
|
private emit;
|
|
468
575
|
private trace;
|
|
469
576
|
private patch;
|
|
577
|
+
private lastProgressLabel;
|
|
470
578
|
private emitProgress;
|
|
471
579
|
private configureRelayBootstrapRegistration;
|
|
472
580
|
private probeRelayReachableOverProxy;
|
|
@@ -520,4 +628,4 @@ interface DeploymentProgressEmitter {
|
|
|
520
628
|
}
|
|
521
629
|
declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
|
|
522
630
|
|
|
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 };
|
|
631
|
+
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, deploymentFailureMessage, describeRequest, 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.4";
|
|
46
100
|
|
|
47
101
|
// src/shared/format.ts
|
|
48
102
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -2776,7 +2830,8 @@ function browserMultiaddrTransport(addr) {
|
|
|
2776
2830
|
}
|
|
2777
2831
|
function selectCompactRelayBootstrapMultiaddrs(addrs, limit = DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT) {
|
|
2778
2832
|
const entries = filterPublicMultiaddrs(addrs, {
|
|
2779
|
-
browserDialableOnly: true
|
|
2833
|
+
browserDialableOnly: true,
|
|
2834
|
+
allowInsecureWebSockets: true
|
|
2780
2835
|
}).map((addr, index) => ({
|
|
2781
2836
|
addr,
|
|
2782
2837
|
index,
|
|
@@ -2846,12 +2901,23 @@ function isLocalHostname(host) {
|
|
|
2846
2901
|
function hasPeerId(addr) {
|
|
2847
2902
|
return splitMultiaddr(addr).includes("p2p");
|
|
2848
2903
|
}
|
|
2849
|
-
function
|
|
2904
|
+
function isSecureWebSocketMultiaddr(addr) {
|
|
2905
|
+
const parts = splitMultiaddr(addr.toLowerCase());
|
|
2906
|
+
if (parts.includes("wss")) return true;
|
|
2907
|
+
const wsIndex = parts.indexOf("ws");
|
|
2908
|
+
return wsIndex > 0 && parts.lastIndexOf("tls", wsIndex) >= 0;
|
|
2909
|
+
}
|
|
2910
|
+
function isWebSocketMultiaddr(addr) {
|
|
2911
|
+
const parts = splitMultiaddr(addr.toLowerCase());
|
|
2912
|
+
return parts.includes("ws") || parts.includes("wss");
|
|
2913
|
+
}
|
|
2914
|
+
function isBrowserDialableMultiaddr(addr, options = {}) {
|
|
2850
2915
|
const normalized = addr.toLowerCase();
|
|
2851
2916
|
if (normalized.includes("/webtransport") || normalized.includes("/webrtc-direct")) {
|
|
2852
2917
|
return normalized.includes("/certhash/");
|
|
2853
2918
|
}
|
|
2854
|
-
|
|
2919
|
+
if (!isWebSocketMultiaddr(addr)) return false;
|
|
2920
|
+
return (options.allowInsecureWebSockets ?? false) || isSecureWebSocketMultiaddr(addr);
|
|
2855
2921
|
}
|
|
2856
2922
|
function dedupeMultiaddrs(addrs) {
|
|
2857
2923
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2885,7 +2951,9 @@ function filterPublicMultiaddrs(addrs, options = {}) {
|
|
|
2885
2951
|
return dedupeMultiaddrs(addrs).filter((addr) => {
|
|
2886
2952
|
if (!isPublicMultiaddr(addr)) return false;
|
|
2887
2953
|
if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
|
|
2888
|
-
if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr
|
|
2954
|
+
if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr, {
|
|
2955
|
+
allowInsecureWebSockets: options.allowInsecureWebSockets
|
|
2956
|
+
})) {
|
|
2889
2957
|
return false;
|
|
2890
2958
|
}
|
|
2891
2959
|
return true;
|
|
@@ -3000,7 +3068,8 @@ function buildRelayBootstrapPostContent(args) {
|
|
|
3000
3068
|
const updatedAt = args.now ?? Date.now();
|
|
3001
3069
|
const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
|
|
3002
3070
|
const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
|
|
3003
|
-
browserDialableOnly: true
|
|
3071
|
+
browserDialableOnly: true,
|
|
3072
|
+
allowInsecureWebSockets: true
|
|
3004
3073
|
}) : void 0;
|
|
3005
3074
|
const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
|
|
3006
3075
|
fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
|
|
@@ -3082,7 +3151,8 @@ async function signRelayBootstrapAuthorization(args) {
|
|
|
3082
3151
|
async function signRelayBootstrapProof(args) {
|
|
3083
3152
|
const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
|
|
3084
3153
|
const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
|
|
3085
|
-
browserDialableOnly: true
|
|
3154
|
+
browserDialableOnly: true,
|
|
3155
|
+
allowInsecureWebSockets: true
|
|
3086
3156
|
}) : void 0;
|
|
3087
3157
|
const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
|
|
3088
3158
|
fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
|
|
@@ -3884,12 +3954,30 @@ function validateUcanStoreBootstrapPackage(value) {
|
|
|
3884
3954
|
function deploymentNoun(profile) {
|
|
3885
3955
|
return profile === "ucan-store" ? "service" : "relay";
|
|
3886
3956
|
}
|
|
3957
|
+
function describeRequest(input, init) {
|
|
3958
|
+
const method = String(init?.method ?? "GET").toUpperCase();
|
|
3959
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof input?.url === "string" ? input.url : "<unknown url>";
|
|
3960
|
+
return `${method} ${url}`;
|
|
3961
|
+
}
|
|
3962
|
+
function deploymentFailureMessage(error, stage) {
|
|
3963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3964
|
+
const step = String(stage ?? "").trim();
|
|
3965
|
+
return step ? `${message} (while: ${step})` : message;
|
|
3966
|
+
}
|
|
3887
3967
|
function asJsonFetch(input, init) {
|
|
3888
|
-
return fetch(input, init).then(
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3968
|
+
return fetch(input, init).then(
|
|
3969
|
+
async (response) => ({
|
|
3970
|
+
ok: response.ok,
|
|
3971
|
+
status: response.status,
|
|
3972
|
+
json: async () => await response.json()
|
|
3973
|
+
}),
|
|
3974
|
+
(error) => {
|
|
3975
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3976
|
+
throw new Error(`${reason} (${describeRequest(input, init)})`, {
|
|
3977
|
+
cause: error
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
);
|
|
3893
3981
|
}
|
|
3894
3982
|
function isConfirmedGuestSetupResult(value) {
|
|
3895
3983
|
return Boolean(
|
|
@@ -4532,11 +4620,15 @@ var SponsorRelayController = class {
|
|
|
4532
4620
|
};
|
|
4533
4621
|
this.emit();
|
|
4534
4622
|
}
|
|
4623
|
+
lastProgressLabel = null;
|
|
4535
4624
|
emitProgress(event) {
|
|
4536
4625
|
const nextEvent = {
|
|
4537
4626
|
...event,
|
|
4538
4627
|
timestamp: Date.now()
|
|
4539
4628
|
};
|
|
4629
|
+
if (nextEvent.stage !== "error") {
|
|
4630
|
+
this.lastProgressLabel = nextEvent.label;
|
|
4631
|
+
}
|
|
4540
4632
|
this.trace(`progress:${nextEvent.stage}`, {
|
|
4541
4633
|
label: nextEvent.label,
|
|
4542
4634
|
progress: nextEvent.progress,
|
|
@@ -6453,10 +6545,15 @@ var SponsorRelayController = class {
|
|
|
6453
6545
|
attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
|
|
6454
6546
|
);
|
|
6455
6547
|
} catch (error) {
|
|
6456
|
-
|
|
6548
|
+
const message = deploymentFailureMessage(error, this.lastProgressLabel);
|
|
6549
|
+
this.trace("deploy:error", {
|
|
6550
|
+
message,
|
|
6551
|
+
stage: this.lastProgressLabel,
|
|
6552
|
+
error
|
|
6553
|
+
});
|
|
6457
6554
|
this.patch({
|
|
6458
6555
|
busy: { deploying: false },
|
|
6459
|
-
errorText:
|
|
6556
|
+
errorText: message,
|
|
6460
6557
|
statusText: "Deployment failed"
|
|
6461
6558
|
});
|
|
6462
6559
|
this.emitProgress({
|
|
@@ -6464,7 +6561,7 @@ var SponsorRelayController = class {
|
|
|
6464
6561
|
label: "Deployment failed",
|
|
6465
6562
|
progress: 100,
|
|
6466
6563
|
status: "error",
|
|
6467
|
-
error:
|
|
6564
|
+
error: message
|
|
6468
6565
|
});
|
|
6469
6566
|
}
|
|
6470
6567
|
}
|
|
@@ -6724,6 +6821,7 @@ function createSponsorRelayController(props = {}) {
|
|
|
6724
6821
|
return new SponsorRelayController(props);
|
|
6725
6822
|
}
|
|
6726
6823
|
export {
|
|
6824
|
+
DEFAULT_FAB_CORNER,
|
|
6727
6825
|
DEFAULT_INSTANCE_NAME,
|
|
6728
6826
|
DEFAULT_MANIFEST_URL,
|
|
6729
6827
|
DEFAULT_TIER_ID,
|
|
@@ -6734,6 +6832,8 @@ export {
|
|
|
6734
6832
|
DEFAULT_UCAN_STORE_SERVICE_ORIGIN,
|
|
6735
6833
|
DEFAULT_UCAN_STORE_USER_DELEGATION_EXPIRATION_SECONDS,
|
|
6736
6834
|
DEPLOYMENT_PENDING_WARNING_MS,
|
|
6835
|
+
FAB_DRAG_THRESHOLD,
|
|
6836
|
+
FAB_EDGE_MARGIN,
|
|
6737
6837
|
IDLE_DEPLOYMENT_PROGRESS,
|
|
6738
6838
|
INSTANCE_RUNTIME_PENDING_REFRESH_MS,
|
|
6739
6839
|
INSTANCE_RUNTIME_READY_REFRESH_MS,
|
|
@@ -6745,10 +6845,15 @@ export {
|
|
|
6745
6845
|
STALE_INSTANCE_ALLOCATION_COOLDOWN_MS,
|
|
6746
6846
|
SponsorRelayController,
|
|
6747
6847
|
UI_PACKAGE_VERSION,
|
|
6848
|
+
anchorPanelToLauncher,
|
|
6748
6849
|
buildSshCommand,
|
|
6850
|
+
clampToViewport,
|
|
6749
6851
|
connectWallet,
|
|
6750
6852
|
createDeploymentProgressEmitter,
|
|
6751
6853
|
createSponsorRelayController,
|
|
6854
|
+
deploymentFailureMessage,
|
|
6855
|
+
describeRequest,
|
|
6856
|
+
dragExceedsThreshold,
|
|
6752
6857
|
formatDateTime,
|
|
6753
6858
|
formatNumber,
|
|
6754
6859
|
formatTierSpecLabel,
|
|
@@ -6757,9 +6862,12 @@ export {
|
|
|
6757
6862
|
joinRequiredPortForwards,
|
|
6758
6863
|
parseInstanceAddressPayload,
|
|
6759
6864
|
personalSign,
|
|
6865
|
+
readStoredPosition,
|
|
6866
|
+
resolveCornerPlacement,
|
|
6760
6867
|
resolveManifestSource,
|
|
6761
6868
|
rootfsHealth,
|
|
6762
6869
|
shortHash,
|
|
6763
6870
|
toChecksumAddress,
|
|
6764
|
-
watchWallet
|
|
6871
|
+
watchWallet,
|
|
6872
|
+
writeStoredPosition
|
|
6765
6873
|
};
|
|
@@ -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,104 @@ 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
|
|
|
215
|
+
// Before `controller.init()`, not after it, and the order is the whole
|
|
216
|
+
// feature. Placement reads storage and the viewport — no network — while
|
|
217
|
+
// `init()` fetches the manifest and the account's instances. Behind that
|
|
218
|
+
// await, `placement` stays null for as long as Aleph takes to answer, and
|
|
219
|
+
// `handleDragStart` returns early on a null placement: the launcher simply
|
|
220
|
+
// cannot be dragged. Measured in simple-todo: still immovable 20 seconds
|
|
221
|
+
// after the app was ready, with pointerdown, eight pointermoves and
|
|
222
|
+
// pointerup all arriving at the button.
|
|
223
|
+
//
|
|
224
|
+
// The React build never had this — it resolves placement in its own effect.
|
|
225
|
+
initPlacement()
|
|
226
|
+
|
|
139
227
|
await controller.init()
|
|
140
|
-
|
|
228
|
+
window.addEventListener('resize', handleViewportChange)
|
|
229
|
+
window.addEventListener('orientationchange', handleViewportChange)
|
|
230
|
+
return () => {
|
|
231
|
+
window.removeEventListener('resize', handleViewportChange)
|
|
232
|
+
window.removeEventListener('orientationchange', handleViewportChange)
|
|
233
|
+
unsubscribe()
|
|
234
|
+
}
|
|
141
235
|
})
|
|
142
236
|
|
|
143
237
|
onDestroy(() => {
|
|
@@ -145,14 +239,24 @@ export let apiHosts = undefined
|
|
|
145
239
|
})
|
|
146
240
|
</script>
|
|
147
241
|
|
|
148
|
-
<LauncherButton
|
|
242
|
+
<LauncherButton
|
|
243
|
+
bind:element={launcherEl}
|
|
244
|
+
open={state.open}
|
|
245
|
+
onToggle={() => controller.toggleOpen()}
|
|
246
|
+
mode={launcherMode}
|
|
247
|
+
{placement}
|
|
248
|
+
{draggable}
|
|
249
|
+
onDragStart={handleDragStart}
|
|
250
|
+
onDragMove={handleDragMove}
|
|
251
|
+
onDragEnd={handleDragEnd}
|
|
252
|
+
/>
|
|
149
253
|
|
|
150
254
|
{#if state.open}
|
|
151
255
|
<div class="backdrop" on:click={() => controller.setOpen(false)}></div>
|
|
152
256
|
{/if}
|
|
153
257
|
|
|
154
258
|
{#if state.open}
|
|
155
|
-
<aside class="panel">
|
|
259
|
+
<aside class="panel" bind:this={panelEl} style={panelStyle}>
|
|
156
260
|
<div class="panel-head">
|
|
157
261
|
<div>
|
|
158
262
|
<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;
|