locadot 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +17 -1
- package/dist/cli/commands/hosts.js +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/protect.js +76 -0
- package/dist/cli/commands/tunnel.js +12 -1
- package/dist/cli/commands/ui.js +6 -3
- package/dist/constants/index.js +1 -0
- package/dist/dashboard/api.js +11 -1
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/page.js +20 -4285
- package/dist/dashboard/ui/core.js +803 -0
- package/dist/dashboard/ui/hosts.js +1522 -0
- package/dist/dashboard/ui/remote.js +958 -0
- package/dist/dashboard/ui/shell.js +509 -0
- package/dist/dashboard/ui/system.js +764 -0
- package/dist/dashboard/ui/theme.js +1710 -0
- package/dist/index.js +11 -0
- package/dist/lib/host-auth.js +123 -0
- package/dist/lib/hosts.js +53 -0
- package/dist/proxy/cloudflare-logins.js +197 -0
- package/dist/proxy/protect.js +215 -0
- package/dist/proxy/router.js +5 -0
- package/dist/proxy/tunnel.js +16 -11
- package/package.json +1 -1
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared client state and helpers (elements, fetch, toasts, UI components) and the polling loop.
|
|
4
|
+
*
|
|
5
|
+
* UI component API (all functions live in the single page script scope):
|
|
6
|
+
*
|
|
7
|
+
* openMenu(anchorEl, items, opts?) -> close()
|
|
8
|
+
* items: Array of "-" (separator) or { label, icon?, hint?, danger?, disabled?, onSelect?, info? }
|
|
9
|
+
* icon an SVG path "d" string (24x24 viewBox, stroked), e.g. ICONS.edit
|
|
10
|
+
* hint right-aligned secondary text; info: true renders a non-interactive label/hint row
|
|
11
|
+
* opts: { label? (aria-label), title? (heading shown in the phone sheet), align?: "end" | "start", onClose? }
|
|
12
|
+
* Anchored popover on >640px (arrows/Home/End/Enter/Esc, focus returns to the anchor), bottom sheet on <=640px.
|
|
13
|
+
* Calling it again with the same anchor toggles the menu closed. closeMenu(returnFocus) closes any open menu.
|
|
14
|
+
* makeMoreButton(items | () => items, { label?, small?, align?, title? }) -> a ready "..." button (.btn-more)
|
|
15
|
+
* svgIcon(d, className?) -> <svg> element; ICONS: common path strings (edit, trash, copy, external, globe,
|
|
16
|
+
* lock, unlock, shield, refresh, plus, check, close, stop, play, link, logs, settings, signOut, machine, key, more)
|
|
17
|
+
*
|
|
18
|
+
* initTabs(tablistEl, opts?) -> { select(name, { focus? }), current(), names() }
|
|
19
|
+
* Markup: <div class="tabs" role="tablist" data-tabs="<view>" aria-label="...">
|
|
20
|
+
* <button role="tab" data-tab="machines">Machines</button> ...
|
|
21
|
+
* </div>
|
|
22
|
+
* <div data-tab-panel="machines"> ... </div> (inside the same section.view, or opts.scope)
|
|
23
|
+
* When data-tabs (or opts.view) names a view, the group is routed: #/<view>/<tab> deep-links to the tab,
|
|
24
|
+
* clicking a tab rewrites the hash, and a missing/unknown tab selects the first one. opts.route = false
|
|
25
|
+
* opts out. opts.onChange(tab, prevTab) runs when the tab changes and when the view is entered.
|
|
26
|
+
* opts.initial picks the tab selected before routing runs. currentTab(view) reads a group's tab.
|
|
27
|
+
*
|
|
28
|
+
* animateIn(el, index?) plays the list-row enter animation once (index staggers by 25ms, capped).
|
|
29
|
+
* showToast(message, kind?: "info" | "success" | "error" | "warn", hint?) stacks max 4, above dialogs.
|
|
30
|
+
* confirmDialog / promptDialog (shell.ts) for confirmations; every <dialog> animates open/close
|
|
31
|
+
* (close() plays the exit animation, then closes ~170ms later) and is a bottom sheet on phones.
|
|
32
|
+
* data-tip="text" (+ data-tip-pos="top|right|bottom") on any element shows a tooltip on hover/keyboard focus.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.jsPollingLoop = exports.jsPolling = exports.jsPrelude = void 0;
|
|
36
|
+
exports.jsPrelude = `(function () {
|
|
37
|
+
"use strict";
|
|
38
|
+
var setTheme = (function () {
|
|
39
|
+
var btn = document.getElementById("theme-toggle");
|
|
40
|
+
var order = ["system", "light", "dark"];
|
|
41
|
+
var mode = document.documentElement.getAttribute("data-theme") || "system";
|
|
42
|
+
function apply(next) {
|
|
43
|
+
mode = next;
|
|
44
|
+
if (mode === "system") document.documentElement.removeAttribute("data-theme");
|
|
45
|
+
else document.documentElement.setAttribute("data-theme", mode);
|
|
46
|
+
try { if (mode === "system") localStorage.removeItem("locadot.theme"); else localStorage.setItem("locadot.theme", mode); } catch (e) {}
|
|
47
|
+
var label = "Theme: " + mode;
|
|
48
|
+
btn.setAttribute("data-mode", mode);
|
|
49
|
+
btn.title = label;
|
|
50
|
+
btn.setAttribute("aria-label", label + " (click to change)");
|
|
51
|
+
}
|
|
52
|
+
apply(mode);
|
|
53
|
+
btn.addEventListener("click", function () { apply(order[(order.indexOf(mode) + 1) % order.length]); });
|
|
54
|
+
return function (next) { apply(next); if (typeof syncThemeSeg === "function") syncThemeSeg(next); };
|
|
55
|
+
})();
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
var tokenMeta = document.querySelector('meta[name="locadot-token"]');
|
|
59
|
+
var TOKEN = tokenMeta ? tokenMeta.getAttribute("content") : "";
|
|
60
|
+
var uiAuthMeta = document.querySelector('meta[name="locadot-ui-auth"]');
|
|
61
|
+
var UI_AUTH = uiAuthMeta ? uiAuthMeta.getAttribute("content") === "on" : false;
|
|
62
|
+
|
|
63
|
+
var versionEl = document.getElementById("version");
|
|
64
|
+
var uptimeEl = document.getElementById("uptime");
|
|
65
|
+
var pidEl = document.getElementById("pid");
|
|
66
|
+
var liveDot = document.getElementById("live-dot");
|
|
67
|
+
var liveLabel = document.getElementById("live-label");
|
|
68
|
+
var hostsErrorEl = document.getElementById("hosts-error");
|
|
69
|
+
var updatedEl = document.getElementById("updated");
|
|
70
|
+
|
|
71
|
+
var rootValueEl = document.getElementById("tile-root-value");
|
|
72
|
+
var portsValueEl = document.getElementById("tile-ports-value");
|
|
73
|
+
var portsHintEl = document.getElementById("tile-ports-hint");
|
|
74
|
+
|
|
75
|
+
var caValueEl = document.getElementById("tile-ca-value");
|
|
76
|
+
var caToggle = document.getElementById("ca-toggle");
|
|
77
|
+
|
|
78
|
+
var startupValueEl = document.getElementById("tile-startup-value");
|
|
79
|
+
var startupMethodEl = document.getElementById("tile-startup-method");
|
|
80
|
+
var startupToggle = document.getElementById("startup-toggle");
|
|
81
|
+
|
|
82
|
+
var proxyPortsEl = document.getElementById("tile-proxy-ports");
|
|
83
|
+
var proxyBindEl = document.getElementById("tile-proxy-bind");
|
|
84
|
+
var proxyStateDirEl = document.getElementById("tile-proxy-statedir");
|
|
85
|
+
var proxyPlatformEl = document.getElementById("tile-proxy-platform");
|
|
86
|
+
var stopProxyBtn = document.getElementById("stop-proxy-btn");
|
|
87
|
+
var stoppedBanner = document.getElementById("stopped-banner");
|
|
88
|
+
|
|
89
|
+
var cfValueEl = document.getElementById("tile-cloudflared-value");
|
|
90
|
+
var cfSubEl = document.getElementById("tile-cloudflared-sub");
|
|
91
|
+
var cfInstallBtn = document.getElementById("cloudflared-install-btn");
|
|
92
|
+
|
|
93
|
+
var addForm = document.getElementById("add-form");
|
|
94
|
+
var addHostInput = document.getElementById("add-host");
|
|
95
|
+
var addTargetInput = document.getElementById("add-target");
|
|
96
|
+
var addInsecureInput = document.getElementById("add-insecure");
|
|
97
|
+
var addCorsInput = document.getElementById("add-cors");
|
|
98
|
+
var addSubmitBtn = document.getElementById("add-submit");
|
|
99
|
+
var addErrorEl = document.getElementById("add-error");
|
|
100
|
+
|
|
101
|
+
var emptyEl = document.getElementById("empty");
|
|
102
|
+
var tableWrap = document.getElementById("table-wrap");
|
|
103
|
+
var tbody = document.getElementById("tbody");
|
|
104
|
+
var hostsSkeletonEl = document.getElementById("hosts-skeleton");
|
|
105
|
+
var hostsCountEl = document.getElementById("hosts-count");
|
|
106
|
+
var sbHostsCountEl = document.getElementById("sb-hosts-count");
|
|
107
|
+
var sbRemotesCountEl = document.getElementById("sb-remotes-count");
|
|
108
|
+
var hostsFilterInput = document.getElementById("hosts-filter");
|
|
109
|
+
// As many skeleton rows as hosts last time, so the table doesn't jump when data lands.
|
|
110
|
+
try {
|
|
111
|
+
var skelBody = hostsSkeletonEl.querySelector("tbody");
|
|
112
|
+
var lastCount = Math.max(1, Math.min(8, parseInt(localStorage.getItem("locadot.hostCount") || "3", 10) || 3));
|
|
113
|
+
while (skelBody.children.length > lastCount) skelBody.removeChild(skelBody.lastElementChild);
|
|
114
|
+
while (skelBody.children.length < lastCount) skelBody.appendChild(skelBody.firstElementChild.cloneNode(true));
|
|
115
|
+
} catch (e) {}
|
|
116
|
+
var hostsBooted = false;
|
|
117
|
+
|
|
118
|
+
var loadErrorBanner = document.getElementById("load-error-banner");
|
|
119
|
+
var loadErrorText = document.getElementById("load-error-text");
|
|
120
|
+
var loadErrorRetry = document.getElementById("load-error-retry");
|
|
121
|
+
var isDown = false;
|
|
122
|
+
|
|
123
|
+
var logsPanel = document.getElementById("logs-panel");
|
|
124
|
+
var logsBox = document.getElementById("logs-box");
|
|
125
|
+
var logsRefreshBtn = document.getElementById("logs-refresh");
|
|
126
|
+
var logsClearBtn = document.getElementById("logs-clear");
|
|
127
|
+
|
|
128
|
+
var cliAddPre = document.getElementById("cli-add-pre");
|
|
129
|
+
var cliAddCopy = document.getElementById("cli-add-copy");
|
|
130
|
+
var cliCurlPre = document.getElementById("cli-curl-pre");
|
|
131
|
+
var cliCurlCopy = document.getElementById("cli-curl-copy");
|
|
132
|
+
var cliTunnelPre = document.getElementById("cli-tunnel-pre");
|
|
133
|
+
var cliTunnelCopy = document.getElementById("cli-tunnel-copy");
|
|
134
|
+
|
|
135
|
+
var toastContainer = document.getElementById("toast-container");
|
|
136
|
+
|
|
137
|
+
var hubDot = document.getElementById("hub-dot");
|
|
138
|
+
var hubStatusText = document.getElementById("hub-status-text");
|
|
139
|
+
var hubUrlRow = document.getElementById("hub-url-row");
|
|
140
|
+
var hubUrlEl = document.getElementById("hub-url");
|
|
141
|
+
var hubUrlCopyBtn = document.getElementById("hub-url-copy");
|
|
142
|
+
var hubLoginRow = document.getElementById("hub-login-row");
|
|
143
|
+
var hubLoginLink = document.getElementById("hub-login-link");
|
|
144
|
+
var hubQuickWarning = document.getElementById("hub-quick-warning");
|
|
145
|
+
var hubErrorRow = document.getElementById("hub-error-row");
|
|
146
|
+
var hubNamedForm = document.getElementById("hub-named-form");
|
|
147
|
+
var hubDomainInput = document.getElementById("hub-domain");
|
|
148
|
+
var hubTunnelNameInput = document.getElementById("hub-tunnel-name");
|
|
149
|
+
var hubNamedSubmitBtn = document.getElementById("hub-named-submit");
|
|
150
|
+
var hubQuickBtn = document.getElementById("hub-quick-btn");
|
|
151
|
+
var hubStopBtn = document.getElementById("hub-stop-btn");
|
|
152
|
+
var hubShareSection = document.getElementById("hub-share-section");
|
|
153
|
+
var inviteRoleSelect = document.getElementById("invite-role");
|
|
154
|
+
var inviteCreateBtn = document.getElementById("invite-create-btn");
|
|
155
|
+
var inviteHostsWrap = document.getElementById("invite-hosts-wrap");
|
|
156
|
+
var inviteResultEl = document.getElementById("invite-result");
|
|
157
|
+
var inviteStringEl = document.getElementById("invite-string");
|
|
158
|
+
var inviteCopyBtn = document.getElementById("invite-copy-btn");
|
|
159
|
+
var inviteCountdownEl = document.getElementById("invite-countdown");
|
|
160
|
+
var peersEmptyEl = document.getElementById("peers-empty");
|
|
161
|
+
var peersListEl = document.getElementById("peers-list");
|
|
162
|
+
var invitesEmptyEl = document.getElementById("invites-empty");
|
|
163
|
+
var invitesListEl = document.getElementById("invites-list");
|
|
164
|
+
|
|
165
|
+
var connectForm = document.getElementById("connect-form");
|
|
166
|
+
var connectStringInput = document.getElementById("connect-string");
|
|
167
|
+
var connectNameInput = document.getElementById("connect-name");
|
|
168
|
+
var connectDomainInput = document.getElementById("connect-domain");
|
|
169
|
+
var hubLocalhostRow = document.getElementById("hub-localhost-row");
|
|
170
|
+
var hubLocalhostSwitch = document.getElementById("hub-localhost");
|
|
171
|
+
var hubPanelRow = document.getElementById("hub-panel-row");
|
|
172
|
+
var hubPanelSwitch = document.getElementById("hub-panel");
|
|
173
|
+
var hubPanelSub = document.getElementById("hub-panel-sub");
|
|
174
|
+
var hubPanelUrl = "";
|
|
175
|
+
var connectSubmitBtn = document.getElementById("connect-submit");
|
|
176
|
+
var connectErrorEl = document.getElementById("connect-error");
|
|
177
|
+
var remotesEmptyEl = document.getElementById("remotes-empty");
|
|
178
|
+
var remotesListEl = document.getElementById("remotes-list");
|
|
179
|
+
|
|
180
|
+
var lastStatus = null;
|
|
181
|
+
var lastHosts = [];
|
|
182
|
+
var lastHub = null;
|
|
183
|
+
var lastRemotes = [];
|
|
184
|
+
var rowElements = {};
|
|
185
|
+
var editingHost = null;
|
|
186
|
+
var caBusy = false;
|
|
187
|
+
var startupBusy = false;
|
|
188
|
+
var cfInstallBusy = false;
|
|
189
|
+
var cloudflaredInstalled = false;
|
|
190
|
+
var cloudflareLoggedIn = false;
|
|
191
|
+
var cloudflareZones = [];
|
|
192
|
+
var proxyStopped = false;
|
|
193
|
+
var pollTimer = null;
|
|
194
|
+
var remotesPollTimer = null;
|
|
195
|
+
var inviteTimer = null;
|
|
196
|
+
var inviteExpiresAt = null;
|
|
197
|
+
|
|
198
|
+
function clear(el) {
|
|
199
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function fmtRelative(iso) {
|
|
203
|
+
if (!iso) return "never";
|
|
204
|
+
var diffMs = Date.now() - new Date(iso).getTime();
|
|
205
|
+
var s = Math.max(0, Math.round(diffMs / 1000));
|
|
206
|
+
if (s < 60) return s + "s ago";
|
|
207
|
+
var m = Math.round(s / 60);
|
|
208
|
+
if (m < 60) return m + "m ago";
|
|
209
|
+
var h = Math.round(m / 60);
|
|
210
|
+
if (h < 24) return h + "h ago";
|
|
211
|
+
var d = Math.round(h / 24);
|
|
212
|
+
return d + "d ago";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function fmtUptime(sec) {
|
|
216
|
+
sec = Math.max(0, Math.floor(sec));
|
|
217
|
+
var h = Math.floor(sec / 3600);
|
|
218
|
+
var m = Math.floor((sec % 3600) / 60);
|
|
219
|
+
var s = sec % 60;
|
|
220
|
+
return h + "h " + m + "m " + s + "s";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function copyText(text, btn) {
|
|
224
|
+
function done(ok) {
|
|
225
|
+
if (!btn) return;
|
|
226
|
+
var prev = btn.getAttribute("data-original") || btn.textContent;
|
|
227
|
+
btn.setAttribute("data-original", prev);
|
|
228
|
+
btn.textContent = ok ? "Copied" : "Failed";
|
|
229
|
+
setTimeout(function () { btn.textContent = prev; }, 1500);
|
|
230
|
+
}
|
|
231
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
232
|
+
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
var ta = document.createElement("textarea");
|
|
237
|
+
ta.className = "clip-helper";
|
|
238
|
+
ta.value = text;
|
|
239
|
+
document.body.appendChild(ta);
|
|
240
|
+
ta.select();
|
|
241
|
+
document.execCommand("copy");
|
|
242
|
+
document.body.removeChild(ta);
|
|
243
|
+
done(true);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
done(false);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function makeCopyButton(getText, label) {
|
|
250
|
+
var btn = document.createElement("button");
|
|
251
|
+
btn.type = "button";
|
|
252
|
+
btn.className = "copy-btn";
|
|
253
|
+
btn.textContent = label || "Copy";
|
|
254
|
+
btn.setAttribute("aria-label", "Copy to clipboard");
|
|
255
|
+
btn.addEventListener("click", function () { copyText(getText(), btn); });
|
|
256
|
+
return btn;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Toasts live in a manual popover so they stay visible above an open modal dialog.
|
|
260
|
+
if (typeof HTMLElement.prototype.showPopover === "function") toastContainer.setAttribute("popover", "manual");
|
|
261
|
+
function liftToasts() {
|
|
262
|
+
if (typeof toastContainer.showPopover !== "function" || !toastContainer.hasAttribute("popover")) return;
|
|
263
|
+
try {
|
|
264
|
+
if (toastContainer.matches(":popover-open")) toastContainer.hidePopover();
|
|
265
|
+
toastContainer.showPopover();
|
|
266
|
+
} catch (e) {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function showToast(message, kind, hint) {
|
|
270
|
+
var el = document.createElement("div");
|
|
271
|
+
el.className = "toast toast-" + (kind || "info");
|
|
272
|
+
var row = document.createElement("div");
|
|
273
|
+
row.textContent = message;
|
|
274
|
+
el.appendChild(row);
|
|
275
|
+
if (hint) {
|
|
276
|
+
var hintRow = document.createElement("div");
|
|
277
|
+
hintRow.className = "toast-hint";
|
|
278
|
+
var code = document.createElement("code");
|
|
279
|
+
code.className = "mono";
|
|
280
|
+
code.textContent = hint;
|
|
281
|
+
hintRow.appendChild(code);
|
|
282
|
+
hintRow.appendChild(makeCopyButton(function () { return hint; }));
|
|
283
|
+
el.appendChild(hintRow);
|
|
284
|
+
}
|
|
285
|
+
while (toastContainer.children.length >= 4) toastContainer.removeChild(toastContainer.firstElementChild);
|
|
286
|
+
toastContainer.appendChild(el);
|
|
287
|
+
liftToasts();
|
|
288
|
+
requestAnimationFrame(function () { requestAnimationFrame(function () { el.classList.add("show"); }); });
|
|
289
|
+
var timeout = hint ? 9000 : 4500;
|
|
290
|
+
setTimeout(function () {
|
|
291
|
+
el.classList.remove("show");
|
|
292
|
+
setTimeout(function () {
|
|
293
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
294
|
+
if (hasPopover && !toastContainer.children.length) { try { toastContainer.hidePopover(); } catch (e) {} }
|
|
295
|
+
}, 300);
|
|
296
|
+
}, timeout);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function apiError(err, context) {
|
|
300
|
+
var message = err && err.message ? err.message : String(err);
|
|
301
|
+
if (context) message = context + ": " + message;
|
|
302
|
+
showToast(message, "error", err && err.hint ? err.hint : undefined);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseJsonOrThrow(r) {
|
|
306
|
+
// The dashboard session ran out (or the password was set elsewhere): back to the sign-in page.
|
|
307
|
+
if (r.status === 401) location.reload();
|
|
308
|
+
return r.json().catch(function () { return {}; }).then(function (body) {
|
|
309
|
+
if (!r.ok) {
|
|
310
|
+
var err = new Error((body && body.error) || ("Request failed (" + r.status + ")"));
|
|
311
|
+
err.hint = body && body.hint;
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
return body;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function apiFetch(url, opts) {
|
|
319
|
+
opts = opts || {};
|
|
320
|
+
var method = opts.method || "GET";
|
|
321
|
+
var headers = {};
|
|
322
|
+
if (method !== "GET") {
|
|
323
|
+
headers["X-Locadot-Token"] = TOKEN;
|
|
324
|
+
headers["Content-Type"] = "application/json";
|
|
325
|
+
}
|
|
326
|
+
opts.headers = headers;
|
|
327
|
+
return fetch(url, opts).then(parseJsonOrThrow);
|
|
328
|
+
}
|
|
329
|
+
// ---------- shared UI components ----------
|
|
330
|
+
// See the API block at the top of this file (core.ts).
|
|
331
|
+
|
|
332
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
333
|
+
var ICONS = {
|
|
334
|
+
more: "M5 12h.01M12 12h.01M19 12h.01",
|
|
335
|
+
edit: "M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z",
|
|
336
|
+
trash: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6",
|
|
337
|
+
copy: "M9 9h11v11H9zM5 15H4V4h11v1",
|
|
338
|
+
external: "M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5",
|
|
339
|
+
globe: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18",
|
|
340
|
+
lock: "M6 11h12v10H6zM8 11V7a4 4 0 0 1 8 0v4",
|
|
341
|
+
unlock: "M6 11h12v10H6zM8 11V7a4 4 0 0 1 7.5-2",
|
|
342
|
+
shield: "M12 3l8 3v6c0 4.5-3.4 8.3-8 9-4.6-.7-8-4.5-8-9V6z",
|
|
343
|
+
refresh: "M21 12a9 9 0 0 1-15.5 6.2M3 12a9 9 0 0 1 15.5-6.2M21 4v5h-5M3 20v-5h5",
|
|
344
|
+
plus: "M12 5v14M5 12h14",
|
|
345
|
+
check: "M5 12l5 5L20 7",
|
|
346
|
+
close: "M6 6l12 12M18 6L6 18",
|
|
347
|
+
stop: "M6 6h12v12H6z",
|
|
348
|
+
play: "M7 4l13 8-13 8z",
|
|
349
|
+
link: "M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1",
|
|
350
|
+
logs: "M4 6h16M4 12h16M4 18h10",
|
|
351
|
+
settings: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15l1.2 2-2 3.4-2.3-.6a8 8 0 0 1-2 1.2L13.6 23h-3.2l-.7-2a8 8 0 0 1-2-1.2l-2.3.6-2-3.4 1.2-2a8 8 0 0 1 0-2.3l-1.2-2 2-3.4 2.3.6a8 8 0 0 1 2-1.2l.7-2h3.2l.7 2a8 8 0 0 1 2 1.2l2.3-.6 2 3.4-1.2 2a8 8 0 0 1 0 2.3z",
|
|
352
|
+
signOut: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9",
|
|
353
|
+
machine: "M3 4h18v12H3zM8 20h8M12 16v4",
|
|
354
|
+
key: "M15 7a4 4 0 1 1-3.9 5H3v3h3v3h3v-3h2.1A4 4 0 0 1 15 7z"
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
function svgIcon(d, cls) {
|
|
358
|
+
var svg = document.createElementNS(SVG_NS, "svg");
|
|
359
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
360
|
+
svg.setAttribute("aria-hidden", "true");
|
|
361
|
+
if (cls) svg.setAttribute("class", cls);
|
|
362
|
+
var path = document.createElementNS(SVG_NS, "path");
|
|
363
|
+
path.setAttribute("d", d);
|
|
364
|
+
svg.appendChild(path);
|
|
365
|
+
return svg;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
var reducedMotionMq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
369
|
+
var sheetMq = window.matchMedia("(max-width: 640px)");
|
|
370
|
+
function motionOk() { return !reducedMotionMq.matches; }
|
|
371
|
+
var hasPopover = typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function";
|
|
372
|
+
|
|
373
|
+
function animateIn(el, index) {
|
|
374
|
+
if (!el || !motionOk()) return el;
|
|
375
|
+
el.classList.remove("anim-in");
|
|
376
|
+
void el.offsetWidth;
|
|
377
|
+
if (index) el.style.animationDelay = Math.min(index, 8) * 25 + "ms";
|
|
378
|
+
el.classList.add("anim-in");
|
|
379
|
+
el.addEventListener("animationend", function done() {
|
|
380
|
+
el.classList.remove("anim-in");
|
|
381
|
+
el.style.animationDelay = "";
|
|
382
|
+
el.removeEventListener("animationend", done);
|
|
383
|
+
});
|
|
384
|
+
return el;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Every <dialog> gets an exit animation: close() plays it, then really closes.
|
|
388
|
+
// showModal() while a close is pending finishes the close first.
|
|
389
|
+
(function () {
|
|
390
|
+
if (typeof HTMLDialogElement === "undefined") return;
|
|
391
|
+
var proto = HTMLDialogElement.prototype;
|
|
392
|
+
var nativeClose = proto.close;
|
|
393
|
+
var nativeShowModal = proto.showModal;
|
|
394
|
+
function finish(d, value) {
|
|
395
|
+
if (d.__closeTimer) { clearTimeout(d.__closeTimer); d.__closeTimer = null; }
|
|
396
|
+
d.classList.remove("closing");
|
|
397
|
+
if (d.open) nativeClose.call(d, value);
|
|
398
|
+
}
|
|
399
|
+
proto.close = function (value) {
|
|
400
|
+
var d = this;
|
|
401
|
+
if (!d.open || !motionOk() || !d.isConnected) { finish(d, value); return; }
|
|
402
|
+
if (d.__closeTimer) return;
|
|
403
|
+
d.classList.add("closing");
|
|
404
|
+
d.__closeTimer = setTimeout(function () { d.__closeTimer = null; finish(d, value); }, 170);
|
|
405
|
+
};
|
|
406
|
+
proto.showModal = function () {
|
|
407
|
+
if (this.__closeTimer) finish(this);
|
|
408
|
+
if (this.open) return;
|
|
409
|
+
return nativeShowModal.apply(this, arguments);
|
|
410
|
+
};
|
|
411
|
+
})();
|
|
412
|
+
|
|
413
|
+
// ---------- menu ----------
|
|
414
|
+
var menuState = null;
|
|
415
|
+
|
|
416
|
+
function closeMenu(returnFocus) {
|
|
417
|
+
var st = menuState;
|
|
418
|
+
if (!st) return;
|
|
419
|
+
menuState = null;
|
|
420
|
+
document.removeEventListener("pointerdown", st.onPointer, true);
|
|
421
|
+
window.removeEventListener("resize", st.onWin);
|
|
422
|
+
window.removeEventListener("scroll", st.onScroll, true);
|
|
423
|
+
st.anchor.setAttribute("aria-expanded", "false");
|
|
424
|
+
var menu = st.menu;
|
|
425
|
+
function remove() {
|
|
426
|
+
if (hasPopover) { try { menu.hidePopover(); } catch (e) {} }
|
|
427
|
+
if (menu.parentNode) menu.parentNode.removeChild(menu);
|
|
428
|
+
}
|
|
429
|
+
if (motionOk()) { menu.classList.add("menu-closing"); setTimeout(remove, 180); }
|
|
430
|
+
else remove();
|
|
431
|
+
if (returnFocus && st.anchor.isConnected) st.anchor.focus({ preventScroll: true });
|
|
432
|
+
if (st.opts.onClose) st.opts.onClose();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function positionMenu(menu, anchor, align) {
|
|
436
|
+
var r = anchor.getBoundingClientRect();
|
|
437
|
+
var vw = document.documentElement.clientWidth;
|
|
438
|
+
var vh = window.innerHeight;
|
|
439
|
+
var mw = menu.offsetWidth;
|
|
440
|
+
var mh = menu.offsetHeight;
|
|
441
|
+
var start = align === "start";
|
|
442
|
+
var left = start ? r.left : r.right - mw;
|
|
443
|
+
if (left + mw > vw - 8) { left = vw - mw - 8; }
|
|
444
|
+
if (left < 8) { left = 8; }
|
|
445
|
+
var top = r.bottom + 6;
|
|
446
|
+
var up = false;
|
|
447
|
+
if (top + mh > vh - 8 && r.top - mh - 6 >= 8) { top = r.top - mh - 6; up = true; }
|
|
448
|
+
if (top + mh > vh - 8) top = Math.max(8, vh - mh - 8);
|
|
449
|
+
menu.style.left = Math.round(left) + "px";
|
|
450
|
+
menu.style.top = Math.round(top) + "px";
|
|
451
|
+
menu.classList.toggle("menu-up", up);
|
|
452
|
+
menu.classList.toggle("menu-start", start);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function openMenu(anchorEl, items, opts) {
|
|
456
|
+
opts = opts || {};
|
|
457
|
+
if (menuState) {
|
|
458
|
+
var same = menuState.anchor === anchorEl;
|
|
459
|
+
closeMenu(false);
|
|
460
|
+
if (same) return null;
|
|
461
|
+
}
|
|
462
|
+
hideTooltip();
|
|
463
|
+
var sheet = sheetMq.matches;
|
|
464
|
+
var menu = document.createElement("div");
|
|
465
|
+
menu.className = "menu" + (sheet ? " menu-sheet" : "");
|
|
466
|
+
menu.setAttribute("role", "menu");
|
|
467
|
+
menu.tabIndex = -1;
|
|
468
|
+
if (opts.label) menu.setAttribute("aria-label", opts.label);
|
|
469
|
+
if (sheet && opts.title) {
|
|
470
|
+
var title = document.createElement("div");
|
|
471
|
+
title.className = "menu-title";
|
|
472
|
+
title.textContent = opts.title;
|
|
473
|
+
menu.appendChild(title);
|
|
474
|
+
}
|
|
475
|
+
var buttons = [];
|
|
476
|
+
var withIcons = items.some(function (it) { return it && it !== "-" && it.icon; });
|
|
477
|
+
function addButton(it, extraClass) {
|
|
478
|
+
var b = document.createElement("button");
|
|
479
|
+
b.type = "button";
|
|
480
|
+
b.className = "menu-item" + (it.danger ? " danger" : "") + (extraClass ? " " + extraClass : "");
|
|
481
|
+
b.setAttribute("role", "menuitem");
|
|
482
|
+
b.tabIndex = -1;
|
|
483
|
+
if (it.disabled) { b.disabled = true; b.setAttribute("aria-disabled", "true"); }
|
|
484
|
+
if (it.icon) b.appendChild(svgIcon(it.icon, "menu-icon"));
|
|
485
|
+
else if (withIcons && !extraClass) { var ph = document.createElement("span"); ph.className = "menu-icon"; b.appendChild(ph); }
|
|
486
|
+
var lab = document.createElement("span");
|
|
487
|
+
lab.className = "menu-label";
|
|
488
|
+
lab.textContent = it.label;
|
|
489
|
+
b.appendChild(lab);
|
|
490
|
+
if (it.hint) { var h = document.createElement("span"); h.className = "menu-hint"; h.textContent = it.hint; b.appendChild(h); }
|
|
491
|
+
b.addEventListener("click", function () {
|
|
492
|
+
if (b.disabled) return;
|
|
493
|
+
closeMenu(true);
|
|
494
|
+
if (typeof it.onSelect === "function") it.onSelect();
|
|
495
|
+
});
|
|
496
|
+
buttons.push(b);
|
|
497
|
+
menu.appendChild(b);
|
|
498
|
+
}
|
|
499
|
+
items.forEach(function (it) {
|
|
500
|
+
if (!it) return;
|
|
501
|
+
if (it === "-") {
|
|
502
|
+
var sep = document.createElement("div");
|
|
503
|
+
sep.className = "menu-sep";
|
|
504
|
+
sep.setAttribute("role", "separator");
|
|
505
|
+
menu.appendChild(sep);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (it.info) {
|
|
509
|
+
var row = document.createElement("div");
|
|
510
|
+
row.className = "menu-info";
|
|
511
|
+
row.setAttribute("role", "presentation");
|
|
512
|
+
var l = document.createElement("span");
|
|
513
|
+
l.textContent = it.label;
|
|
514
|
+
row.appendChild(l);
|
|
515
|
+
if (it.hint) { var hv = document.createElement("span"); hv.className = "menu-hint"; hv.textContent = it.hint; row.appendChild(hv); }
|
|
516
|
+
menu.appendChild(row);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
addButton(it);
|
|
520
|
+
});
|
|
521
|
+
if (sheet) addButton({ label: "Cancel" }, "menu-cancel");
|
|
522
|
+
|
|
523
|
+
function enabled() { return buttons.filter(function (b) { return !b.disabled && b.offsetParent !== null; }); }
|
|
524
|
+
function move(delta) {
|
|
525
|
+
var list = enabled();
|
|
526
|
+
if (!list.length) return;
|
|
527
|
+
var i = list.indexOf(document.activeElement);
|
|
528
|
+
var next = i === -1 ? (delta > 0 ? 0 : list.length - 1) : (i + delta + list.length) % list.length;
|
|
529
|
+
list[next].focus();
|
|
530
|
+
}
|
|
531
|
+
menu.addEventListener("keydown", function (e) {
|
|
532
|
+
if (e.key === "ArrowDown") { e.preventDefault(); move(1); }
|
|
533
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); move(-1); }
|
|
534
|
+
else if (e.key === "Home") { e.preventDefault(); var a = enabled(); if (a[0]) a[0].focus(); }
|
|
535
|
+
else if (e.key === "End") { e.preventDefault(); var z = enabled(); if (z.length) z[z.length - 1].focus(); }
|
|
536
|
+
else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeMenu(true); }
|
|
537
|
+
else if (e.key === "Tab") { e.preventDefault(); closeMenu(true); }
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (hasPopover) menu.setAttribute("popover", "manual");
|
|
541
|
+
document.body.appendChild(menu);
|
|
542
|
+
if (hasPopover) { try { menu.showPopover(); } catch (e) {} }
|
|
543
|
+
if (!sheet) positionMenu(menu, anchorEl, opts.align);
|
|
544
|
+
anchorEl.setAttribute("aria-haspopup", "menu");
|
|
545
|
+
anchorEl.setAttribute("aria-expanded", "true");
|
|
546
|
+
|
|
547
|
+
var st = {
|
|
548
|
+
menu: menu,
|
|
549
|
+
anchor: anchorEl,
|
|
550
|
+
opts: opts,
|
|
551
|
+
onPointer: function (e) {
|
|
552
|
+
if (anchorEl.contains(e.target)) return;
|
|
553
|
+
if (menu.contains(e.target)) {
|
|
554
|
+
if (e.target !== menu) return;
|
|
555
|
+
var r = menu.getBoundingClientRect();
|
|
556
|
+
if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) return;
|
|
557
|
+
}
|
|
558
|
+
closeMenu(false);
|
|
559
|
+
},
|
|
560
|
+
onWin: function () { closeMenu(false); },
|
|
561
|
+
onScroll: function (e) { if (!sheet && !menu.contains(e.target)) closeMenu(false); }
|
|
562
|
+
};
|
|
563
|
+
menuState = st;
|
|
564
|
+
document.addEventListener("pointerdown", st.onPointer, true);
|
|
565
|
+
window.addEventListener("resize", st.onWin);
|
|
566
|
+
window.addEventListener("scroll", st.onScroll, true);
|
|
567
|
+
var first = enabled()[0];
|
|
568
|
+
(first || menu).focus({ preventScroll: true });
|
|
569
|
+
return function () { if (menuState === st) closeMenu(true); };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function makeMoreButton(items, opts) {
|
|
573
|
+
opts = opts || {};
|
|
574
|
+
var btn = document.createElement("button");
|
|
575
|
+
btn.type = "button";
|
|
576
|
+
btn.className = "btn btn-ghost btn-icon btn-more" + (opts.small ? " btn-sm" : "");
|
|
577
|
+
btn.setAttribute("aria-label", opts.label || "More actions");
|
|
578
|
+
btn.setAttribute("aria-haspopup", "menu");
|
|
579
|
+
btn.setAttribute("aria-expanded", "false");
|
|
580
|
+
var svg = document.createElementNS(SVG_NS, "svg");
|
|
581
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
582
|
+
svg.setAttribute("aria-hidden", "true");
|
|
583
|
+
[5, 12, 19].forEach(function (cx) {
|
|
584
|
+
var c = document.createElementNS(SVG_NS, "circle");
|
|
585
|
+
c.setAttribute("cx", String(cx));
|
|
586
|
+
c.setAttribute("cy", "12");
|
|
587
|
+
c.setAttribute("r", "1.7");
|
|
588
|
+
svg.appendChild(c);
|
|
589
|
+
});
|
|
590
|
+
btn.appendChild(svg);
|
|
591
|
+
btn.addEventListener("click", function (e) {
|
|
592
|
+
e.stopPropagation();
|
|
593
|
+
openMenu(btn, typeof items === "function" ? items() : items, { label: opts.label || "More actions", title: opts.title, align: opts.align || "end" });
|
|
594
|
+
});
|
|
595
|
+
return btn;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ---------- tabs ----------
|
|
599
|
+
var tabGroups = {};
|
|
600
|
+
|
|
601
|
+
function initTabs(root, opts) {
|
|
602
|
+
opts = opts || {};
|
|
603
|
+
var view = opts.view || root.getAttribute("data-tabs") || "";
|
|
604
|
+
var scope = opts.scope || root.closest("section.view") || document;
|
|
605
|
+
var routed = opts.route !== false && !!view && !!document.querySelector('section.view[data-view="' + view + '"]');
|
|
606
|
+
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"][data-tab]'));
|
|
607
|
+
var current = null;
|
|
608
|
+
var ink = document.createElement("span");
|
|
609
|
+
ink.className = "tabs-ink";
|
|
610
|
+
ink.setAttribute("aria-hidden", "true");
|
|
611
|
+
root.appendChild(ink);
|
|
612
|
+
|
|
613
|
+
function names() { return tabs.map(function (t) { return t.getAttribute("data-tab"); }); }
|
|
614
|
+
function tabEl(name) { for (var i = 0; i < tabs.length; i++) if (tabs[i].getAttribute("data-tab") === name) return tabs[i]; return null; }
|
|
615
|
+
function panelEl(name) { return scope.querySelector('[data-tab-panel="' + name + '"]'); }
|
|
616
|
+
|
|
617
|
+
function moveInk() {
|
|
618
|
+
var t = tabEl(current);
|
|
619
|
+
if (!t || !t.offsetWidth) return;
|
|
620
|
+
ink.style.transform = "translateX(" + t.offsetLeft + "px) scaleX(" + t.offsetWidth + ")";
|
|
621
|
+
if (!root.classList.contains("ink-ready")) requestAnimationFrame(function () { root.classList.add("ink-ready"); });
|
|
622
|
+
}
|
|
623
|
+
function reveal(t) {
|
|
624
|
+
var l = t.offsetLeft, r = l + t.offsetWidth;
|
|
625
|
+
if (l < root.scrollLeft + 16) root.scrollLeft = Math.max(0, l - 16);
|
|
626
|
+
else if (r > root.scrollLeft + root.clientWidth - 16) root.scrollLeft = r - root.clientWidth + 16;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function select(name, o) {
|
|
630
|
+
o = o || {};
|
|
631
|
+
var list = names();
|
|
632
|
+
if (list.indexOf(name) === -1) name = list[0];
|
|
633
|
+
var prev = current;
|
|
634
|
+
current = name;
|
|
635
|
+
tabs.forEach(function (t) {
|
|
636
|
+
var on = t.getAttribute("data-tab") === name;
|
|
637
|
+
t.setAttribute("aria-selected", String(on));
|
|
638
|
+
t.tabIndex = on ? 0 : -1;
|
|
639
|
+
var p = panelEl(t.getAttribute("data-tab"));
|
|
640
|
+
if (p) p.hidden = !on;
|
|
641
|
+
});
|
|
642
|
+
var t = tabEl(name);
|
|
643
|
+
if (t) {
|
|
644
|
+
if (o.focus) t.focus();
|
|
645
|
+
requestAnimationFrame(function () { moveInk(); reveal(t); });
|
|
646
|
+
}
|
|
647
|
+
if (o.user && routed) replaceHashTab(view, name);
|
|
648
|
+
if (prev !== null && prev !== name && !o.silent && typeof opts.onChange === "function") opts.onChange(name, prev);
|
|
649
|
+
return name;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
tabs.forEach(function (t) {
|
|
653
|
+
var name = t.getAttribute("data-tab");
|
|
654
|
+
if (!t.id) t.id = "tab-" + (view || "g") + "-" + name;
|
|
655
|
+
if (t.tagName === "BUTTON") t.type = "button";
|
|
656
|
+
var p = panelEl(name);
|
|
657
|
+
if (p) {
|
|
658
|
+
if (!p.id) p.id = t.id + "-panel";
|
|
659
|
+
p.setAttribute("role", "tabpanel");
|
|
660
|
+
p.setAttribute("aria-labelledby", t.id);
|
|
661
|
+
if (!p.hasAttribute("tabindex")) p.tabIndex = 0;
|
|
662
|
+
t.setAttribute("aria-controls", p.id);
|
|
663
|
+
}
|
|
664
|
+
t.addEventListener("click", function () { select(name, { user: true }); });
|
|
665
|
+
t.addEventListener("keydown", function (e) {
|
|
666
|
+
var list = names();
|
|
667
|
+
var i = list.indexOf(name);
|
|
668
|
+
var next = null;
|
|
669
|
+
if (e.key === "ArrowRight") next = list[(i + 1) % list.length];
|
|
670
|
+
else if (e.key === "ArrowLeft") next = list[(i - 1 + list.length) % list.length];
|
|
671
|
+
else if (e.key === "Home") next = list[0];
|
|
672
|
+
else if (e.key === "End") next = list[list.length - 1];
|
|
673
|
+
if (next !== null) { e.preventDefault(); select(next, { user: true, focus: true }); }
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
window.addEventListener("resize", moveInk);
|
|
677
|
+
if (typeof ResizeObserver === "function") new ResizeObserver(moveInk).observe(root);
|
|
678
|
+
|
|
679
|
+
var api = {
|
|
680
|
+
select: function (name, o) { return select(name, o); },
|
|
681
|
+
current: function () { return current; },
|
|
682
|
+
names: names,
|
|
683
|
+
opts: opts,
|
|
684
|
+
root: root
|
|
685
|
+
};
|
|
686
|
+
select(opts.initial || names()[0], { silent: true });
|
|
687
|
+
current = current || null;
|
|
688
|
+
if (routed) tabGroups[view] = api;
|
|
689
|
+
return api;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function replaceHashTab(view, tab) {
|
|
693
|
+
var h = "#/" + view + "/" + encodeURIComponent(tab);
|
|
694
|
+
if (location.hash !== h) { try { history.replaceState(null, "", h); } catch (e) {} }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function currentTab(view) { return tabGroups[view] ? tabGroups[view].current() : null; }
|
|
698
|
+
|
|
699
|
+
// Handle for console/e2e poking at the shared components (the page script itself is a closure).
|
|
700
|
+
|
|
701
|
+
// ---------- tooltips ([data-tip]) ----------
|
|
702
|
+
var tooltipEl = null;
|
|
703
|
+
var tooltipFor = null;
|
|
704
|
+
var tooltipGuards = [];
|
|
705
|
+
function hideTooltip() {
|
|
706
|
+
tooltipFor = null;
|
|
707
|
+
if (tooltipEl) tooltipEl.classList.remove("show");
|
|
708
|
+
}
|
|
709
|
+
function showTooltip(el) {
|
|
710
|
+
var text = el.getAttribute("data-tip");
|
|
711
|
+
if (!text || sheetMq.matches) return;
|
|
712
|
+
for (var i = 0; i < tooltipGuards.length; i++) if (!tooltipGuards[i](el)) return;
|
|
713
|
+
if (!tooltipEl) {
|
|
714
|
+
tooltipEl = document.createElement("div");
|
|
715
|
+
tooltipEl.className = "tooltip";
|
|
716
|
+
tooltipEl.setAttribute("role", "tooltip");
|
|
717
|
+
document.body.appendChild(tooltipEl);
|
|
718
|
+
}
|
|
719
|
+
tooltipFor = el;
|
|
720
|
+
tooltipEl.textContent = text;
|
|
721
|
+
var r = el.getBoundingClientRect();
|
|
722
|
+
var pos = el.getAttribute("data-tip-pos") || "top";
|
|
723
|
+
var tw = tooltipEl.offsetWidth, th = tooltipEl.offsetHeight;
|
|
724
|
+
var x, y;
|
|
725
|
+
if (pos === "right") { x = r.right + 10; y = r.top + (r.height - th) / 2; }
|
|
726
|
+
else if (pos === "bottom") { x = r.left + (r.width - tw) / 2; y = r.bottom + 8; }
|
|
727
|
+
else { x = r.left + (r.width - tw) / 2; y = r.top - th - 8; }
|
|
728
|
+
x = Math.max(6, Math.min(x, document.documentElement.clientWidth - tw - 6));
|
|
729
|
+
y = Math.max(6, y);
|
|
730
|
+
tooltipEl.style.transform = "translate(" + Math.round(x) + "px, " + Math.round(y) + "px)";
|
|
731
|
+
tooltipEl.classList.add("show");
|
|
732
|
+
}
|
|
733
|
+
function tipTarget(e) { return e.target && e.target.closest ? e.target.closest("[data-tip]") : null; }
|
|
734
|
+
document.addEventListener("mouseover", function (e) { var t = tipTarget(e); if (t && t !== tooltipFor) showTooltip(t); else if (!t && tooltipFor) hideTooltip(); });
|
|
735
|
+
document.addEventListener("focusin", function (e) { var t = tipTarget(e); if (t && e.target.matches(":focus-visible")) showTooltip(t); else hideTooltip(); });
|
|
736
|
+
document.addEventListener("focusout", hideTooltip);
|
|
737
|
+
document.addEventListener("pointerdown", hideTooltip, true);
|
|
738
|
+
window.addEventListener("scroll", hideTooltip, true);
|
|
739
|
+
|
|
740
|
+
`;
|
|
741
|
+
exports.jsPolling = ` // ---------- polling ----------
|
|
742
|
+
|
|
743
|
+
function refreshAll() {
|
|
744
|
+
if (proxyStopped) return Promise.resolve();
|
|
745
|
+
return Promise.all([
|
|
746
|
+
fetch("/api/status").then(parseJsonOrThrow),
|
|
747
|
+
fetch("/api/hosts").then(parseJsonOrThrow)
|
|
748
|
+
]).then(function (results) {
|
|
749
|
+
lastStatus = results[0];
|
|
750
|
+
renderHeader(lastStatus);
|
|
751
|
+
renderSystemTiles(lastStatus);
|
|
752
|
+
renderCli(lastStatus);
|
|
753
|
+
renderHosts(results[1]);
|
|
754
|
+
hideLoadError();
|
|
755
|
+
}).catch(function (err) {
|
|
756
|
+
document.body.classList.remove("boot");
|
|
757
|
+
hideHostsSkeleton();
|
|
758
|
+
markDown();
|
|
759
|
+
showLoadError(err);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
document.addEventListener("visibilitychange", function () {
|
|
764
|
+
if (!document.hidden && !proxyStopped) {
|
|
765
|
+
refreshAll().then(tickViews);
|
|
766
|
+
refreshHubAndRemotes();
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
remotesPollTimer = setInterval(function () {
|
|
772
|
+
if (!document.hidden && !proxyStopped) refreshHubAndRemotes();
|
|
773
|
+
}, 15000);
|
|
774
|
+
|
|
775
|
+
`;
|
|
776
|
+
exports.jsPollingLoop = ` // ---------- polling ----------
|
|
777
|
+
|
|
778
|
+
function tickViews() {
|
|
779
|
+
if (currentView === "overview") { renderOverview(); loadOverviewLogs(); }
|
|
780
|
+
else renderOverview();
|
|
781
|
+
if (currentView === "sharing" || currentView === "hosts") renderShareList();
|
|
782
|
+
if (currentView === "logs" && logsAuto.checked) loadLogs();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function schedulePoll() {
|
|
786
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
787
|
+
if (!prefs.refresh || proxyStopped) return;
|
|
788
|
+
pollTimer = setInterval(function () {
|
|
789
|
+
if (!document.hidden && !proxyStopped) refreshAll().then(tickViews);
|
|
790
|
+
}, prefs.refresh);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
setCollapsed(isCollapsed(), false);
|
|
794
|
+
var lastViewSaved = null;
|
|
795
|
+
try { lastViewSaved = localStorage.getItem("locadot.lastView"); } catch (e) {}
|
|
796
|
+
if (routeFromHash()) showRoute();
|
|
797
|
+
else showView((prefs.start === "last" ? lastViewSaved : prefs.start) || "overview");
|
|
798
|
+
schedulePoll();
|
|
799
|
+
|
|
800
|
+
refreshAll().then(tickViews);
|
|
801
|
+
refreshHubAndRemotes();
|
|
802
|
+
})();
|
|
803
|
+
`;
|