@cboxdk/cbox-ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -22
- package/dist/app-switcher.d.ts +19 -0
- package/dist/app-switcher.js +7 -0
- package/dist/app-switcher.js.map +1 -0
- package/dist/catalog-BroZWvws.d.ts +100 -0
- package/dist/chunk-AJCYF7SS.js +808 -0
- package/dist/chunk-AJCYF7SS.js.map +1 -0
- package/dist/index.d.ts +21 -69
- package/dist/index.js +22 -239
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
// src/app-switcher/catalog.ts
|
|
2
|
+
var DEFAULT_REFRESH_SECONDS = 60;
|
|
3
|
+
var KEPT_CATALOG_KEY = "cbox-app-catalog";
|
|
4
|
+
var CATALOG_EVENT = "cbox-app-catalog";
|
|
5
|
+
function parseAppCatalog(ok, body) {
|
|
6
|
+
const data = typeof body === "object" && body !== null ? body : {};
|
|
7
|
+
if (!ok) {
|
|
8
|
+
const signin = typeof data.signin_url === "string" && data.signin_url !== "" ? data.signin_url : null;
|
|
9
|
+
const authorize = typeof data.authorize_url === "string" && data.authorize_url !== "" ? data.authorize_url : null;
|
|
10
|
+
if (signin !== null || authorize !== null || data.state === "authorization_required") {
|
|
11
|
+
return { status: "signin_required", signinUrl: signin, authorizeUrl: authorize };
|
|
12
|
+
}
|
|
13
|
+
return { status: "unavailable" };
|
|
14
|
+
}
|
|
15
|
+
if (!Array.isArray(data.apps)) {
|
|
16
|
+
return { status: "unavailable" };
|
|
17
|
+
}
|
|
18
|
+
const apps = [];
|
|
19
|
+
for (const entry of data.apps) {
|
|
20
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
21
|
+
const app = entry;
|
|
22
|
+
if (typeof app.key !== "string" || typeof app.name !== "string" || typeof app.launch_url !== "string") continue;
|
|
23
|
+
if (!/^https?:\/\//i.test(app.launch_url)) continue;
|
|
24
|
+
apps.push({
|
|
25
|
+
key: app.key,
|
|
26
|
+
name: app.name,
|
|
27
|
+
launch_url: app.launch_url,
|
|
28
|
+
icon_url: typeof app.icon_url === "string" ? app.icon_url : null,
|
|
29
|
+
icon_dark_url: typeof app.icon_dark_url === "string" ? app.icon_dark_url : null,
|
|
30
|
+
description: typeof app.description === "string" ? app.description : null
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const refresh = typeof data.refresh_after === "number" && Number.isFinite(data.refresh_after) ? data.refresh_after : DEFAULT_REFRESH_SECONDS;
|
|
34
|
+
return { status: "ready", apps, refreshAfter: Math.max(1, Math.min(DEFAULT_REFRESH_SECONDS, refresh)) };
|
|
35
|
+
}
|
|
36
|
+
function authorizeHref(authorizeUrl, location) {
|
|
37
|
+
if (authorizeUrl === null) return null;
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(authorizeUrl, location.origin);
|
|
40
|
+
if (url.origin !== location.origin) return null;
|
|
41
|
+
url.searchParams.set("return_to", location.pathname + location.search);
|
|
42
|
+
return url.pathname + url.search;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function signInTarget(state, location) {
|
|
48
|
+
if (state.signinUrl !== null) {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(state.signinUrl, location.origin);
|
|
51
|
+
if (url.origin === location.origin) return url.href;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const legacy = authorizeHref(state.authorizeUrl, location);
|
|
56
|
+
return legacy === null ? null : new URL(legacy, location.origin).href;
|
|
57
|
+
}
|
|
58
|
+
function secureIconUrl(url) {
|
|
59
|
+
if (typeof url !== "string" || url === "") return null;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(url);
|
|
62
|
+
return parsed.protocol === "https:" ? parsed.href : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function displayName(name) {
|
|
68
|
+
return name.replace(/^Cbox\s*·\s*/i, "");
|
|
69
|
+
}
|
|
70
|
+
function initials(name) {
|
|
71
|
+
return displayName(name).slice(0, 2);
|
|
72
|
+
}
|
|
73
|
+
var EMPTY = [];
|
|
74
|
+
var cache = { raw: null, apps: EMPTY };
|
|
75
|
+
function storage() {
|
|
76
|
+
try {
|
|
77
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function readKnownAppCatalog() {
|
|
83
|
+
let raw = null;
|
|
84
|
+
try {
|
|
85
|
+
raw = storage()?.getItem(KEPT_CATALOG_KEY) ?? null;
|
|
86
|
+
} catch {
|
|
87
|
+
raw = null;
|
|
88
|
+
}
|
|
89
|
+
if (raw === cache.raw) return cache.apps;
|
|
90
|
+
let apps = EMPTY;
|
|
91
|
+
try {
|
|
92
|
+
const value = raw === null ? [] : JSON.parse(raw);
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
apps = value.flatMap((entry) => {
|
|
95
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
96
|
+
const app = entry;
|
|
97
|
+
if (typeof app.key !== "string" || typeof app.name !== "string") return [];
|
|
98
|
+
const known = { key: app.key, name: app.name, icon_url: secureIconUrl(app.icon_url), icon_dark_url: secureIconUrl(app.icon_dark_url) };
|
|
99
|
+
if (typeof app.launch_url === "string" && /^https?:\/\//i.test(app.launch_url)) known.launch_url = app.launch_url;
|
|
100
|
+
return [known];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
apps = EMPTY;
|
|
105
|
+
}
|
|
106
|
+
cache = { raw, apps };
|
|
107
|
+
return apps;
|
|
108
|
+
}
|
|
109
|
+
function rememberAppCatalog(apps) {
|
|
110
|
+
const kept = apps.map(({ key, name, launch_url, icon_url, icon_dark_url }) => {
|
|
111
|
+
const app = { key, name, icon_url: icon_url ?? null, icon_dark_url: icon_dark_url ?? null };
|
|
112
|
+
if (typeof launch_url === "string" && launch_url !== "") app.launch_url = launch_url;
|
|
113
|
+
return app;
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
storage()?.setItem(KEPT_CATALOG_KEY, JSON.stringify(kept));
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
if (typeof document !== "undefined") document.dispatchEvent(new CustomEvent(CATALOG_EVENT));
|
|
120
|
+
}
|
|
121
|
+
function subscribeKnownAppCatalog(change) {
|
|
122
|
+
if (typeof window === "undefined" || typeof document === "undefined") return () => void 0;
|
|
123
|
+
const storageChange = (event) => {
|
|
124
|
+
if (event.key === KEPT_CATALOG_KEY) change();
|
|
125
|
+
};
|
|
126
|
+
document.addEventListener(CATALOG_EVENT, change);
|
|
127
|
+
window.addEventListener("storage", storageChange);
|
|
128
|
+
return () => {
|
|
129
|
+
document.removeEventListener(CATALOG_EVENT, change);
|
|
130
|
+
window.removeEventListener("storage", storageChange);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/app-switcher/theme.ts
|
|
135
|
+
function themeMode(value) {
|
|
136
|
+
return value === "class" || value === "light" || value === "dark" ? value : "auto";
|
|
137
|
+
}
|
|
138
|
+
function isDark(mode = "auto") {
|
|
139
|
+
if (mode === "light" || mode === "dark") return mode === "dark";
|
|
140
|
+
if (typeof document === "undefined") return false;
|
|
141
|
+
const root = document.documentElement;
|
|
142
|
+
const marked = root.getAttribute("data-theme");
|
|
143
|
+
if (root.classList.contains("dark") || marked === "dark") return true;
|
|
144
|
+
if (root.classList.contains("light") || marked === "light") return false;
|
|
145
|
+
if (mode === "class") return false;
|
|
146
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
147
|
+
}
|
|
148
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
149
|
+
var stop = null;
|
|
150
|
+
function notify() {
|
|
151
|
+
for (const listener of [...listeners]) listener();
|
|
152
|
+
}
|
|
153
|
+
function subscribeTheme(change) {
|
|
154
|
+
listeners.add(change);
|
|
155
|
+
if (stop === null && typeof document !== "undefined") {
|
|
156
|
+
const observer = typeof MutationObserver === "function" ? new MutationObserver(notify) : null;
|
|
157
|
+
observer?.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme"] });
|
|
158
|
+
const media = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
159
|
+
media?.addEventListener?.("change", notify);
|
|
160
|
+
stop = () => {
|
|
161
|
+
observer?.disconnect();
|
|
162
|
+
media?.removeEventListener?.("change", notify);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return () => {
|
|
166
|
+
listeners.delete(change);
|
|
167
|
+
if (listeners.size === 0 && stop !== null) {
|
|
168
|
+
stop();
|
|
169
|
+
stop = null;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/app-switcher/styles.ts
|
|
175
|
+
var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', ui-sans-serif, system-ui, Arial, sans-serif";
|
|
176
|
+
var PALETTE = `
|
|
177
|
+
.root {
|
|
178
|
+
--fg: var(--foreground, #1f1d1a);
|
|
179
|
+
--muted-fg: var(--muted-foreground, #6b665e);
|
|
180
|
+
--surface: var(--popover, #ffffff);
|
|
181
|
+
--line: var(--border, #e6e3dd);
|
|
182
|
+
--hover: var(--secondary, #f2f0ec);
|
|
183
|
+
--tile: var(--muted, #f2f0ec);
|
|
184
|
+
--current: var(--accent-soft, #eef2ff);
|
|
185
|
+
--accent: var(--accent-strong, var(--primary, #2f5bd3));
|
|
186
|
+
--focus: var(--ring, #2f5bd3);
|
|
187
|
+
--shadow: var(--shadow-popover, 0 10px 30px -8px rgb(0 0 0 / 0.18), 0 2px 6px rgb(0 0 0 / 0.06));
|
|
188
|
+
}
|
|
189
|
+
.root.dark {
|
|
190
|
+
--fg: var(--foreground, #f3f2ef);
|
|
191
|
+
--muted-fg: var(--muted-foreground, #a29d94);
|
|
192
|
+
--surface: var(--popover, #24221f);
|
|
193
|
+
--line: var(--border, #3a3733);
|
|
194
|
+
--hover: var(--secondary, #312e2a);
|
|
195
|
+
--tile: var(--muted, #312e2a);
|
|
196
|
+
--current: var(--accent-soft, #26304a);
|
|
197
|
+
--accent: var(--accent-strong, var(--primary, #8aa6ff));
|
|
198
|
+
--focus: var(--ring, #8aa6ff);
|
|
199
|
+
--shadow: var(--shadow-popover, 0 10px 30px -8px rgb(0 0 0 / 0.6), 0 2px 6px rgb(0 0 0 / 0.3));
|
|
200
|
+
}`;
|
|
201
|
+
var ICON = `
|
|
202
|
+
.icon { position: relative; display: inline-flex; flex: none; align-items: center; justify-content: center; width: var(--size, 24px); height: var(--size, 24px); }
|
|
203
|
+
.icon img { display: block; width: 100%; height: 100%; max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; object-fit: contain; }
|
|
204
|
+
.icon img.on-dark { display: none; }
|
|
205
|
+
.root.dark .icon img.on-light { display: none; }
|
|
206
|
+
.root.dark .icon img.on-dark { display: block; }
|
|
207
|
+
.icon.initials { border-radius: 6px; background: var(--tile); color: var(--fg); font: 600 10px/1 ${FONT}; letter-spacing: 0; text-transform: none; }`;
|
|
208
|
+
var SWITCHER_CSS = `
|
|
209
|
+
:host { all: initial; display: inline-flex; flex: none; vertical-align: middle; }
|
|
210
|
+
:host([hidden]) { display: none; }
|
|
211
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
212
|
+
${PALETTE}
|
|
213
|
+
.root { display: inline-flex; color: var(--fg); font: 400 13px/1.4 ${FONT}; }
|
|
214
|
+
.trigger {
|
|
215
|
+
all: unset; box-sizing: border-box; display: inline-grid; place-items: center;
|
|
216
|
+
width: 32px; height: 32px; border-radius: 8px; color: var(--fg); background: transparent; cursor: pointer;
|
|
217
|
+
transition: background-color 150ms cubic-bezier(.4, 0, .2, 1);
|
|
218
|
+
}
|
|
219
|
+
.trigger svg { display: block; width: 18px; height: 18px; fill: currentColor; }
|
|
220
|
+
.trigger:hover, .trigger[aria-expanded='true'] { background: var(--hover); }
|
|
221
|
+
.trigger:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
222
|
+
.trigger:disabled { cursor: default; opacity: .6; background: transparent; }
|
|
223
|
+
.panel {
|
|
224
|
+
position: fixed; inset: auto; top: 0; left: 0; margin: 0; z-index: 2147483000;
|
|
225
|
+
width: min(292px, calc(100vw - 16px)); max-height: calc(100vh - 16px); overflow-y: auto;
|
|
226
|
+
padding: 12px; border: 1px solid var(--line); border-radius: 12px;
|
|
227
|
+
background: var(--surface); color: var(--fg); box-shadow: var(--shadow);
|
|
228
|
+
font: 400 13px/1.4 ${FONT}; text-align: left; outline: none;
|
|
229
|
+
}
|
|
230
|
+
.panel[hidden] { display: none; }
|
|
231
|
+
.panel h2 { margin: 0 4px 8px; padding: 0; color: var(--fg); font: 600 14px/1.4 ${FONT}; letter-spacing: 0; text-transform: none; }
|
|
232
|
+
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; margin: 0; padding: 0; }
|
|
233
|
+
.tile {
|
|
234
|
+
position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
|
|
235
|
+
min-width: 0; min-height: 60px; padding: 8px 4px; border-radius: 8px;
|
|
236
|
+
color: var(--fg); background: transparent; font: 500 13px/1.25 ${FONT}; text-align: center; text-decoration: none; overflow-wrap: anywhere;
|
|
237
|
+
}
|
|
238
|
+
.tile:hover { background: var(--hover); }
|
|
239
|
+
.tile[aria-current='true'] { background: var(--current); }
|
|
240
|
+
.tile:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
|
241
|
+
.tile .label { max-width: 100%; }
|
|
242
|
+
.check { position: absolute; top: 5px; right: 8px; width: 12px; height: 12px; color: var(--accent); }
|
|
243
|
+
.check svg { display: block; width: 12px; height: 12px; }
|
|
244
|
+
.message { margin: 0; padding: 8px 4px; color: var(--muted-fg); font: 400 13px/1.4 ${FONT}; }
|
|
245
|
+
${ICON}
|
|
246
|
+
@media (prefers-reduced-motion: reduce) { .trigger { transition: none; } }`;
|
|
247
|
+
var MARK_CSS = `
|
|
248
|
+
:host { all: initial; display: inline-flex; flex: none; align-items: center; justify-content: center; vertical-align: middle; }
|
|
249
|
+
:host([hidden]) { display: none; }
|
|
250
|
+
${PALETTE}
|
|
251
|
+
.root { display: inline-flex; --size: var(--cbox-service-mark-size, 24px); }
|
|
252
|
+
${ICON}`;
|
|
253
|
+
var sheets = /* @__PURE__ */ new Map();
|
|
254
|
+
function adopt(root, css) {
|
|
255
|
+
if (!sheets.has(css)) {
|
|
256
|
+
let sheet2 = null;
|
|
257
|
+
try {
|
|
258
|
+
if ("adoptedStyleSheets" in Document.prototype && typeof CSSStyleSheet === "function") {
|
|
259
|
+
sheet2 = new CSSStyleSheet();
|
|
260
|
+
sheet2.replaceSync(css);
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
sheet2 = null;
|
|
264
|
+
}
|
|
265
|
+
sheets.set(css, sheet2);
|
|
266
|
+
}
|
|
267
|
+
const sheet = sheets.get(css);
|
|
268
|
+
if (sheet) {
|
|
269
|
+
root.adoptedStyleSheets = [sheet];
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const style = document.createElement("style");
|
|
273
|
+
style.textContent = css;
|
|
274
|
+
root.append(style);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/app-switcher/elements.ts
|
|
278
|
+
var DEFAULT_LABELS = {
|
|
279
|
+
title: "Switch app",
|
|
280
|
+
loading: "Loading apps\u2026",
|
|
281
|
+
empty: "No apps available",
|
|
282
|
+
unavailable: "Apps could not be loaded."
|
|
283
|
+
};
|
|
284
|
+
var SIGNIN_EVENT = "cbox-app-switcher:signin";
|
|
285
|
+
var SIGNIN_GUARD = "cbox-app-switcher-signin";
|
|
286
|
+
var LOOP_WINDOW_MS = 3e4;
|
|
287
|
+
var REFRESH_INTERVAL_MS = 6e4;
|
|
288
|
+
var SVG = "http://www.w3.org/2000/svg";
|
|
289
|
+
function svg(viewBox, build, attributes = {}) {
|
|
290
|
+
const node = document.createElementNS(SVG, "svg");
|
|
291
|
+
node.setAttribute("viewBox", viewBox);
|
|
292
|
+
node.setAttribute("aria-hidden", "true");
|
|
293
|
+
node.setAttribute("focusable", "false");
|
|
294
|
+
for (const [name, value] of Object.entries(attributes)) node.setAttribute(name, value);
|
|
295
|
+
build(node);
|
|
296
|
+
return node;
|
|
297
|
+
}
|
|
298
|
+
function gridIcon() {
|
|
299
|
+
return svg("0 0 18 18", (node) => {
|
|
300
|
+
for (const y of [3, 9, 15]) {
|
|
301
|
+
for (const x of [3, 9, 15]) {
|
|
302
|
+
const dot = document.createElementNS(SVG, "circle");
|
|
303
|
+
dot.setAttribute("cx", String(x));
|
|
304
|
+
dot.setAttribute("cy", String(y));
|
|
305
|
+
dot.setAttribute("r", "1.65");
|
|
306
|
+
node.append(dot);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function checkIcon() {
|
|
312
|
+
return svg("0 0 24 24", (node) => {
|
|
313
|
+
const path = document.createElementNS(SVG, "path");
|
|
314
|
+
path.setAttribute("d", "m5 12 4 4L19 6");
|
|
315
|
+
node.append(path);
|
|
316
|
+
}, { fill: "none", stroke: "currentColor", "stroke-width": "1.8" });
|
|
317
|
+
}
|
|
318
|
+
function appIcon(app, missing) {
|
|
319
|
+
const box = document.createElement("span");
|
|
320
|
+
box.className = "icon";
|
|
321
|
+
box.setAttribute("aria-hidden", "true");
|
|
322
|
+
const fallback = () => {
|
|
323
|
+
box.replaceChildren();
|
|
324
|
+
if (missing === "initials") {
|
|
325
|
+
box.classList.add("initials");
|
|
326
|
+
box.textContent = initials(app.name);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const light = secureIconUrl(app.icon_url);
|
|
330
|
+
const dark = secureIconUrl(app.icon_dark_url);
|
|
331
|
+
if (light === null) {
|
|
332
|
+
fallback();
|
|
333
|
+
return box;
|
|
334
|
+
}
|
|
335
|
+
for (const [url, variant] of [[light, dark === null ? "single" : "on-light"], [dark, "on-dark"]]) {
|
|
336
|
+
if (url === null) continue;
|
|
337
|
+
const image = document.createElement("img");
|
|
338
|
+
image.alt = "";
|
|
339
|
+
image.decoding = "async";
|
|
340
|
+
image.className = variant;
|
|
341
|
+
image.addEventListener("error", fallback, { once: true });
|
|
342
|
+
image.src = url;
|
|
343
|
+
box.append(image);
|
|
344
|
+
}
|
|
345
|
+
return box;
|
|
346
|
+
}
|
|
347
|
+
function readLabels(value) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = JSON.parse(value ?? "{}");
|
|
350
|
+
if (typeof parsed !== "object" || parsed === null) return DEFAULT_LABELS;
|
|
351
|
+
const labels = { ...DEFAULT_LABELS };
|
|
352
|
+
for (const key of Object.keys(DEFAULT_LABELS)) {
|
|
353
|
+
const text = parsed[key];
|
|
354
|
+
if (typeof text === "string" && text !== "") labels[key] = text;
|
|
355
|
+
}
|
|
356
|
+
return labels;
|
|
357
|
+
} catch {
|
|
358
|
+
return DEFAULT_LABELS;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function recentlySentTo(url) {
|
|
362
|
+
try {
|
|
363
|
+
const kept = JSON.parse(window.sessionStorage.getItem(SIGNIN_GUARD) ?? "null");
|
|
364
|
+
if (typeof kept !== "object" || kept === null) return false;
|
|
365
|
+
const { url: sent, at } = kept;
|
|
366
|
+
return sent === url && typeof at === "number" && Date.now() - at < LOOP_WINDOW_MS;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function markSentTo(url) {
|
|
372
|
+
try {
|
|
373
|
+
if (url === null) window.sessionStorage.removeItem(SIGNIN_GUARD);
|
|
374
|
+
else window.sessionStorage.setItem(SIGNIN_GUARD, JSON.stringify({ url, at: Date.now() }));
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function navigateTop(url) {
|
|
379
|
+
try {
|
|
380
|
+
if (window.top !== null && window.top !== window) {
|
|
381
|
+
window.top.location.href = url;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
window.location.href = url;
|
|
387
|
+
}
|
|
388
|
+
var uid = 0;
|
|
389
|
+
function defineSwitcher() {
|
|
390
|
+
return class CboxAppSwitcherElement extends HTMLElement {
|
|
391
|
+
static observedAttributes = ["endpoint", "current", "labels", "theme"];
|
|
392
|
+
frame;
|
|
393
|
+
button;
|
|
394
|
+
panel;
|
|
395
|
+
heading;
|
|
396
|
+
body;
|
|
397
|
+
native;
|
|
398
|
+
copy = DEFAULT_LABELS;
|
|
399
|
+
view = { kind: "loading" };
|
|
400
|
+
flight = null;
|
|
401
|
+
expires = 0;
|
|
402
|
+
failures = 0;
|
|
403
|
+
retry;
|
|
404
|
+
ticker;
|
|
405
|
+
signin = null;
|
|
406
|
+
mounted = false;
|
|
407
|
+
restoreFocus = false;
|
|
408
|
+
stopTheme = null;
|
|
409
|
+
syncTheme = () => {
|
|
410
|
+
this.frame.classList.toggle("dark", isDark(themeMode(this.getAttribute("theme"))));
|
|
411
|
+
};
|
|
412
|
+
refresh = () => {
|
|
413
|
+
if (document.visibilityState !== "hidden" && Date.now() >= this.expires) void this.load();
|
|
414
|
+
};
|
|
415
|
+
place = () => {
|
|
416
|
+
if (this.isOpen()) this.position();
|
|
417
|
+
};
|
|
418
|
+
outside = (event) => {
|
|
419
|
+
if (!event.composedPath().includes(this)) this.close();
|
|
420
|
+
};
|
|
421
|
+
constructor() {
|
|
422
|
+
super();
|
|
423
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
424
|
+
adopt(shadow, SWITCHER_CSS);
|
|
425
|
+
const id = `cbox-app-switcher-${++uid}`;
|
|
426
|
+
this.frame = document.createElement("div");
|
|
427
|
+
this.frame.className = "root";
|
|
428
|
+
this.button = document.createElement("button");
|
|
429
|
+
this.button.type = "button";
|
|
430
|
+
this.button.className = "trigger";
|
|
431
|
+
this.button.setAttribute("aria-haspopup", "dialog");
|
|
432
|
+
this.button.setAttribute("aria-expanded", "false");
|
|
433
|
+
this.button.setAttribute("aria-controls", `${id}-panel`);
|
|
434
|
+
this.button.append(gridIcon());
|
|
435
|
+
this.panel = document.createElement("div");
|
|
436
|
+
this.panel.id = `${id}-panel`;
|
|
437
|
+
this.panel.className = "panel";
|
|
438
|
+
this.panel.tabIndex = -1;
|
|
439
|
+
this.panel.setAttribute("role", "dialog");
|
|
440
|
+
this.panel.setAttribute("aria-labelledby", `${id}-title`);
|
|
441
|
+
this.native = typeof this.panel.togglePopover === "function";
|
|
442
|
+
if (this.native) {
|
|
443
|
+
this.panel.popover = "auto";
|
|
444
|
+
const invoker = this.button;
|
|
445
|
+
if ("popoverTargetElement" in invoker) invoker.popoverTargetElement = this.panel;
|
|
446
|
+
else invoker.setAttribute("popovertarget", this.panel.id);
|
|
447
|
+
this.panel.addEventListener("toggle", (event) => {
|
|
448
|
+
this.toggled(event.newState === "open");
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
this.panel.hidden = true;
|
|
452
|
+
this.button.addEventListener("click", () => {
|
|
453
|
+
if (this.isOpen()) this.close();
|
|
454
|
+
else this.open();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
this.heading = document.createElement("h2");
|
|
458
|
+
this.heading.id = `${id}-title`;
|
|
459
|
+
this.body = document.createElement("div");
|
|
460
|
+
this.body.setAttribute("aria-live", "polite");
|
|
461
|
+
this.panel.append(this.heading, this.body);
|
|
462
|
+
this.panel.addEventListener("keydown", (event) => {
|
|
463
|
+
this.key(event);
|
|
464
|
+
});
|
|
465
|
+
this.frame.append(this.button, this.panel);
|
|
466
|
+
shadow.append(this.frame);
|
|
467
|
+
}
|
|
468
|
+
connectedCallback() {
|
|
469
|
+
if (this.mounted) return;
|
|
470
|
+
this.mounted = true;
|
|
471
|
+
this.applyLabels();
|
|
472
|
+
this.applyEndpoint();
|
|
473
|
+
this.syncTheme();
|
|
474
|
+
this.stopTheme = subscribeTheme(this.syncTheme);
|
|
475
|
+
this.paintKept();
|
|
476
|
+
this.ticker = setInterval(this.refresh, REFRESH_INTERVAL_MS);
|
|
477
|
+
window.addEventListener("focus", this.refresh);
|
|
478
|
+
document.addEventListener("visibilitychange", this.refresh);
|
|
479
|
+
window.addEventListener("resize", this.place);
|
|
480
|
+
window.addEventListener("scroll", this.place, true);
|
|
481
|
+
this.refresh();
|
|
482
|
+
}
|
|
483
|
+
disconnectedCallback() {
|
|
484
|
+
this.mounted = false;
|
|
485
|
+
this.flight?.abort();
|
|
486
|
+
this.flight = null;
|
|
487
|
+
clearInterval(this.ticker);
|
|
488
|
+
clearTimeout(this.retry);
|
|
489
|
+
this.retry = void 0;
|
|
490
|
+
this.stopTheme?.();
|
|
491
|
+
this.stopTheme = null;
|
|
492
|
+
window.removeEventListener("focus", this.refresh);
|
|
493
|
+
document.removeEventListener("visibilitychange", this.refresh);
|
|
494
|
+
window.removeEventListener("resize", this.place);
|
|
495
|
+
window.removeEventListener("scroll", this.place, true);
|
|
496
|
+
document.removeEventListener("pointerdown", this.outside, true);
|
|
497
|
+
}
|
|
498
|
+
attributeChangedCallback(name, before, after) {
|
|
499
|
+
if (!this.mounted || before === after) return;
|
|
500
|
+
if (name === "labels") {
|
|
501
|
+
this.applyLabels();
|
|
502
|
+
this.render();
|
|
503
|
+
}
|
|
504
|
+
if (name === "current") this.render();
|
|
505
|
+
if (name === "theme") this.syncTheme();
|
|
506
|
+
if (name === "endpoint") {
|
|
507
|
+
this.flight?.abort();
|
|
508
|
+
this.flight = null;
|
|
509
|
+
clearTimeout(this.retry);
|
|
510
|
+
this.retry = void 0;
|
|
511
|
+
this.expires = 0;
|
|
512
|
+
this.failures = 0;
|
|
513
|
+
this.signin = null;
|
|
514
|
+
this.view = { kind: "loading" };
|
|
515
|
+
this.applyEndpoint();
|
|
516
|
+
this.paintKept();
|
|
517
|
+
this.refresh();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
applyLabels() {
|
|
521
|
+
this.copy = readLabels(this.getAttribute("labels"));
|
|
522
|
+
this.button.title = this.copy.title;
|
|
523
|
+
this.button.setAttribute("aria-label", this.copy.title);
|
|
524
|
+
this.heading.textContent = this.copy.title;
|
|
525
|
+
}
|
|
526
|
+
applyEndpoint() {
|
|
527
|
+
this.button.disabled = !this.getAttribute("endpoint");
|
|
528
|
+
if (this.button.disabled) this.close();
|
|
529
|
+
}
|
|
530
|
+
/** The last catalog this browser kept paints the grid at once; the live answer replaces it. */
|
|
531
|
+
paintKept() {
|
|
532
|
+
if (this.view.kind === "apps" && this.view.live) return;
|
|
533
|
+
const kept = readKnownAppCatalog().filter((app) => typeof app.launch_url === "string");
|
|
534
|
+
this.view = kept.length > 0 ? { kind: "apps", apps: kept, live: false, failed: false } : { kind: "loading" };
|
|
535
|
+
this.render();
|
|
536
|
+
}
|
|
537
|
+
isOpen() {
|
|
538
|
+
if (!this.native) return !this.panel.hidden;
|
|
539
|
+
try {
|
|
540
|
+
return this.panel.matches(":popover-open");
|
|
541
|
+
} catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
open() {
|
|
546
|
+
if (this.button.disabled || this.isOpen()) return;
|
|
547
|
+
if (this.native) {
|
|
548
|
+
this.panel.showPopover();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
this.panel.hidden = false;
|
|
552
|
+
document.addEventListener("pointerdown", this.outside, true);
|
|
553
|
+
this.toggled(true);
|
|
554
|
+
}
|
|
555
|
+
close() {
|
|
556
|
+
if (!this.isOpen()) return;
|
|
557
|
+
if (this.native) {
|
|
558
|
+
this.panel.hidePopover();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.panel.hidden = true;
|
|
562
|
+
document.removeEventListener("pointerdown", this.outside, true);
|
|
563
|
+
this.toggled(false);
|
|
564
|
+
}
|
|
565
|
+
toggled(open) {
|
|
566
|
+
this.button.setAttribute("aria-expanded", String(open));
|
|
567
|
+
if (!open) {
|
|
568
|
+
if (this.restoreFocus && this.isConnected) this.button.focus();
|
|
569
|
+
this.restoreFocus = false;
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.position();
|
|
573
|
+
if (this.signin !== null) {
|
|
574
|
+
this.goSignIn(this.signin);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this.focusFirst();
|
|
578
|
+
this.refresh();
|
|
579
|
+
}
|
|
580
|
+
position() {
|
|
581
|
+
const box = this.button.getBoundingClientRect();
|
|
582
|
+
const width = Math.min(292, window.innerWidth - 16);
|
|
583
|
+
this.panel.style.width = `${width}px`;
|
|
584
|
+
this.panel.style.left = `${Math.max(8, Math.min(box.right - width, window.innerWidth - width - 8))}px`;
|
|
585
|
+
this.panel.style.top = `${box.bottom + 6}px`;
|
|
586
|
+
this.panel.style.maxHeight = `${Math.max(60, window.innerHeight - box.bottom - 14)}px`;
|
|
587
|
+
}
|
|
588
|
+
tiles() {
|
|
589
|
+
return [...this.body.querySelectorAll("a[href]")];
|
|
590
|
+
}
|
|
591
|
+
focusFirst() {
|
|
592
|
+
(this.tiles()[0] ?? this.panel).focus();
|
|
593
|
+
}
|
|
594
|
+
key(event) {
|
|
595
|
+
if (event.key === "Escape") {
|
|
596
|
+
this.restoreFocus = true;
|
|
597
|
+
if (!this.native) {
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
this.close();
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const tiles = this.tiles();
|
|
604
|
+
const active = this.shadowRoot?.activeElement ?? null;
|
|
605
|
+
if (event.key === "Tab") {
|
|
606
|
+
const first = tiles[0];
|
|
607
|
+
const last = tiles.at(-1);
|
|
608
|
+
if (first === void 0 || last === void 0) {
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (event.shiftKey && (active === first || active === this.panel)) {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
last.focus();
|
|
615
|
+
} else if (!event.shiftKey && (active === last || active === this.panel)) {
|
|
616
|
+
event.preventDefault();
|
|
617
|
+
first.focus();
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const at = tiles.indexOf(active);
|
|
622
|
+
const step = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: 3, ArrowUp: -3 };
|
|
623
|
+
let next = null;
|
|
624
|
+
if (event.key === "Home") next = 0;
|
|
625
|
+
else if (event.key === "End") next = tiles.length - 1;
|
|
626
|
+
else if (event.key in step) next = at === -1 ? 0 : Math.max(0, Math.min(tiles.length - 1, at + (step[event.key] ?? 0)));
|
|
627
|
+
if (next === null || tiles.length === 0) return;
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
tiles[next]?.focus();
|
|
630
|
+
}
|
|
631
|
+
render() {
|
|
632
|
+
const view = this.view;
|
|
633
|
+
if (view.kind !== "apps") {
|
|
634
|
+
const text = view.kind === "empty" ? this.copy.empty : view.kind === "unavailable" ? this.copy.unavailable : this.copy.loading;
|
|
635
|
+
const message = document.createElement("p");
|
|
636
|
+
message.className = "message";
|
|
637
|
+
message.textContent = text;
|
|
638
|
+
if (view.kind === "unavailable") message.setAttribute("role", "status");
|
|
639
|
+
this.body.replaceChildren(message);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const hadFocus = this.shadowRoot?.activeElement instanceof HTMLAnchorElement;
|
|
643
|
+
const nav = document.createElement("nav");
|
|
644
|
+
nav.className = "grid";
|
|
645
|
+
nav.setAttribute("aria-label", this.copy.title);
|
|
646
|
+
const current = this.getAttribute("current");
|
|
647
|
+
for (const app of view.apps) {
|
|
648
|
+
if (typeof app.launch_url !== "string") continue;
|
|
649
|
+
const tile = document.createElement("a");
|
|
650
|
+
tile.className = "tile";
|
|
651
|
+
tile.href = app.launch_url;
|
|
652
|
+
const description = "description" in app && typeof app.description === "string" ? app.description : null;
|
|
653
|
+
tile.title = description ?? displayName(app.name);
|
|
654
|
+
const label = document.createElement("span");
|
|
655
|
+
label.className = "label";
|
|
656
|
+
label.textContent = displayName(app.name);
|
|
657
|
+
tile.append(appIcon(app, "initials"), label);
|
|
658
|
+
if (app.key === current) {
|
|
659
|
+
tile.setAttribute("aria-current", "true");
|
|
660
|
+
const check = document.createElement("span");
|
|
661
|
+
check.className = "check";
|
|
662
|
+
check.append(checkIcon());
|
|
663
|
+
tile.append(check);
|
|
664
|
+
}
|
|
665
|
+
tile.addEventListener("click", () => {
|
|
666
|
+
this.close();
|
|
667
|
+
});
|
|
668
|
+
nav.append(tile);
|
|
669
|
+
}
|
|
670
|
+
const parts = [];
|
|
671
|
+
if (view.failed) {
|
|
672
|
+
const message = document.createElement("p");
|
|
673
|
+
message.className = "message";
|
|
674
|
+
message.setAttribute("role", "status");
|
|
675
|
+
message.textContent = this.copy.unavailable;
|
|
676
|
+
parts.push(message);
|
|
677
|
+
}
|
|
678
|
+
parts.push(nav);
|
|
679
|
+
this.body.replaceChildren(...parts);
|
|
680
|
+
if (hadFocus || this.isOpen() && this.shadowRoot?.activeElement === this.panel) this.focusFirst();
|
|
681
|
+
}
|
|
682
|
+
async load() {
|
|
683
|
+
const endpoint = this.getAttribute("endpoint");
|
|
684
|
+
if (!endpoint || this.flight !== null) return;
|
|
685
|
+
let source;
|
|
686
|
+
try {
|
|
687
|
+
source = new URL(endpoint, window.location.href);
|
|
688
|
+
} catch {
|
|
689
|
+
this.fail(false);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (source.origin !== window.location.origin) {
|
|
693
|
+
this.fail(false);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const request = new AbortController();
|
|
697
|
+
this.flight = request;
|
|
698
|
+
try {
|
|
699
|
+
const response = await fetch(source.href, {
|
|
700
|
+
credentials: "same-origin",
|
|
701
|
+
cache: "no-store",
|
|
702
|
+
redirect: "error",
|
|
703
|
+
headers: { Accept: "application/json", "X-Requested-With": "XMLHttpRequest" },
|
|
704
|
+
signal: request.signal
|
|
705
|
+
});
|
|
706
|
+
const body = await response.json().catch(() => null);
|
|
707
|
+
if (request.signal.aborted || !this.isConnected) return;
|
|
708
|
+
const state = parseAppCatalog(response.ok, body);
|
|
709
|
+
if (state.status === "ready") {
|
|
710
|
+
this.failures = 0;
|
|
711
|
+
clearTimeout(this.retry);
|
|
712
|
+
this.retry = void 0;
|
|
713
|
+
this.signin = null;
|
|
714
|
+
markSentTo(null);
|
|
715
|
+
this.expires = Date.now() + state.refreshAfter * 1e3;
|
|
716
|
+
this.view = state.apps.length > 0 ? { kind: "apps", apps: state.apps, live: true, failed: false } : { kind: "empty" };
|
|
717
|
+
rememberAppCatalog(state.apps);
|
|
718
|
+
this.render();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const target = state.status === "signin_required" ? signInTarget(state, window.location) : null;
|
|
722
|
+
if (target === null) {
|
|
723
|
+
this.fail(true);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.signin = target;
|
|
727
|
+
this.expires = 0;
|
|
728
|
+
if (this.isOpen()) this.goSignIn(target);
|
|
729
|
+
} catch {
|
|
730
|
+
if (!request.signal.aborted) this.fail(true);
|
|
731
|
+
} finally {
|
|
732
|
+
if (this.flight === request) this.flight = null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/** Quietly say the apps could not be loaded, keep any grid, and try again by itself. */
|
|
736
|
+
fail(again) {
|
|
737
|
+
this.expires = 0;
|
|
738
|
+
this.view = this.view.kind === "apps" ? { ...this.view, failed: true } : { kind: "unavailable" };
|
|
739
|
+
this.render();
|
|
740
|
+
if (!again) return;
|
|
741
|
+
this.failures += 1;
|
|
742
|
+
clearTimeout(this.retry);
|
|
743
|
+
const delay = Math.min(REFRESH_INTERVAL_MS, 2e3 * 2 ** (this.failures - 1));
|
|
744
|
+
this.retry = setTimeout(() => {
|
|
745
|
+
this.retry = void 0;
|
|
746
|
+
this.refresh();
|
|
747
|
+
}, delay);
|
|
748
|
+
}
|
|
749
|
+
/** The person has to sign in again: go there, no question asked (but never in a loop). */
|
|
750
|
+
goSignIn(url) {
|
|
751
|
+
if (recentlySentTo(url)) {
|
|
752
|
+
this.signin = null;
|
|
753
|
+
this.fail(true);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const event = new CustomEvent(SIGNIN_EVENT, { detail: { url }, bubbles: true, composed: true, cancelable: true });
|
|
757
|
+
if (!this.dispatchEvent(event)) return;
|
|
758
|
+
markSentTo(url);
|
|
759
|
+
navigateTop(url);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function defineServiceMark() {
|
|
764
|
+
return class CboxServiceMarkElement extends HTMLElement {
|
|
765
|
+
static observedAttributes = ["service", "theme"];
|
|
766
|
+
frame;
|
|
767
|
+
stops = [];
|
|
768
|
+
paint = () => {
|
|
769
|
+
const app = readKnownAppCatalog().find((entry) => entry.key === this.getAttribute("service"));
|
|
770
|
+
this.frame.replaceChildren(...app !== void 0 && secureIconUrl(app.icon_url) !== null ? [appIcon(app, "nothing")] : []);
|
|
771
|
+
};
|
|
772
|
+
syncTheme = () => {
|
|
773
|
+
this.frame.classList.toggle("dark", isDark(themeMode(this.getAttribute("theme"))));
|
|
774
|
+
};
|
|
775
|
+
constructor() {
|
|
776
|
+
super();
|
|
777
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
778
|
+
adopt(shadow, MARK_CSS);
|
|
779
|
+
this.frame = document.createElement("span");
|
|
780
|
+
this.frame.className = "root";
|
|
781
|
+
shadow.append(this.frame);
|
|
782
|
+
}
|
|
783
|
+
connectedCallback() {
|
|
784
|
+
this.stops.forEach((stop2) => stop2());
|
|
785
|
+
this.stops = [subscribeKnownAppCatalog(this.paint), subscribeTheme(this.syncTheme)];
|
|
786
|
+
this.syncTheme();
|
|
787
|
+
this.paint();
|
|
788
|
+
}
|
|
789
|
+
disconnectedCallback() {
|
|
790
|
+
this.stops.forEach((stop2) => stop2());
|
|
791
|
+
this.stops = [];
|
|
792
|
+
}
|
|
793
|
+
attributeChangedCallback(name) {
|
|
794
|
+
if (!this.isConnected) return;
|
|
795
|
+
if (name === "theme") this.syncTheme();
|
|
796
|
+
else this.paint();
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function defineCboxAppSwitcher() {
|
|
801
|
+
if (typeof window === "undefined" || typeof customElements === "undefined") return;
|
|
802
|
+
if (customElements.get("cbox-service-mark") === void 0) customElements.define("cbox-service-mark", defineServiceMark());
|
|
803
|
+
if (customElements.get("cbox-app-switcher") === void 0) customElements.define("cbox-app-switcher", defineSwitcher());
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export { DEFAULT_LABELS, SIGNIN_EVENT, authorizeHref, defineCboxAppSwitcher, initials, isDark, parseAppCatalog, readKnownAppCatalog, rememberAppCatalog, secureIconUrl, signInTarget, subscribeKnownAppCatalog };
|
|
807
|
+
//# sourceMappingURL=chunk-AJCYF7SS.js.map
|
|
808
|
+
//# sourceMappingURL=chunk-AJCYF7SS.js.map
|