@etamong-playground/ui 0.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1302 -0
- package/dist/helpers.cjs +137 -0
- package/dist/helpers.d.cts +96 -0
- package/dist/helpers.d.ts +96 -0
- package/dist/helpers.js +118 -0
- package/dist/index.cjs +3684 -0
- package/dist/index.d.cts +1800 -0
- package/dist/index.d.ts +1800 -0
- package/dist/index.js +3585 -0
- package/dist/styles.css +2312 -0
- package/dist/testing.cjs +185 -0
- package/dist/testing.d.cts +166 -0
- package/dist/testing.d.ts +166 -0
- package/dist/testing.js +173 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3585 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useState, useCallback, useEffect, useRef, useId, useContext, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Command } from 'cmdk';
|
|
4
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/CommandPalette.tsx
|
|
7
|
+
|
|
8
|
+
// src/keywords.ts
|
|
9
|
+
function crossLocaleKeywords(dicts, getter) {
|
|
10
|
+
return dicts.map(getter).filter(Boolean).join(" ");
|
|
11
|
+
}
|
|
12
|
+
function isInputTarget(e) {
|
|
13
|
+
const t = e.target;
|
|
14
|
+
if (!t) return false;
|
|
15
|
+
return t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable;
|
|
16
|
+
}
|
|
17
|
+
var CODE_TO_KEY = {
|
|
18
|
+
Slash: "/",
|
|
19
|
+
KeyG: "g",
|
|
20
|
+
KeyH: "h",
|
|
21
|
+
KeyK: "k",
|
|
22
|
+
KeyS: "s",
|
|
23
|
+
KeyN: "n",
|
|
24
|
+
KeyT: "t",
|
|
25
|
+
KeyM: "m",
|
|
26
|
+
KeyP: "p"
|
|
27
|
+
};
|
|
28
|
+
function shortcutKey(e) {
|
|
29
|
+
const isAscii = e.key === "/" || e.key === "?" || /^[a-z]$/.test(e.key);
|
|
30
|
+
return isAscii ? e.key : CODE_TO_KEY[e.code] ?? e.key;
|
|
31
|
+
}
|
|
32
|
+
var COMMAND_PALETTE_OPEN_EVENT = "command-palette:open";
|
|
33
|
+
function openCommandPalette() {
|
|
34
|
+
document.dispatchEvent(new CustomEvent(COMMAND_PALETTE_OPEN_EVENT));
|
|
35
|
+
}
|
|
36
|
+
var DEFAULT_LABELS = {
|
|
37
|
+
placeholder: "Type a command or search\u2026",
|
|
38
|
+
noResults: "No results.",
|
|
39
|
+
searchHeading: "Search"
|
|
40
|
+
};
|
|
41
|
+
function CommandPalette({
|
|
42
|
+
sections,
|
|
43
|
+
searchActions,
|
|
44
|
+
onNavigate,
|
|
45
|
+
isAdmin = false,
|
|
46
|
+
labels,
|
|
47
|
+
openOnSlash = true,
|
|
48
|
+
open: controlledOpen,
|
|
49
|
+
onOpenChange
|
|
50
|
+
}) {
|
|
51
|
+
const isControlled = controlledOpen !== void 0;
|
|
52
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
53
|
+
const [search, setSearch] = useState("");
|
|
54
|
+
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
|
55
|
+
const text = { ...DEFAULT_LABELS, ...labels };
|
|
56
|
+
const setOpen = useCallback(
|
|
57
|
+
(next) => {
|
|
58
|
+
if (!next) setSearch("");
|
|
59
|
+
if (isControlled) onOpenChange?.(next);
|
|
60
|
+
else setUncontrolledOpen(next);
|
|
61
|
+
},
|
|
62
|
+
[isControlled, onOpenChange]
|
|
63
|
+
);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
function onKeyDown(e) {
|
|
66
|
+
if (e.repeat) return;
|
|
67
|
+
const key = shortcutKey(e);
|
|
68
|
+
if ((e.metaKey || e.ctrlKey) && key === "k") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
setOpen(!open);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (openOnSlash && key === "/" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey && !isInputTarget(e)) {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
setOpen(true);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function onOpenEvent() {
|
|
79
|
+
setOpen(true);
|
|
80
|
+
}
|
|
81
|
+
document.addEventListener("keydown", onKeyDown);
|
|
82
|
+
document.addEventListener(COMMAND_PALETTE_OPEN_EVENT, onOpenEvent);
|
|
83
|
+
return () => {
|
|
84
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
85
|
+
document.removeEventListener(COMMAND_PALETTE_OPEN_EVENT, onOpenEvent);
|
|
86
|
+
};
|
|
87
|
+
}, [open, openOnSlash, setOpen]);
|
|
88
|
+
const select = useCallback(
|
|
89
|
+
(item) => {
|
|
90
|
+
setOpen(false);
|
|
91
|
+
if (item.onSelect) item.onSelect();
|
|
92
|
+
else if (item.href) onNavigate?.(item.href);
|
|
93
|
+
},
|
|
94
|
+
[onNavigate, setOpen]
|
|
95
|
+
);
|
|
96
|
+
const runSearch = useCallback(
|
|
97
|
+
(action) => {
|
|
98
|
+
setOpen(false);
|
|
99
|
+
action.run(search.trim());
|
|
100
|
+
},
|
|
101
|
+
[search, setOpen]
|
|
102
|
+
);
|
|
103
|
+
const visibleSections = sections.map((s) => ({
|
|
104
|
+
...s,
|
|
105
|
+
items: s.items.filter((i) => !i.adminOnly || isAdmin)
|
|
106
|
+
})).filter((s) => s.forceMount || s.items.length > 0);
|
|
107
|
+
return /* @__PURE__ */ jsxs(
|
|
108
|
+
Command.Dialog,
|
|
109
|
+
{
|
|
110
|
+
open,
|
|
111
|
+
onOpenChange: setOpen,
|
|
112
|
+
className: "etu-cmdk",
|
|
113
|
+
overlayClassName: "etu-cmdk-overlay",
|
|
114
|
+
contentClassName: "etu-cmdk-content",
|
|
115
|
+
shouldFilter: true,
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
Command.Input,
|
|
119
|
+
{
|
|
120
|
+
value: search,
|
|
121
|
+
onValueChange: setSearch,
|
|
122
|
+
placeholder: text.placeholder,
|
|
123
|
+
className: "etu-cmdk-input",
|
|
124
|
+
autoFocus: true
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
/* @__PURE__ */ jsxs(Command.List, { className: "etu-cmdk-list", children: [
|
|
128
|
+
/* @__PURE__ */ jsx(Command.Empty, { className: "etu-cmdk-empty", children: text.noResults }),
|
|
129
|
+
visibleSections.map((section) => /* @__PURE__ */ jsx(
|
|
130
|
+
Command.Group,
|
|
131
|
+
{
|
|
132
|
+
heading: section.heading,
|
|
133
|
+
forceMount: section.forceMount,
|
|
134
|
+
children: section.items.map((item) => /* @__PURE__ */ jsxs(
|
|
135
|
+
Command.Item,
|
|
136
|
+
{
|
|
137
|
+
value: item.keywords || item.label,
|
|
138
|
+
onSelect: () => select(item),
|
|
139
|
+
className: "etu-cmdk-item",
|
|
140
|
+
children: [
|
|
141
|
+
item.icon ? /* @__PURE__ */ jsx("span", { className: "etu-cmdk-item-icon", children: item.icon }) : null,
|
|
142
|
+
/* @__PURE__ */ jsx("span", { className: "etu-cmdk-item-label", children: item.label }),
|
|
143
|
+
item.sublabel ? /* @__PURE__ */ jsx("span", { className: "etu-cmdk-item-sub", children: item.sublabel }) : null
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
item.id
|
|
147
|
+
))
|
|
148
|
+
},
|
|
149
|
+
section.id
|
|
150
|
+
)),
|
|
151
|
+
searchActions && searchActions.length > 0 ? /* @__PURE__ */ jsx(
|
|
152
|
+
Command.Group,
|
|
153
|
+
{
|
|
154
|
+
heading: text.searchHeading,
|
|
155
|
+
forceMount: true,
|
|
156
|
+
className: "etu-cmdk-search",
|
|
157
|
+
children: searchActions.map((action) => /* @__PURE__ */ jsxs(
|
|
158
|
+
Command.Item,
|
|
159
|
+
{
|
|
160
|
+
value: `__search__ ${action.keywords || action.label}`,
|
|
161
|
+
onSelect: () => runSearch(action),
|
|
162
|
+
className: "etu-cmdk-item etu-cmdk-item-search",
|
|
163
|
+
children: [
|
|
164
|
+
action.icon ? /* @__PURE__ */ jsx("span", { className: "etu-cmdk-item-icon", children: action.icon }) : null,
|
|
165
|
+
/* @__PURE__ */ jsx("span", { className: "etu-cmdk-item-label", children: action.label }),
|
|
166
|
+
search.trim() ? /* @__PURE__ */ jsxs("span", { className: "etu-cmdk-item-sub", children: [
|
|
167
|
+
'"',
|
|
168
|
+
search.trim(),
|
|
169
|
+
'"'
|
|
170
|
+
] }) : null
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
action.id
|
|
174
|
+
))
|
|
175
|
+
}
|
|
176
|
+
) : null
|
|
177
|
+
] }),
|
|
178
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-cmdk-footer", children: [
|
|
179
|
+
/* @__PURE__ */ jsx("kbd", { className: "etu-cmdk-kbd", children: "\u2191\u2193" }),
|
|
180
|
+
/* @__PURE__ */ jsx("kbd", { className: "etu-cmdk-kbd", children: "\u21B5" }),
|
|
181
|
+
/* @__PURE__ */ jsx("kbd", { className: "etu-cmdk-kbd", children: "esc" })
|
|
182
|
+
] })
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
function CommandPaletteTrigger({ label = "Search\u2026", className }) {
|
|
188
|
+
const [isMac, setIsMac] = useState(false);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const p = (navigator.platform || navigator.userAgent || "").toLowerCase();
|
|
191
|
+
setIsMac(/mac|iphone|ipad|ipod/.test(p));
|
|
192
|
+
}, []);
|
|
193
|
+
return /* @__PURE__ */ jsxs(
|
|
194
|
+
"button",
|
|
195
|
+
{
|
|
196
|
+
type: "button",
|
|
197
|
+
onClick: () => openCommandPalette(),
|
|
198
|
+
className: "etu-palette-trigger" + (className ? " " + className : ""),
|
|
199
|
+
"aria-label": label,
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ jsxs(
|
|
202
|
+
"svg",
|
|
203
|
+
{
|
|
204
|
+
className: "etu-palette-trigger-icon",
|
|
205
|
+
width: "14",
|
|
206
|
+
height: "14",
|
|
207
|
+
viewBox: "0 0 24 24",
|
|
208
|
+
fill: "none",
|
|
209
|
+
stroke: "currentColor",
|
|
210
|
+
strokeWidth: "2",
|
|
211
|
+
strokeLinecap: "round",
|
|
212
|
+
strokeLinejoin: "round",
|
|
213
|
+
"aria-hidden": "true",
|
|
214
|
+
children: [
|
|
215
|
+
/* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
|
|
216
|
+
/* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ jsx("span", { className: "etu-palette-trigger-label", children: label }),
|
|
221
|
+
/* @__PURE__ */ jsxs("kbd", { className: "etu-palette-trigger-kbd", children: [
|
|
222
|
+
isMac ? "\u2318" : "Ctrl+",
|
|
223
|
+
"K"
|
|
224
|
+
] })
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
function useGoToShortcuts(routes, onNavigate, options = {}) {
|
|
230
|
+
const { isAdmin = false, timeoutMs = 1500 } = options;
|
|
231
|
+
const [pending, setPending] = useState(null);
|
|
232
|
+
const pendingRef = useRef(null);
|
|
233
|
+
const timerRef = useRef(void 0);
|
|
234
|
+
const routesRef = useRef(routes);
|
|
235
|
+
const navRef = useRef(onNavigate);
|
|
236
|
+
const adminRef = useRef(isAdmin);
|
|
237
|
+
routesRef.current = routes;
|
|
238
|
+
navRef.current = onNavigate;
|
|
239
|
+
adminRef.current = isAdmin;
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
function clearPending() {
|
|
242
|
+
clearTimeout(timerRef.current);
|
|
243
|
+
pendingRef.current = null;
|
|
244
|
+
setPending(null);
|
|
245
|
+
}
|
|
246
|
+
function onKeyDown(e) {
|
|
247
|
+
if (e.repeat || isInputTarget(e)) return;
|
|
248
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
249
|
+
const key = shortcutKey(e);
|
|
250
|
+
if (key === "g" && !e.shiftKey && !pendingRef.current) {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
pendingRef.current = "g";
|
|
253
|
+
setPending("g");
|
|
254
|
+
timerRef.current = setTimeout(clearPending, timeoutMs);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (pendingRef.current === "g" && !e.shiftKey) {
|
|
258
|
+
const route = routesRef.current.find((r) => r.key === key);
|
|
259
|
+
if (route && (!route.adminOnly || adminRef.current)) {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
navRef.current(route.href);
|
|
262
|
+
}
|
|
263
|
+
clearPending();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
document.addEventListener("keydown", onKeyDown);
|
|
267
|
+
return () => {
|
|
268
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
269
|
+
clearTimeout(timerRef.current);
|
|
270
|
+
};
|
|
271
|
+
}, [timeoutMs]);
|
|
272
|
+
return pending;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/theme.ts
|
|
276
|
+
function storageKey(appKey) {
|
|
277
|
+
return `${appKey}-theme`;
|
|
278
|
+
}
|
|
279
|
+
function noFlashThemeScript(appKey) {
|
|
280
|
+
return `(function(){try{var k=${JSON.stringify(storageKey(appKey))};var s=localStorage.getItem(k);var d=s;if(d!=="light"&&d!=="dark"){d=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches)?"light":"dark";}document.documentElement.setAttribute("data-theme",d);}catch(e){}})();`;
|
|
281
|
+
}
|
|
282
|
+
function getTheme(appKey) {
|
|
283
|
+
try {
|
|
284
|
+
const saved = localStorage.getItem(storageKey(appKey));
|
|
285
|
+
if (saved === "light" || saved === "dark") return saved;
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
if (typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
289
|
+
return "light";
|
|
290
|
+
}
|
|
291
|
+
return "dark";
|
|
292
|
+
}
|
|
293
|
+
function setTheme(appKey, theme) {
|
|
294
|
+
try {
|
|
295
|
+
localStorage.setItem(storageKey(appKey), theme);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
299
|
+
}
|
|
300
|
+
var items = [];
|
|
301
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
302
|
+
var nextId = 1;
|
|
303
|
+
function emit() {
|
|
304
|
+
for (const l of listeners) l(items);
|
|
305
|
+
}
|
|
306
|
+
function toast(message, kind = "ok", durationMs = 3200) {
|
|
307
|
+
const id = nextId++;
|
|
308
|
+
items = [...items, { id, message, kind }];
|
|
309
|
+
emit();
|
|
310
|
+
if (durationMs > 0) {
|
|
311
|
+
setTimeout(() => dismissToast(id), durationMs);
|
|
312
|
+
}
|
|
313
|
+
return id;
|
|
314
|
+
}
|
|
315
|
+
function dismissToast(id) {
|
|
316
|
+
items = items.filter((t) => t.id !== id);
|
|
317
|
+
emit();
|
|
318
|
+
}
|
|
319
|
+
function Toaster() {
|
|
320
|
+
const [list, setList] = useState(items);
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
listeners.add(setList);
|
|
323
|
+
setList(items);
|
|
324
|
+
return () => {
|
|
325
|
+
listeners.delete(setList);
|
|
326
|
+
};
|
|
327
|
+
}, []);
|
|
328
|
+
if (list.length === 0) return null;
|
|
329
|
+
return /* @__PURE__ */ jsx("div", { className: "etu-toaster", role: "status", "aria-live": "polite", children: list.map((t) => /* @__PURE__ */ jsx(
|
|
330
|
+
"div",
|
|
331
|
+
{
|
|
332
|
+
className: `etu-toast etu-toast-${t.kind}`,
|
|
333
|
+
onClick: () => dismissToast(t.id),
|
|
334
|
+
children: t.message
|
|
335
|
+
},
|
|
336
|
+
t.id
|
|
337
|
+
)) });
|
|
338
|
+
}
|
|
339
|
+
var current = null;
|
|
340
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
341
|
+
function setReq(r) {
|
|
342
|
+
current = r;
|
|
343
|
+
for (const l of listeners2) l(r);
|
|
344
|
+
}
|
|
345
|
+
function uiConfirm(opts) {
|
|
346
|
+
return new Promise((resolve) => setReq({ kind: "confirm", ...opts, resolve }));
|
|
347
|
+
}
|
|
348
|
+
function uiPrompt(opts) {
|
|
349
|
+
return new Promise((resolve) => setReq({ kind: "prompt", ...opts, resolve }));
|
|
350
|
+
}
|
|
351
|
+
function DialogHost() {
|
|
352
|
+
const [req, setLocalReq] = useState(current);
|
|
353
|
+
const [value, setValue] = useState("");
|
|
354
|
+
const inputRef = useRef(null);
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
listeners2.add(setLocalReq);
|
|
357
|
+
return () => {
|
|
358
|
+
listeners2.delete(setLocalReq);
|
|
359
|
+
};
|
|
360
|
+
}, []);
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (req?.kind === "prompt") {
|
|
363
|
+
setValue(req.defaultValue ?? "");
|
|
364
|
+
const t = setTimeout(() => inputRef.current?.focus(), 0);
|
|
365
|
+
return () => clearTimeout(t);
|
|
366
|
+
}
|
|
367
|
+
}, [req]);
|
|
368
|
+
if (!req) return null;
|
|
369
|
+
const cancel = () => {
|
|
370
|
+
if (req.kind === "prompt") req.resolve(null);
|
|
371
|
+
else req.resolve(false);
|
|
372
|
+
setReq(null);
|
|
373
|
+
};
|
|
374
|
+
const confirm = () => {
|
|
375
|
+
if (req.kind === "prompt") req.resolve(value);
|
|
376
|
+
else req.resolve(true);
|
|
377
|
+
setReq(null);
|
|
378
|
+
};
|
|
379
|
+
const onKeyDown = (e) => {
|
|
380
|
+
if (e.key === "Escape") {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
cancel();
|
|
383
|
+
} else if (e.key === "Enter" && req.kind === "confirm") {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
confirm();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
return /* @__PURE__ */ jsx(
|
|
389
|
+
"div",
|
|
390
|
+
{
|
|
391
|
+
className: "etu-dialog-overlay",
|
|
392
|
+
onMouseDown: (e) => {
|
|
393
|
+
if (e.target === e.currentTarget) cancel();
|
|
394
|
+
},
|
|
395
|
+
onKeyDown,
|
|
396
|
+
children: /* @__PURE__ */ jsxs("div", { className: "etu-dialog", role: "dialog", "aria-modal": "true", children: [
|
|
397
|
+
req.title ? /* @__PURE__ */ jsx("div", { className: "etu-dialog-title", children: req.title }) : null,
|
|
398
|
+
req.body ? /* @__PURE__ */ jsx("div", { className: "etu-dialog-body", children: req.body }) : null,
|
|
399
|
+
req.kind === "prompt" ? /* @__PURE__ */ jsx(
|
|
400
|
+
"input",
|
|
401
|
+
{
|
|
402
|
+
ref: inputRef,
|
|
403
|
+
className: "etu-dialog-input",
|
|
404
|
+
value,
|
|
405
|
+
placeholder: req.placeholder,
|
|
406
|
+
onChange: (e) => setValue(e.target.value),
|
|
407
|
+
onKeyDown: (e) => {
|
|
408
|
+
if (e.key === "Enter") {
|
|
409
|
+
e.preventDefault();
|
|
410
|
+
confirm();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
) : null,
|
|
415
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-dialog-actions", children: [
|
|
416
|
+
/* @__PURE__ */ jsx("button", { className: "etu-dialog-btn", onClick: cancel, children: req.cancelLabel ?? "Cancel" }),
|
|
417
|
+
/* @__PURE__ */ jsx(
|
|
418
|
+
"button",
|
|
419
|
+
{
|
|
420
|
+
className: "etu-dialog-btn etu-dialog-btn-primary" + (req.kind === "confirm" && req.danger ? " etu-dialog-btn-danger" : ""),
|
|
421
|
+
onClick: confirm,
|
|
422
|
+
children: req.confirmLabel ?? "OK"
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
] })
|
|
426
|
+
] })
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
function toDate(when) {
|
|
431
|
+
if (when instanceof Date) return Number.isNaN(when.getTime()) ? null : when;
|
|
432
|
+
if (typeof when === "number") {
|
|
433
|
+
const d = new Date(when);
|
|
434
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
435
|
+
}
|
|
436
|
+
if (typeof when === "string") {
|
|
437
|
+
const d = new Date(when);
|
|
438
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
var REL_STEPS = [
|
|
443
|
+
["second", 60],
|
|
444
|
+
["minute", 60],
|
|
445
|
+
["hour", 24],
|
|
446
|
+
["day", 30],
|
|
447
|
+
["month", 12],
|
|
448
|
+
["year", Infinity]
|
|
449
|
+
];
|
|
450
|
+
function formatRelTime(when, opts = {}) {
|
|
451
|
+
const d = toDate(when);
|
|
452
|
+
if (!d) return "";
|
|
453
|
+
const nowDate = opts.now !== void 0 ? toDate(opts.now) : /* @__PURE__ */ new Date();
|
|
454
|
+
if (!nowDate) return "";
|
|
455
|
+
const diffSec = (d.getTime() - nowDate.getTime()) / 1e3;
|
|
456
|
+
const rtf = new Intl.RelativeTimeFormat(opts.locale, {
|
|
457
|
+
numeric: opts.numeric ?? "auto"
|
|
458
|
+
});
|
|
459
|
+
let value = diffSec;
|
|
460
|
+
for (const [unit, step] of REL_STEPS) {
|
|
461
|
+
if (Math.abs(value) < step) return rtf.format(Math.round(value), unit);
|
|
462
|
+
value /= step;
|
|
463
|
+
}
|
|
464
|
+
return rtf.format(Math.round(value), "year");
|
|
465
|
+
}
|
|
466
|
+
var STYLE_PRESETS = {
|
|
467
|
+
date: { year: "numeric", month: "2-digit", day: "2-digit" },
|
|
468
|
+
time: { hour: "2-digit", minute: "2-digit", hour12: false },
|
|
469
|
+
datetime: {
|
|
470
|
+
year: "numeric",
|
|
471
|
+
month: "2-digit",
|
|
472
|
+
day: "2-digit",
|
|
473
|
+
hour: "2-digit",
|
|
474
|
+
minute: "2-digit",
|
|
475
|
+
hour12: false
|
|
476
|
+
},
|
|
477
|
+
"datetime-seconds": {
|
|
478
|
+
year: "numeric",
|
|
479
|
+
month: "2-digit",
|
|
480
|
+
day: "2-digit",
|
|
481
|
+
hour: "2-digit",
|
|
482
|
+
minute: "2-digit",
|
|
483
|
+
second: "2-digit",
|
|
484
|
+
hour12: false
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var ZONE_SUFFIX_LABELS = {
|
|
488
|
+
"Asia/Seoul": "KST",
|
|
489
|
+
UTC: "UTC"
|
|
490
|
+
};
|
|
491
|
+
function formatAbsTime(when, opts = {}) {
|
|
492
|
+
const d = toDate(when);
|
|
493
|
+
if (!d) return "";
|
|
494
|
+
const timeZone = opts.timeZone ?? "Asia/Seoul";
|
|
495
|
+
const locale = opts.locale ?? "ko-KR";
|
|
496
|
+
const options = {
|
|
497
|
+
timeZone,
|
|
498
|
+
...opts.formatOptions ?? STYLE_PRESETS[opts.style ?? "datetime"]
|
|
499
|
+
};
|
|
500
|
+
const out = new Intl.DateTimeFormat(locale, options).format(d);
|
|
501
|
+
if (!opts.withZoneSuffix) return out;
|
|
502
|
+
const suffix = ZONE_SUFFIX_LABELS[timeZone] ?? timeZone;
|
|
503
|
+
return out + " " + suffix;
|
|
504
|
+
}
|
|
505
|
+
function refreshIntervalMs(diffMs) {
|
|
506
|
+
const abs = Math.abs(diffMs);
|
|
507
|
+
if (abs < 6e4) return 15e3;
|
|
508
|
+
if (abs < 36e5) return 6e4;
|
|
509
|
+
return 10 * 6e4;
|
|
510
|
+
}
|
|
511
|
+
function RelTime({
|
|
512
|
+
when,
|
|
513
|
+
locale,
|
|
514
|
+
numeric,
|
|
515
|
+
absoluteOptions,
|
|
516
|
+
as = "time",
|
|
517
|
+
className
|
|
518
|
+
}) {
|
|
519
|
+
const d = toDate(when);
|
|
520
|
+
const [tick, setTick] = useState(0);
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
if (!d) return;
|
|
523
|
+
const id = window.setInterval(() => {
|
|
524
|
+
setTick((t) => t + 1);
|
|
525
|
+
}, refreshIntervalMs(d.getTime() - Date.now()));
|
|
526
|
+
return () => window.clearInterval(id);
|
|
527
|
+
}, [d?.getTime()]);
|
|
528
|
+
if (!d) return null;
|
|
529
|
+
const rel = formatRelTime(d, { locale, numeric });
|
|
530
|
+
const abs = formatAbsTime(d, { withZoneSuffix: true, ...absoluteOptions });
|
|
531
|
+
if (as === "span") {
|
|
532
|
+
return /* @__PURE__ */ jsx("span", { title: abs, className, "data-tick": tick, children: rel });
|
|
533
|
+
}
|
|
534
|
+
return /* @__PURE__ */ jsx("time", { dateTime: d.toISOString(), title: abs, className, "data-tick": tick, children: rel });
|
|
535
|
+
}
|
|
536
|
+
function DeployInfo({ version, builtAt, label = "deployed", href, className }) {
|
|
537
|
+
if (!version && !builtAt) return null;
|
|
538
|
+
const short = version ? version.slice(0, 7) : null;
|
|
539
|
+
const rel = builtAt ? formatRelTime(builtAt) : null;
|
|
540
|
+
const title = [
|
|
541
|
+
version && `commit ${version}`,
|
|
542
|
+
builtAt && `built ${formatAbsTime(builtAt, { withZoneSuffix: true })}`
|
|
543
|
+
].filter(Boolean).join("\n");
|
|
544
|
+
return /* @__PURE__ */ jsxs("span", { className: "etu-deploy-info" + (className ? " " + className : ""), title: title || void 0, children: [
|
|
545
|
+
/* @__PURE__ */ jsx("span", { className: "etu-deploy-info-label", children: label }),
|
|
546
|
+
short && (href ? /* @__PURE__ */ jsx("a", { className: "etu-deploy-info-ver", href, target: "_blank", rel: "noreferrer", children: short }) : /* @__PURE__ */ jsx("code", { className: "etu-deploy-info-ver", children: short })),
|
|
547
|
+
rel && /* @__PURE__ */ jsxs("span", { className: "etu-deploy-info-time", children: [
|
|
548
|
+
"\xB7 ",
|
|
549
|
+
rel
|
|
550
|
+
] })
|
|
551
|
+
] });
|
|
552
|
+
}
|
|
553
|
+
var DEFAULTS = {
|
|
554
|
+
label: "\uD648 \uD654\uBA74\uC5D0 \uCD94\uAC00\uD558\uBA74 \uB354 \uBE60\uB974\uAC8C!",
|
|
555
|
+
iosHint: '\uACF5\uC720 \u2192 "\uD648 \uD654\uBA74\uC5D0 \uCD94\uAC00"\uB85C \uC124\uCE58\uD558\uC138\uC694',
|
|
556
|
+
installLabel: "\uC124\uCE58",
|
|
557
|
+
dismissLabel: "\uB2EB\uAE30",
|
|
558
|
+
cooldownMs: 3 * 24 * 60 * 60 * 1e3,
|
|
559
|
+
// 3 days
|
|
560
|
+
maxDismiss: 3,
|
|
561
|
+
storageKey: "etu-install-banner"
|
|
562
|
+
};
|
|
563
|
+
function readState(key) {
|
|
564
|
+
try {
|
|
565
|
+
const raw = localStorage.getItem(key);
|
|
566
|
+
if (!raw) return { count: 0, last: 0 };
|
|
567
|
+
const p = JSON.parse(raw);
|
|
568
|
+
if (Number.isFinite(p.count) && Number.isFinite(p.last)) return p;
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
return { count: 0, last: 0 };
|
|
572
|
+
}
|
|
573
|
+
function saveState(key, s) {
|
|
574
|
+
try {
|
|
575
|
+
localStorage.setItem(key, JSON.stringify(s));
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function isStandalone() {
|
|
580
|
+
if (typeof window === "undefined") return false;
|
|
581
|
+
try {
|
|
582
|
+
if (window.matchMedia("(display-mode: standalone)").matches) return true;
|
|
583
|
+
return "standalone" in navigator && navigator.standalone === true;
|
|
584
|
+
} catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function isIOS() {
|
|
589
|
+
if (typeof navigator === "undefined") return false;
|
|
590
|
+
try {
|
|
591
|
+
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !("MSStream" in window);
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function useInstallPrompt() {
|
|
597
|
+
const [canPrompt, setCanPrompt] = useState(false);
|
|
598
|
+
const [iosFlag, setIos] = useState(false);
|
|
599
|
+
const [standalone, setStandalone] = useState(false);
|
|
600
|
+
const evtRef = useRef(null);
|
|
601
|
+
useEffect(() => {
|
|
602
|
+
setIos(isIOS());
|
|
603
|
+
setStandalone(isStandalone());
|
|
604
|
+
const onPrompt = (e) => {
|
|
605
|
+
e.preventDefault();
|
|
606
|
+
evtRef.current = e;
|
|
607
|
+
setCanPrompt(true);
|
|
608
|
+
};
|
|
609
|
+
const onInstalled = () => {
|
|
610
|
+
evtRef.current = null;
|
|
611
|
+
setCanPrompt(false);
|
|
612
|
+
setStandalone(true);
|
|
613
|
+
};
|
|
614
|
+
window.addEventListener("beforeinstallprompt", onPrompt);
|
|
615
|
+
window.addEventListener("appinstalled", onInstalled);
|
|
616
|
+
return () => {
|
|
617
|
+
window.removeEventListener("beforeinstallprompt", onPrompt);
|
|
618
|
+
window.removeEventListener("appinstalled", onInstalled);
|
|
619
|
+
};
|
|
620
|
+
}, []);
|
|
621
|
+
async function promptInstall() {
|
|
622
|
+
const evt = evtRef.current;
|
|
623
|
+
if (!evt) return "unsupported";
|
|
624
|
+
await evt.prompt();
|
|
625
|
+
const { outcome } = await evt.userChoice;
|
|
626
|
+
evtRef.current = null;
|
|
627
|
+
setCanPrompt(false);
|
|
628
|
+
return outcome;
|
|
629
|
+
}
|
|
630
|
+
return { canPrompt, promptInstall, isIOS: iosFlag, isStandalone: standalone };
|
|
631
|
+
}
|
|
632
|
+
function DefaultIcon() {
|
|
633
|
+
return /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M12 3v12m0 0l-4-4m4 4l4-4M5 21h14" }) });
|
|
634
|
+
}
|
|
635
|
+
function InstallBanner({
|
|
636
|
+
label = DEFAULTS.label,
|
|
637
|
+
iosHint = DEFAULTS.iosHint,
|
|
638
|
+
installLabel = DEFAULTS.installLabel,
|
|
639
|
+
dismissLabel = DEFAULTS.dismissLabel,
|
|
640
|
+
icon,
|
|
641
|
+
cooldownMs = DEFAULTS.cooldownMs,
|
|
642
|
+
maxDismiss = DEFAULTS.maxDismiss,
|
|
643
|
+
storageKey: storageKey3 = DEFAULTS.storageKey,
|
|
644
|
+
className
|
|
645
|
+
}) {
|
|
646
|
+
const { canPrompt, promptInstall, isIOS: iosFlag, isStandalone: standalone } = useInstallPrompt();
|
|
647
|
+
const [allowed, setAllowed] = useState(false);
|
|
648
|
+
const [closed, setClosed] = useState(false);
|
|
649
|
+
useEffect(() => {
|
|
650
|
+
if (standalone) return;
|
|
651
|
+
const { count, last } = readState(storageKey3);
|
|
652
|
+
if (count >= maxDismiss) return;
|
|
653
|
+
const remaining = Math.max(0, last + cooldownMs - Date.now());
|
|
654
|
+
const t = setTimeout(() => setAllowed(true), remaining);
|
|
655
|
+
return () => clearTimeout(t);
|
|
656
|
+
}, [standalone, storageKey3, cooldownMs, maxDismiss]);
|
|
657
|
+
if (closed || standalone || !allowed) return null;
|
|
658
|
+
if (!iosFlag && !canPrompt) return null;
|
|
659
|
+
function dismiss() {
|
|
660
|
+
const prev = readState(storageKey3);
|
|
661
|
+
saveState(storageKey3, { count: prev.count + 1, last: Date.now() });
|
|
662
|
+
setClosed(true);
|
|
663
|
+
}
|
|
664
|
+
async function install() {
|
|
665
|
+
const outcome = await promptInstall();
|
|
666
|
+
if (outcome !== "unsupported") setClosed(true);
|
|
667
|
+
}
|
|
668
|
+
return /* @__PURE__ */ jsxs("div", { className: "etu-install-banner" + (className ? " " + className : ""), children: [
|
|
669
|
+
/* @__PURE__ */ jsx("div", { className: "etu-install-banner-icon", children: icon ?? /* @__PURE__ */ jsx(DefaultIcon, {}) }),
|
|
670
|
+
/* @__PURE__ */ jsx("p", { className: "etu-install-banner-body", children: iosFlag ? iosHint : label }),
|
|
671
|
+
!iosFlag && /* @__PURE__ */ jsx("button", { type: "button", className: "etu-install-banner-cta", onClick: install, children: installLabel }),
|
|
672
|
+
/* @__PURE__ */ jsx(
|
|
673
|
+
"button",
|
|
674
|
+
{
|
|
675
|
+
type: "button",
|
|
676
|
+
className: "etu-install-banner-close",
|
|
677
|
+
onClick: dismiss,
|
|
678
|
+
"aria-label": dismissLabel,
|
|
679
|
+
children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" }) })
|
|
680
|
+
}
|
|
681
|
+
)
|
|
682
|
+
] });
|
|
683
|
+
}
|
|
684
|
+
var DEFAULT_ENDPOINT = "/.well-known/maintenance.json";
|
|
685
|
+
var DEFAULT_POLL_MS = 6e4;
|
|
686
|
+
function useStatusBanner(opts = {}) {
|
|
687
|
+
const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
688
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
689
|
+
const [data, setData] = useState(null);
|
|
690
|
+
const abortRef = useRef(null);
|
|
691
|
+
useEffect(() => {
|
|
692
|
+
let cancelled = false;
|
|
693
|
+
let timer = null;
|
|
694
|
+
const tick = async () => {
|
|
695
|
+
abortRef.current?.abort();
|
|
696
|
+
const ctrl = new AbortController();
|
|
697
|
+
abortRef.current = ctrl;
|
|
698
|
+
try {
|
|
699
|
+
const res = await fetch(endpoint, { credentials: "omit", signal: ctrl.signal });
|
|
700
|
+
if (!res.ok) {
|
|
701
|
+
if (!cancelled) setData(null);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const body = await res.json();
|
|
705
|
+
if (cancelled) return;
|
|
706
|
+
setData({
|
|
707
|
+
enabled: !!body.enabled,
|
|
708
|
+
severity: body.severity ?? null,
|
|
709
|
+
message_ko: body.message_ko ?? "",
|
|
710
|
+
message_en: body.message_en ?? "",
|
|
711
|
+
eta_iso: body.eta_iso ?? null,
|
|
712
|
+
retry_after_seconds: body.retry_after_seconds ?? null,
|
|
713
|
+
tags: Array.isArray(body.tags) ? body.tags : [],
|
|
714
|
+
updated_at: body.updated_at ?? null
|
|
715
|
+
});
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
const schedule = () => {
|
|
720
|
+
timer = setTimeout(async () => {
|
|
721
|
+
if (cancelled) return;
|
|
722
|
+
if (typeof document === "undefined" || !document.hidden) {
|
|
723
|
+
await tick();
|
|
724
|
+
}
|
|
725
|
+
schedule();
|
|
726
|
+
}, pollMs);
|
|
727
|
+
};
|
|
728
|
+
const onVisible = () => {
|
|
729
|
+
if (typeof document !== "undefined" && !document.hidden) void tick();
|
|
730
|
+
};
|
|
731
|
+
void tick();
|
|
732
|
+
schedule();
|
|
733
|
+
if (typeof document !== "undefined") {
|
|
734
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
735
|
+
}
|
|
736
|
+
return () => {
|
|
737
|
+
cancelled = true;
|
|
738
|
+
if (timer) clearTimeout(timer);
|
|
739
|
+
abortRef.current?.abort();
|
|
740
|
+
if (typeof document !== "undefined") {
|
|
741
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}, [endpoint, pollMs]);
|
|
745
|
+
return data;
|
|
746
|
+
}
|
|
747
|
+
var SEVERITY_LABEL = {
|
|
748
|
+
outage: { ko: "\uC7A5\uC560", en: "Outage" },
|
|
749
|
+
degraded: { ko: "\uC77C\uBD80 \uC7A5\uC560", en: "Degraded" },
|
|
750
|
+
maintenance: { ko: "\uC810\uAC80 \uC911", en: "Maintenance" }
|
|
751
|
+
};
|
|
752
|
+
var pickLang = (override) => {
|
|
753
|
+
if (override) return override;
|
|
754
|
+
if (typeof document === "undefined") return "en";
|
|
755
|
+
return (document.documentElement.lang || "").toLowerCase().startsWith("ko") ? "ko" : "en";
|
|
756
|
+
};
|
|
757
|
+
var sessionKey = (severity, updatedAt) => `etu-status-banner-dismissed:${severity}:${updatedAt ?? ""}`;
|
|
758
|
+
function StatusBanner({
|
|
759
|
+
endpoint,
|
|
760
|
+
pollMs,
|
|
761
|
+
lang,
|
|
762
|
+
className,
|
|
763
|
+
dismissible = true
|
|
764
|
+
}) {
|
|
765
|
+
const data = useStatusBanner({ endpoint, pollMs });
|
|
766
|
+
const [dismissed, setDismissed] = useState(false);
|
|
767
|
+
const sev = data?.severity;
|
|
768
|
+
const updatedAt = data?.updated_at ?? null;
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
if (!data || !data.enabled || !sev) return;
|
|
771
|
+
try {
|
|
772
|
+
const seen = sessionStorage.getItem(sessionKey(sev, updatedAt));
|
|
773
|
+
setDismissed(seen === "1");
|
|
774
|
+
} catch {
|
|
775
|
+
setDismissed(false);
|
|
776
|
+
}
|
|
777
|
+
}, [data, sev, updatedAt]);
|
|
778
|
+
if (!data || !data.enabled || !sev || sev === "outage" || dismissed) return null;
|
|
779
|
+
const effLang = pickLang(lang);
|
|
780
|
+
const label = SEVERITY_LABEL[sev][effLang];
|
|
781
|
+
const message = (effLang === "ko" ? data.message_ko : data.message_en) || (effLang === "ko" ? data.message_en : data.message_ko);
|
|
782
|
+
const eta = data.eta_iso ? new Date(data.eta_iso) : null;
|
|
783
|
+
const etaText = eta && !isNaN(eta.getTime()) ? effLang === "ko" ? `\uBCF5\uAD6C \uC608\uC815: ${eta.toLocaleString("ko-KR", { hour12: false })}` : `ETA: ${eta.toLocaleString(void 0, { hour12: false })}` : null;
|
|
784
|
+
const onDismiss = () => {
|
|
785
|
+
setDismissed(true);
|
|
786
|
+
try {
|
|
787
|
+
sessionStorage.setItem(sessionKey(sev, updatedAt), "1");
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
return /* @__PURE__ */ jsxs(
|
|
792
|
+
"div",
|
|
793
|
+
{
|
|
794
|
+
role: "status",
|
|
795
|
+
"aria-live": "polite",
|
|
796
|
+
className: ["etu-status-banner", `etu-status-banner-${sev}`, className].filter(Boolean).join(" "),
|
|
797
|
+
"data-severity": sev,
|
|
798
|
+
children: [
|
|
799
|
+
/* @__PURE__ */ jsx("span", { className: "etu-status-banner-label", children: label }),
|
|
800
|
+
/* @__PURE__ */ jsx("span", { className: "etu-status-banner-msg", children: message }),
|
|
801
|
+
etaText && /* @__PURE__ */ jsx("span", { className: "etu-status-banner-eta", children: etaText }),
|
|
802
|
+
dismissible && /* @__PURE__ */ jsx(
|
|
803
|
+
"button",
|
|
804
|
+
{
|
|
805
|
+
type: "button",
|
|
806
|
+
className: "etu-status-banner-close",
|
|
807
|
+
"aria-label": effLang === "ko" ? "\uB2EB\uAE30" : "Dismiss",
|
|
808
|
+
onClick: onDismiss,
|
|
809
|
+
children: "\xD7"
|
|
810
|
+
}
|
|
811
|
+
)
|
|
812
|
+
]
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
var DEFAULTS2 = {
|
|
817
|
+
title: "\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC5B4\uC694",
|
|
818
|
+
description: "\uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694. \uAC19\uC740 \uBB38\uC81C\uAC00 \uACC4\uC18D\uB418\uBA74 \uC544\uB798 \uCF54\uB4DC\uB97C \uC54C\uB824\uC8FC\uC138\uC694.",
|
|
819
|
+
retry: "\uB2E4\uC2DC \uC2DC\uB3C4",
|
|
820
|
+
home: "\uD648\uC73C\uB85C",
|
|
821
|
+
refLabel: "ref"
|
|
822
|
+
};
|
|
823
|
+
function DefaultIcon2() {
|
|
824
|
+
return /* @__PURE__ */ jsxs(
|
|
825
|
+
"svg",
|
|
826
|
+
{
|
|
827
|
+
viewBox: "0 0 24 24",
|
|
828
|
+
width: "42",
|
|
829
|
+
height: "42",
|
|
830
|
+
fill: "none",
|
|
831
|
+
stroke: "currentColor",
|
|
832
|
+
strokeWidth: "2",
|
|
833
|
+
strokeLinecap: "round",
|
|
834
|
+
strokeLinejoin: "round",
|
|
835
|
+
"aria-hidden": "true",
|
|
836
|
+
children: [
|
|
837
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
838
|
+
/* @__PURE__ */ jsx("path", { d: "M12 8v4" }),
|
|
839
|
+
/* @__PURE__ */ jsx("path", { d: "M12 16h.01" })
|
|
840
|
+
]
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
function ErrorPage({
|
|
845
|
+
title = DEFAULTS2.title,
|
|
846
|
+
description = DEFAULTS2.description,
|
|
847
|
+
refCode,
|
|
848
|
+
onRetry,
|
|
849
|
+
onHome,
|
|
850
|
+
labels,
|
|
851
|
+
icon,
|
|
852
|
+
className
|
|
853
|
+
}) {
|
|
854
|
+
const retryLabel = labels?.retry ?? DEFAULTS2.retry;
|
|
855
|
+
const homeLabel = labels?.home ?? DEFAULTS2.home;
|
|
856
|
+
const refLabel = labels?.refLabel ?? DEFAULTS2.refLabel;
|
|
857
|
+
return /* @__PURE__ */ jsx(
|
|
858
|
+
"div",
|
|
859
|
+
{
|
|
860
|
+
role: "alert",
|
|
861
|
+
"aria-live": "assertive",
|
|
862
|
+
className: "etu-error-page" + (className ? " " + className : ""),
|
|
863
|
+
children: /* @__PURE__ */ jsxs("div", { className: "etu-error-page-card", children: [
|
|
864
|
+
/* @__PURE__ */ jsx("div", { className: "etu-error-page-icon", children: icon ?? /* @__PURE__ */ jsx(DefaultIcon2, {}) }),
|
|
865
|
+
/* @__PURE__ */ jsx("h1", { className: "etu-error-page-title", children: title }),
|
|
866
|
+
/* @__PURE__ */ jsx("p", { className: "etu-error-page-body", children: description }),
|
|
867
|
+
(onRetry || onHome) && /* @__PURE__ */ jsxs("div", { className: "etu-error-page-actions", children: [
|
|
868
|
+
onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "etu-error-page-cta", onClick: onRetry, children: retryLabel }),
|
|
869
|
+
onHome && /* @__PURE__ */ jsx("button", { type: "button", className: "etu-error-page-secondary", onClick: onHome, children: homeLabel })
|
|
870
|
+
] }),
|
|
871
|
+
refCode && /* @__PURE__ */ jsxs("p", { className: "etu-error-page-ref", children: [
|
|
872
|
+
/* @__PURE__ */ jsxs("span", { className: "etu-error-page-ref-label", children: [
|
|
873
|
+
refLabel,
|
|
874
|
+
":"
|
|
875
|
+
] }),
|
|
876
|
+
" ",
|
|
877
|
+
/* @__PURE__ */ jsx("code", { className: "etu-error-page-ref-code", children: refCode })
|
|
878
|
+
] })
|
|
879
|
+
] })
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
var defaultSerialize = (v) => JSON.stringify(v);
|
|
884
|
+
var defaultDeserialize = (raw) => JSON.parse(raw);
|
|
885
|
+
function readQueryParam(key) {
|
|
886
|
+
if (typeof window === "undefined") return null;
|
|
887
|
+
const search = new URLSearchParams(window.location.search);
|
|
888
|
+
if (search.has(key)) return search.get(key);
|
|
889
|
+
const hash = window.location.hash;
|
|
890
|
+
const hq = hash.indexOf("?");
|
|
891
|
+
if (hq >= 0) {
|
|
892
|
+
const hashParams = new URLSearchParams(hash.slice(hq + 1));
|
|
893
|
+
if (hashParams.has(key)) return hashParams.get(key);
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
function writeQueryParam(key, value, replace) {
|
|
898
|
+
if (typeof window === "undefined") return;
|
|
899
|
+
const hash = window.location.hash;
|
|
900
|
+
const hq = hash.indexOf("?");
|
|
901
|
+
const useHashQuery = hash.startsWith("#") && hq >= 0;
|
|
902
|
+
if (useHashQuery) {
|
|
903
|
+
const base = hash.slice(0, hq);
|
|
904
|
+
const params2 = new URLSearchParams(hash.slice(hq + 1));
|
|
905
|
+
if (value === null) params2.delete(key);
|
|
906
|
+
else params2.set(key, value);
|
|
907
|
+
const next2 = params2.toString();
|
|
908
|
+
const newHash = next2 ? `${base}?${next2}` : base;
|
|
909
|
+
const url2 = window.location.pathname + window.location.search + newHash;
|
|
910
|
+
apply(url2, replace);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const params = new URLSearchParams(window.location.search);
|
|
914
|
+
if (value === null) params.delete(key);
|
|
915
|
+
else params.set(key, value);
|
|
916
|
+
const next = params.toString();
|
|
917
|
+
const url = window.location.pathname + (next ? "?" + next : "") + window.location.hash;
|
|
918
|
+
apply(url, replace);
|
|
919
|
+
}
|
|
920
|
+
function apply(url, replace) {
|
|
921
|
+
if (replace) window.history.replaceState(window.history.state, "", url);
|
|
922
|
+
else window.history.pushState(null, "", url);
|
|
923
|
+
window.dispatchEvent(new Event("etu:route-state"));
|
|
924
|
+
}
|
|
925
|
+
function useRouteState(key, initial, opts = {}) {
|
|
926
|
+
const serialize = opts.serialize ?? defaultSerialize;
|
|
927
|
+
const deserialize = opts.deserialize ?? defaultDeserialize;
|
|
928
|
+
const replace = opts.replace ?? true;
|
|
929
|
+
const read = useCallback(() => {
|
|
930
|
+
const raw = readQueryParam(key);
|
|
931
|
+
if (raw === null) return initial;
|
|
932
|
+
try {
|
|
933
|
+
return deserialize(raw);
|
|
934
|
+
} catch {
|
|
935
|
+
return initial;
|
|
936
|
+
}
|
|
937
|
+
}, [key, initial, deserialize]);
|
|
938
|
+
const [value, setLocal] = useState(read);
|
|
939
|
+
useEffect(() => {
|
|
940
|
+
setLocal(read());
|
|
941
|
+
const onChange = () => setLocal(read());
|
|
942
|
+
window.addEventListener("popstate", onChange);
|
|
943
|
+
window.addEventListener("hashchange", onChange);
|
|
944
|
+
window.addEventListener("etu:route-state", onChange);
|
|
945
|
+
return () => {
|
|
946
|
+
window.removeEventListener("popstate", onChange);
|
|
947
|
+
window.removeEventListener("hashchange", onChange);
|
|
948
|
+
window.removeEventListener("etu:route-state", onChange);
|
|
949
|
+
};
|
|
950
|
+
}, [read]);
|
|
951
|
+
const set = useCallback(
|
|
952
|
+
(next) => {
|
|
953
|
+
setLocal((prev) => {
|
|
954
|
+
const resolved = typeof next === "function" ? next(prev) : next;
|
|
955
|
+
try {
|
|
956
|
+
writeQueryParam(key, serialize(resolved), replace);
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
return resolved;
|
|
960
|
+
});
|
|
961
|
+
},
|
|
962
|
+
[key, serialize, replace]
|
|
963
|
+
);
|
|
964
|
+
return [value, set];
|
|
965
|
+
}
|
|
966
|
+
function useSessionState(key, initial, opts = {}) {
|
|
967
|
+
const serialize = opts.serialize ?? defaultSerialize;
|
|
968
|
+
const deserialize = opts.deserialize ?? defaultDeserialize;
|
|
969
|
+
const scopeRef = useRef(opts.scope);
|
|
970
|
+
scopeRef.current = opts.scope;
|
|
971
|
+
const storageKey3 = useCallback(() => {
|
|
972
|
+
const scope = scopeRef.current ?? (typeof window !== "undefined" ? window.location.pathname + window.location.hash : "");
|
|
973
|
+
return `etu:ss:${scope}::${key}`;
|
|
974
|
+
}, [key]);
|
|
975
|
+
const read = useCallback(() => {
|
|
976
|
+
if (typeof window === "undefined") return initial;
|
|
977
|
+
try {
|
|
978
|
+
const raw = window.sessionStorage.getItem(storageKey3());
|
|
979
|
+
if (raw === null) return initial;
|
|
980
|
+
return deserialize(raw);
|
|
981
|
+
} catch {
|
|
982
|
+
return initial;
|
|
983
|
+
}
|
|
984
|
+
}, [initial, deserialize, storageKey3]);
|
|
985
|
+
const [value, setLocal] = useState(read);
|
|
986
|
+
useEffect(() => {
|
|
987
|
+
setLocal(read());
|
|
988
|
+
const onNav = () => setLocal(read());
|
|
989
|
+
window.addEventListener("popstate", onNav);
|
|
990
|
+
window.addEventListener("hashchange", onNav);
|
|
991
|
+
window.addEventListener("etu:route-state", onNav);
|
|
992
|
+
return () => {
|
|
993
|
+
window.removeEventListener("popstate", onNav);
|
|
994
|
+
window.removeEventListener("hashchange", onNav);
|
|
995
|
+
window.removeEventListener("etu:route-state", onNav);
|
|
996
|
+
};
|
|
997
|
+
}, [read]);
|
|
998
|
+
const set = useCallback(
|
|
999
|
+
(next) => {
|
|
1000
|
+
setLocal((prev) => {
|
|
1001
|
+
const resolved = typeof next === "function" ? next(prev) : next;
|
|
1002
|
+
try {
|
|
1003
|
+
window.sessionStorage.setItem(storageKey3(), serialize(resolved));
|
|
1004
|
+
} catch {
|
|
1005
|
+
}
|
|
1006
|
+
return resolved;
|
|
1007
|
+
});
|
|
1008
|
+
},
|
|
1009
|
+
[storageKey3, serialize]
|
|
1010
|
+
);
|
|
1011
|
+
return [value, set];
|
|
1012
|
+
}
|
|
1013
|
+
function currentDepth() {
|
|
1014
|
+
if (typeof window === "undefined") return 0;
|
|
1015
|
+
const s = window.history.state ?? {};
|
|
1016
|
+
return s.etuInApp ? s.etuDepth ?? 0 : 0;
|
|
1017
|
+
}
|
|
1018
|
+
function writeEntry(url, depth, replace) {
|
|
1019
|
+
if (typeof window === "undefined") return;
|
|
1020
|
+
const next = { etuInApp: true, etuDepth: depth };
|
|
1021
|
+
if (replace) window.history.replaceState(next, "", url);
|
|
1022
|
+
else window.history.pushState(next, "", url);
|
|
1023
|
+
window.dispatchEvent(new Event("etu:in-app-nav"));
|
|
1024
|
+
}
|
|
1025
|
+
function runInAppBackFallback(fallback) {
|
|
1026
|
+
if (typeof window === "undefined") return;
|
|
1027
|
+
if (typeof fallback === "function") {
|
|
1028
|
+
fallback();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
window.history.pushState(null, "", fallback);
|
|
1032
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
1033
|
+
}
|
|
1034
|
+
function useInAppBack(opts = {}) {
|
|
1035
|
+
const { fallback, onExit } = opts;
|
|
1036
|
+
const [depth, setDepth] = useState(() => currentDepth());
|
|
1037
|
+
useEffect(() => {
|
|
1038
|
+
if (typeof window === "undefined") return;
|
|
1039
|
+
const s = window.history.state ?? {};
|
|
1040
|
+
if (!s.etuInApp) {
|
|
1041
|
+
window.history.replaceState(
|
|
1042
|
+
{ ...s, etuInApp: true, etuDepth: 0 },
|
|
1043
|
+
"",
|
|
1044
|
+
window.location.href
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
const sync = () => setDepth(currentDepth());
|
|
1048
|
+
sync();
|
|
1049
|
+
window.addEventListener("popstate", sync);
|
|
1050
|
+
window.addEventListener("etu:in-app-nav", sync);
|
|
1051
|
+
return () => {
|
|
1052
|
+
window.removeEventListener("popstate", sync);
|
|
1053
|
+
window.removeEventListener("etu:in-app-nav", sync);
|
|
1054
|
+
};
|
|
1055
|
+
}, []);
|
|
1056
|
+
const push = useCallback((url) => {
|
|
1057
|
+
writeEntry(url, currentDepth() + 1, false);
|
|
1058
|
+
}, []);
|
|
1059
|
+
const replace = useCallback((url) => {
|
|
1060
|
+
writeEntry(url, currentDepth(), true);
|
|
1061
|
+
}, []);
|
|
1062
|
+
const goBack = useCallback(() => {
|
|
1063
|
+
if (currentDepth() > 0) {
|
|
1064
|
+
window.history.back();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (fallback !== void 0) {
|
|
1068
|
+
runInAppBackFallback(fallback);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (onExit) onExit();
|
|
1072
|
+
}, [fallback, onExit]);
|
|
1073
|
+
return { canGoBack: depth > 0, goBack, push, replace };
|
|
1074
|
+
}
|
|
1075
|
+
function DefaultIcon3() {
|
|
1076
|
+
return /* @__PURE__ */ jsx(
|
|
1077
|
+
"svg",
|
|
1078
|
+
{
|
|
1079
|
+
viewBox: "0 0 24 24",
|
|
1080
|
+
width: "16",
|
|
1081
|
+
height: "16",
|
|
1082
|
+
fill: "none",
|
|
1083
|
+
stroke: "currentColor",
|
|
1084
|
+
strokeWidth: "2",
|
|
1085
|
+
strokeLinecap: "round",
|
|
1086
|
+
strokeLinejoin: "round",
|
|
1087
|
+
"aria-hidden": "true",
|
|
1088
|
+
children: /* @__PURE__ */ jsx("path", { d: "M15 18l-6-6 6-6" })
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
function BackButton({
|
|
1093
|
+
canGoBack,
|
|
1094
|
+
goBack,
|
|
1095
|
+
onClick,
|
|
1096
|
+
fallback,
|
|
1097
|
+
label = "\uB4A4\uB85C",
|
|
1098
|
+
icon,
|
|
1099
|
+
className,
|
|
1100
|
+
alwaysShow
|
|
1101
|
+
}) {
|
|
1102
|
+
const internal = useInAppBack({ fallback });
|
|
1103
|
+
const effectiveCanGoBack = canGoBack ?? internal.canGoBack;
|
|
1104
|
+
const handler = onClick ?? goBack ?? internal.goBack;
|
|
1105
|
+
const visible = alwaysShow || effectiveCanGoBack || !!onClick || fallback !== void 0;
|
|
1106
|
+
if (!visible) return null;
|
|
1107
|
+
return /* @__PURE__ */ jsxs(
|
|
1108
|
+
"button",
|
|
1109
|
+
{
|
|
1110
|
+
type: "button",
|
|
1111
|
+
className: "etu-back-button" + (className ? " " + className : ""),
|
|
1112
|
+
onClick: handler,
|
|
1113
|
+
"aria-label": label,
|
|
1114
|
+
children: [
|
|
1115
|
+
/* @__PURE__ */ jsx("span", { className: "etu-back-button-icon", children: icon ?? /* @__PURE__ */ jsx(DefaultIcon3, {}) }),
|
|
1116
|
+
/* @__PURE__ */ jsx("span", { className: "etu-back-button-label", children: label })
|
|
1117
|
+
]
|
|
1118
|
+
}
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/createFetch.ts
|
|
1123
|
+
var HttpError = class extends Error {
|
|
1124
|
+
constructor(status, url, body) {
|
|
1125
|
+
const msg = typeof body === "object" && body?.error || `HTTP ${status}` + (typeof body === "object" && body?.ref ? ` (ref=${body.ref})` : "");
|
|
1126
|
+
super(msg);
|
|
1127
|
+
this.name = "HttpError";
|
|
1128
|
+
this.status = status;
|
|
1129
|
+
this.url = url;
|
|
1130
|
+
this.body = body;
|
|
1131
|
+
if (typeof body === "object" && body?.ref) this.ref = body.ref;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
function defaultAuthRedirect() {
|
|
1135
|
+
if (typeof window === "undefined") return;
|
|
1136
|
+
const rd = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash);
|
|
1137
|
+
window.location.href = "/oauth2/start?rd=" + rd;
|
|
1138
|
+
}
|
|
1139
|
+
function buildUrl(base, path, query) {
|
|
1140
|
+
let url = path;
|
|
1141
|
+
if (base && !/^https?:/.test(path)) url = base.replace(/\/$/, "") + (path.startsWith("/") ? path : "/" + path);
|
|
1142
|
+
if (query) {
|
|
1143
|
+
const params = new URLSearchParams();
|
|
1144
|
+
for (const [k, v] of Object.entries(query)) {
|
|
1145
|
+
if (v === void 0 || v === null) continue;
|
|
1146
|
+
params.set(k, String(v));
|
|
1147
|
+
}
|
|
1148
|
+
const q = params.toString();
|
|
1149
|
+
if (q) url += (url.includes("?") ? "&" : "?") + q;
|
|
1150
|
+
}
|
|
1151
|
+
return url;
|
|
1152
|
+
}
|
|
1153
|
+
function isPlainObject(v) {
|
|
1154
|
+
if (v === null || typeof v !== "object") return false;
|
|
1155
|
+
const proto = Object.getPrototypeOf(v);
|
|
1156
|
+
return proto === Object.prototype || proto === null;
|
|
1157
|
+
}
|
|
1158
|
+
function createFetch(opts = {}) {
|
|
1159
|
+
const base = opts.baseUrl ?? "";
|
|
1160
|
+
const onAuthError = opts.onAuthError ?? defaultAuthRedirect;
|
|
1161
|
+
const onError = opts.onError;
|
|
1162
|
+
const fetchImpl = opts.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
1163
|
+
async function request(method, path, ro = {}) {
|
|
1164
|
+
if (!fetchImpl) throw new Error("createFetch: no fetch impl available");
|
|
1165
|
+
const url = buildUrl(base, path, ro.query);
|
|
1166
|
+
const headers = new Headers();
|
|
1167
|
+
const factory = typeof opts.headers === "function" ? opts.headers() : opts.headers;
|
|
1168
|
+
if (factory) new Headers(factory).forEach((v, k) => headers.set(k, v));
|
|
1169
|
+
if (ro.headers) new Headers(ro.headers).forEach((v, k) => headers.set(k, v));
|
|
1170
|
+
let body;
|
|
1171
|
+
if (ro.body !== void 0 && ro.body !== null) {
|
|
1172
|
+
if (isPlainObject(ro.body) || Array.isArray(ro.body)) {
|
|
1173
|
+
body = JSON.stringify(ro.body);
|
|
1174
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
1175
|
+
} else {
|
|
1176
|
+
body = ro.body;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
1180
|
+
const res = await fetchImpl(url, { method, headers, body, signal: ro.signal, credentials: "same-origin" });
|
|
1181
|
+
if (res.status === 401) {
|
|
1182
|
+
onAuthError();
|
|
1183
|
+
const err = new HttpError(401, url, await safeParse(res));
|
|
1184
|
+
if (onError) onError(err);
|
|
1185
|
+
throw err;
|
|
1186
|
+
}
|
|
1187
|
+
if (!res.ok) {
|
|
1188
|
+
const parsed = await safeParse(res);
|
|
1189
|
+
const err = new HttpError(res.status, url, parsed);
|
|
1190
|
+
if (onError) onError(err);
|
|
1191
|
+
throw err;
|
|
1192
|
+
}
|
|
1193
|
+
if (ro.raw) return res;
|
|
1194
|
+
if (res.status === 204 || res.headers.get("Content-Length") === "0") {
|
|
1195
|
+
return void 0;
|
|
1196
|
+
}
|
|
1197
|
+
const ct = res.headers.get("Content-Type") || "";
|
|
1198
|
+
if (ct.includes("application/json")) return await res.json();
|
|
1199
|
+
return await res.text();
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
request,
|
|
1203
|
+
get: (p, o) => request("GET", p, o),
|
|
1204
|
+
post: (p, body, o) => request("POST", p, { ...o, body }),
|
|
1205
|
+
put: (p, body, o) => request("PUT", p, { ...o, body }),
|
|
1206
|
+
patch: (p, body, o) => request("PATCH", p, { ...o, body }),
|
|
1207
|
+
delete: (p, o) => request("DELETE", p, o)
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
async function safeParse(res) {
|
|
1211
|
+
try {
|
|
1212
|
+
const ct = res.headers.get("Content-Type") || "";
|
|
1213
|
+
if (ct.includes("application/json")) return await res.json();
|
|
1214
|
+
const text = await res.text();
|
|
1215
|
+
return text || void 0;
|
|
1216
|
+
} catch {
|
|
1217
|
+
return void 0;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
var REFRESH_EVENT = "etu:me-refresh";
|
|
1221
|
+
async function defaultFetcher(endpoint, treat401) {
|
|
1222
|
+
if (typeof fetch === "undefined") throw new Error("useMe: no fetch impl available");
|
|
1223
|
+
const res = await fetch(endpoint, {
|
|
1224
|
+
credentials: "same-origin",
|
|
1225
|
+
headers: { Accept: "application/json" }
|
|
1226
|
+
});
|
|
1227
|
+
if (res.status === 401 && treat401) return null;
|
|
1228
|
+
if (!res.ok) throw new Error("useMe: " + res.status);
|
|
1229
|
+
return await res.json();
|
|
1230
|
+
}
|
|
1231
|
+
function useMe(opts = {}) {
|
|
1232
|
+
const { endpoint = "/api/me", fetcher, treat401AsAnonymous = true } = opts;
|
|
1233
|
+
const [me, setMe] = useState(null);
|
|
1234
|
+
const [loading, setLoading] = useState(true);
|
|
1235
|
+
const [error, setError] = useState(null);
|
|
1236
|
+
const fetchMe = useCallback(async () => {
|
|
1237
|
+
setLoading(true);
|
|
1238
|
+
setError(null);
|
|
1239
|
+
try {
|
|
1240
|
+
const value = fetcher ? await fetcher() : await defaultFetcher(endpoint, treat401AsAnonymous);
|
|
1241
|
+
setMe(value);
|
|
1242
|
+
} catch (e) {
|
|
1243
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
1244
|
+
} finally {
|
|
1245
|
+
setLoading(false);
|
|
1246
|
+
}
|
|
1247
|
+
}, [endpoint, fetcher, treat401AsAnonymous]);
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
fetchMe();
|
|
1250
|
+
if (typeof window === "undefined") return;
|
|
1251
|
+
const onRefresh = () => fetchMe();
|
|
1252
|
+
window.addEventListener(REFRESH_EVENT, onRefresh);
|
|
1253
|
+
return () => window.removeEventListener(REFRESH_EVENT, onRefresh);
|
|
1254
|
+
}, [fetchMe]);
|
|
1255
|
+
const refresh = useCallback(() => {
|
|
1256
|
+
if (typeof window !== "undefined") {
|
|
1257
|
+
window.dispatchEvent(new Event(REFRESH_EVENT));
|
|
1258
|
+
} else {
|
|
1259
|
+
fetchMe();
|
|
1260
|
+
}
|
|
1261
|
+
}, [fetchMe]);
|
|
1262
|
+
return { me, loading, error, refresh };
|
|
1263
|
+
}
|
|
1264
|
+
function currentRd() {
|
|
1265
|
+
if (typeof window === "undefined") return "/";
|
|
1266
|
+
return window.location.pathname + window.location.search + window.location.hash;
|
|
1267
|
+
}
|
|
1268
|
+
function signInUrl(rd) {
|
|
1269
|
+
return "/oauth2/start?rd=" + encodeURIComponent(rd ?? currentRd());
|
|
1270
|
+
}
|
|
1271
|
+
function signOutUrl(rd) {
|
|
1272
|
+
return "/oauth2/sign_out?rd=" + encodeURIComponent(rd ?? "/");
|
|
1273
|
+
}
|
|
1274
|
+
function signIn(rd) {
|
|
1275
|
+
if (typeof window !== "undefined") window.location.href = signInUrl(rd);
|
|
1276
|
+
}
|
|
1277
|
+
function signOut(rd) {
|
|
1278
|
+
if (typeof window !== "undefined") window.location.href = signOutUrl(rd);
|
|
1279
|
+
}
|
|
1280
|
+
function DefaultIcon4() {
|
|
1281
|
+
return /* @__PURE__ */ jsxs(
|
|
1282
|
+
"svg",
|
|
1283
|
+
{
|
|
1284
|
+
viewBox: "0 0 24 24",
|
|
1285
|
+
width: "38",
|
|
1286
|
+
height: "38",
|
|
1287
|
+
fill: "none",
|
|
1288
|
+
stroke: "currentColor",
|
|
1289
|
+
strokeWidth: "1.5",
|
|
1290
|
+
strokeLinecap: "round",
|
|
1291
|
+
strokeLinejoin: "round",
|
|
1292
|
+
"aria-hidden": "true",
|
|
1293
|
+
children: [
|
|
1294
|
+
/* @__PURE__ */ jsx("path", { d: "M3 7l9-4 9 4" }),
|
|
1295
|
+
/* @__PURE__ */ jsx("path", { d: "M3 7v10l9 4 9-4V7" }),
|
|
1296
|
+
/* @__PURE__ */ jsx("path", { d: "M12 11v10" }),
|
|
1297
|
+
/* @__PURE__ */ jsx("path", { d: "M3 7l9 4 9-4" })
|
|
1298
|
+
]
|
|
1299
|
+
}
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
function EmptyState({
|
|
1303
|
+
title,
|
|
1304
|
+
description,
|
|
1305
|
+
action,
|
|
1306
|
+
icon,
|
|
1307
|
+
compact,
|
|
1308
|
+
className
|
|
1309
|
+
}) {
|
|
1310
|
+
const cls = [
|
|
1311
|
+
"etu-empty-state",
|
|
1312
|
+
compact ? "etu-empty-state--compact" : "",
|
|
1313
|
+
className ?? ""
|
|
1314
|
+
].filter(Boolean).join(" ");
|
|
1315
|
+
const showIcon = icon !== null;
|
|
1316
|
+
return /* @__PURE__ */ jsxs("div", { role: "status", className: cls, children: [
|
|
1317
|
+
showIcon && /* @__PURE__ */ jsx("div", { className: "etu-empty-state-icon", children: icon ?? /* @__PURE__ */ jsx(DefaultIcon4, {}) }),
|
|
1318
|
+
/* @__PURE__ */ jsx("div", { className: "etu-empty-state-title", children: title }),
|
|
1319
|
+
description && /* @__PURE__ */ jsx("div", { className: "etu-empty-state-body", children: description }),
|
|
1320
|
+
action && /* @__PURE__ */ jsx("div", { className: "etu-empty-state-actions", children: action })
|
|
1321
|
+
] });
|
|
1322
|
+
}
|
|
1323
|
+
async function writeClipboard(value) {
|
|
1324
|
+
if (typeof window === "undefined") return false;
|
|
1325
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
1326
|
+
try {
|
|
1327
|
+
await navigator.clipboard.writeText(value);
|
|
1328
|
+
return true;
|
|
1329
|
+
} catch {
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
const ta = document.createElement("textarea");
|
|
1334
|
+
ta.value = value;
|
|
1335
|
+
ta.setAttribute("readonly", "");
|
|
1336
|
+
ta.style.position = "absolute";
|
|
1337
|
+
ta.style.left = "-9999px";
|
|
1338
|
+
document.body.appendChild(ta);
|
|
1339
|
+
ta.select();
|
|
1340
|
+
const ok = document.execCommand("copy");
|
|
1341
|
+
document.body.removeChild(ta);
|
|
1342
|
+
return ok;
|
|
1343
|
+
} catch {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function useClipboard(opts = {}) {
|
|
1348
|
+
const { resetMs = 1500, toastOnSuccess = "\uBCF5\uC0AC\uB428", toastOnError = "\uBCF5\uC0AC \uC2E4\uD328" } = opts;
|
|
1349
|
+
const [copied, setCopied] = useState(false);
|
|
1350
|
+
const copy = useCallback(
|
|
1351
|
+
async (value) => {
|
|
1352
|
+
const ok = await writeClipboard(value);
|
|
1353
|
+
if (ok) {
|
|
1354
|
+
setCopied(true);
|
|
1355
|
+
if (toastOnSuccess) toast(toastOnSuccess, "ok");
|
|
1356
|
+
window.setTimeout(() => setCopied(false), resetMs);
|
|
1357
|
+
} else {
|
|
1358
|
+
if (toastOnError) toast(toastOnError, "err");
|
|
1359
|
+
}
|
|
1360
|
+
return ok;
|
|
1361
|
+
},
|
|
1362
|
+
[resetMs, toastOnSuccess, toastOnError]
|
|
1363
|
+
);
|
|
1364
|
+
return { copied, copy };
|
|
1365
|
+
}
|
|
1366
|
+
function DefaultCopyIcon() {
|
|
1367
|
+
return /* @__PURE__ */ jsxs(
|
|
1368
|
+
"svg",
|
|
1369
|
+
{
|
|
1370
|
+
viewBox: "0 0 24 24",
|
|
1371
|
+
width: "14",
|
|
1372
|
+
height: "14",
|
|
1373
|
+
fill: "none",
|
|
1374
|
+
stroke: "currentColor",
|
|
1375
|
+
strokeWidth: "2",
|
|
1376
|
+
strokeLinecap: "round",
|
|
1377
|
+
strokeLinejoin: "round",
|
|
1378
|
+
"aria-hidden": "true",
|
|
1379
|
+
children: [
|
|
1380
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
|
|
1381
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
1382
|
+
]
|
|
1383
|
+
}
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
function DefaultCheckIcon() {
|
|
1387
|
+
return /* @__PURE__ */ jsx(
|
|
1388
|
+
"svg",
|
|
1389
|
+
{
|
|
1390
|
+
viewBox: "0 0 24 24",
|
|
1391
|
+
width: "14",
|
|
1392
|
+
height: "14",
|
|
1393
|
+
fill: "none",
|
|
1394
|
+
stroke: "currentColor",
|
|
1395
|
+
strokeWidth: "2",
|
|
1396
|
+
strokeLinecap: "round",
|
|
1397
|
+
strokeLinejoin: "round",
|
|
1398
|
+
"aria-hidden": "true",
|
|
1399
|
+
children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" })
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
function CopyButton({
|
|
1404
|
+
value,
|
|
1405
|
+
label = "\uBCF5\uC0AC",
|
|
1406
|
+
successLabel = "\uBCF5\uC0AC\uB428",
|
|
1407
|
+
icon,
|
|
1408
|
+
className,
|
|
1409
|
+
iconOnly,
|
|
1410
|
+
ariaLabel,
|
|
1411
|
+
resetMs,
|
|
1412
|
+
toastOnSuccess,
|
|
1413
|
+
toastOnError
|
|
1414
|
+
}) {
|
|
1415
|
+
const { copied, copy } = useClipboard({ resetMs, toastOnSuccess, toastOnError });
|
|
1416
|
+
const cls = [
|
|
1417
|
+
"etu-copy-button",
|
|
1418
|
+
copied ? "etu-copy-button--copied" : "",
|
|
1419
|
+
iconOnly ? "etu-copy-button--icon-only" : "",
|
|
1420
|
+
className ?? ""
|
|
1421
|
+
].filter(Boolean).join(" ");
|
|
1422
|
+
const showIcon = icon !== null;
|
|
1423
|
+
return /* @__PURE__ */ jsxs(
|
|
1424
|
+
"button",
|
|
1425
|
+
{
|
|
1426
|
+
type: "button",
|
|
1427
|
+
className: cls,
|
|
1428
|
+
onClick: () => copy(value),
|
|
1429
|
+
"aria-label": iconOnly ? ariaLabel ?? label : void 0,
|
|
1430
|
+
"data-copied": copied || void 0,
|
|
1431
|
+
children: [
|
|
1432
|
+
showIcon && /* @__PURE__ */ jsx("span", { className: "etu-copy-button-icon", children: icon ?? (copied ? /* @__PURE__ */ jsx(DefaultCheckIcon, {}) : /* @__PURE__ */ jsx(DefaultCopyIcon, {})) }),
|
|
1433
|
+
!iconOnly && /* @__PURE__ */ jsx("span", { className: "etu-copy-button-label", children: copied ? successLabel : label })
|
|
1434
|
+
]
|
|
1435
|
+
}
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
function DefaultExternalIcon() {
|
|
1439
|
+
return /* @__PURE__ */ jsxs(
|
|
1440
|
+
"svg",
|
|
1441
|
+
{
|
|
1442
|
+
viewBox: "0 0 24 24",
|
|
1443
|
+
width: "14",
|
|
1444
|
+
height: "14",
|
|
1445
|
+
fill: "none",
|
|
1446
|
+
stroke: "currentColor",
|
|
1447
|
+
strokeWidth: "2",
|
|
1448
|
+
strokeLinecap: "round",
|
|
1449
|
+
strokeLinejoin: "round",
|
|
1450
|
+
"aria-hidden": "true",
|
|
1451
|
+
children: [
|
|
1452
|
+
/* @__PURE__ */ jsx("path", { d: "M14 3h7v7" }),
|
|
1453
|
+
/* @__PURE__ */ jsx("path", { d: "M10 14L21 3" }),
|
|
1454
|
+
/* @__PURE__ */ jsx("path", { d: "M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" })
|
|
1455
|
+
]
|
|
1456
|
+
}
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
function OpenInBrowserButton({
|
|
1460
|
+
href,
|
|
1461
|
+
label = "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uAE30",
|
|
1462
|
+
icon,
|
|
1463
|
+
iconOnly,
|
|
1464
|
+
ariaLabel,
|
|
1465
|
+
variant = "ghost",
|
|
1466
|
+
className,
|
|
1467
|
+
...rest
|
|
1468
|
+
}) {
|
|
1469
|
+
const cls = [
|
|
1470
|
+
"etu-open-in-browser-button",
|
|
1471
|
+
`etu-open-in-browser-button--${variant}`,
|
|
1472
|
+
iconOnly ? "etu-open-in-browser-button--icon-only" : "",
|
|
1473
|
+
className ?? ""
|
|
1474
|
+
].filter(Boolean).join(" ");
|
|
1475
|
+
const showIcon = icon !== null;
|
|
1476
|
+
return /* @__PURE__ */ jsxs(
|
|
1477
|
+
"a",
|
|
1478
|
+
{
|
|
1479
|
+
...rest,
|
|
1480
|
+
href,
|
|
1481
|
+
target: "_blank",
|
|
1482
|
+
rel: "noopener noreferrer",
|
|
1483
|
+
className: cls,
|
|
1484
|
+
"aria-label": iconOnly ? ariaLabel ?? label : rest["aria-label"],
|
|
1485
|
+
children: [
|
|
1486
|
+
showIcon && /* @__PURE__ */ jsx("span", { className: "etu-open-in-browser-button-icon", children: icon ?? /* @__PURE__ */ jsx(DefaultExternalIcon, {}) }),
|
|
1487
|
+
!iconOnly && /* @__PURE__ */ jsx("span", { className: "etu-open-in-browser-button-label", children: label })
|
|
1488
|
+
]
|
|
1489
|
+
}
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/serviceWorker.ts
|
|
1494
|
+
var DEFAULT_INTERVAL_MS = 2 * 60 * 1e3;
|
|
1495
|
+
var DEFAULT_TOAST_TEXT = "\uC0C8 \uBC84\uC804\uC774 \uC900\uBE44\uB410\uC5B4\uC694. \uC0C8\uB85C\uACE0\uCE68\uD560\uAE4C\uC694?";
|
|
1496
|
+
function isDevBuild() {
|
|
1497
|
+
try {
|
|
1498
|
+
const proc = globalThis.process;
|
|
1499
|
+
if (proc?.env?.NODE_ENV === "production") return false;
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
function hashStr(s) {
|
|
1505
|
+
let h = 2166136261;
|
|
1506
|
+
for (let i = 0; i < s.length; i++) {
|
|
1507
|
+
h ^= s.charCodeAt(i);
|
|
1508
|
+
h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
1509
|
+
}
|
|
1510
|
+
return h.toString(16);
|
|
1511
|
+
}
|
|
1512
|
+
var SW_STALENESS_STORAGE = "@etamong-playground/ui:sw-staleness";
|
|
1513
|
+
async function assertSwChangesAcrossBuilds(url, currentBuild) {
|
|
1514
|
+
let body;
|
|
1515
|
+
try {
|
|
1516
|
+
const res = await fetch(url, { cache: "no-store", credentials: "same-origin" });
|
|
1517
|
+
if (!res.ok) return;
|
|
1518
|
+
body = await res.text();
|
|
1519
|
+
} catch {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const swHash = hashStr(body);
|
|
1523
|
+
let prev = null;
|
|
1524
|
+
try {
|
|
1525
|
+
const raw = localStorage.getItem(SW_STALENESS_STORAGE);
|
|
1526
|
+
if (raw) prev = JSON.parse(raw);
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
if (prev && prev.url === url && prev.build !== currentBuild && prev.swHash === swHash) {
|
|
1530
|
+
console.warn(
|
|
1531
|
+
`[@etamong-playground/ui] registerServiceWorker: build SHA changed (${prev.build.slice(0, 8)} \u2192 ${currentBuild.slice(0, 8)}) but ${url} is byte-identical. Browsers won't roll over to the new SW \u2014 installed PWAs (esp. iOS) will keep serving the old shell. Stamp the SW source with the build SHA (Vite: __BUILD_ID__ + define; static sw.js: generate at build time). See wiki/concepts/pwa-cache-and-ios-shell.`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
localStorage.setItem(
|
|
1536
|
+
SW_STALENESS_STORAGE,
|
|
1537
|
+
JSON.stringify({
|
|
1538
|
+
url,
|
|
1539
|
+
swHash,
|
|
1540
|
+
build: currentBuild,
|
|
1541
|
+
observedAt: Date.now()
|
|
1542
|
+
})
|
|
1543
|
+
);
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function registerServiceWorker(url, opts = {}) {
|
|
1548
|
+
const {
|
|
1549
|
+
notifyOnUpdate = true,
|
|
1550
|
+
autoReloadOnUpdate = false,
|
|
1551
|
+
updateToastText = DEFAULT_TOAST_TEXT,
|
|
1552
|
+
updateIntervalMs = DEFAULT_INTERVAL_MS,
|
|
1553
|
+
registerOptions,
|
|
1554
|
+
onActivate,
|
|
1555
|
+
currentBuild
|
|
1556
|
+
} = opts;
|
|
1557
|
+
let registration = null;
|
|
1558
|
+
let hasUpdate = false;
|
|
1559
|
+
let intervalId;
|
|
1560
|
+
let reloading = false;
|
|
1561
|
+
const handle = {
|
|
1562
|
+
get registration() {
|
|
1563
|
+
return registration;
|
|
1564
|
+
},
|
|
1565
|
+
get hasUpdate() {
|
|
1566
|
+
return hasUpdate;
|
|
1567
|
+
},
|
|
1568
|
+
applyUpdate() {
|
|
1569
|
+
const waiting = registration?.waiting;
|
|
1570
|
+
if (waiting) waiting.postMessage({ type: "SKIP_WAITING" });
|
|
1571
|
+
},
|
|
1572
|
+
async checkForUpdate() {
|
|
1573
|
+
if (registration) await registration.update();
|
|
1574
|
+
},
|
|
1575
|
+
async unregister() {
|
|
1576
|
+
if (!registration) return false;
|
|
1577
|
+
const ok = await registration.unregister();
|
|
1578
|
+
registration = null;
|
|
1579
|
+
if (intervalId) window.clearInterval(intervalId);
|
|
1580
|
+
return ok;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
|
|
1584
|
+
return handle;
|
|
1585
|
+
}
|
|
1586
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
1587
|
+
if (reloading) return;
|
|
1588
|
+
reloading = true;
|
|
1589
|
+
onActivate?.();
|
|
1590
|
+
window.location.reload();
|
|
1591
|
+
});
|
|
1592
|
+
function watchWaiting(reg) {
|
|
1593
|
+
const waiting = reg.waiting;
|
|
1594
|
+
if (!waiting || hasUpdate) return;
|
|
1595
|
+
hasUpdate = true;
|
|
1596
|
+
if (autoReloadOnUpdate) {
|
|
1597
|
+
waiting.postMessage({ type: "SKIP_WAITING" });
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (notifyOnUpdate) {
|
|
1601
|
+
toast(updateToastText, "info", 1e4);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (isDevBuild() && currentBuild) {
|
|
1605
|
+
void assertSwChangesAcrossBuilds(url, currentBuild);
|
|
1606
|
+
}
|
|
1607
|
+
function start() {
|
|
1608
|
+
navigator.serviceWorker.register(url, registerOptions).then((reg) => {
|
|
1609
|
+
registration = reg;
|
|
1610
|
+
watchWaiting(reg);
|
|
1611
|
+
reg.addEventListener("updatefound", () => {
|
|
1612
|
+
const installing = reg.installing;
|
|
1613
|
+
if (!installing) return;
|
|
1614
|
+
installing.addEventListener("statechange", () => {
|
|
1615
|
+
if (installing.state === "installed" && navigator.serviceWorker.controller) {
|
|
1616
|
+
watchWaiting(reg);
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
void reg.update();
|
|
1621
|
+
document.addEventListener("visibilitychange", () => {
|
|
1622
|
+
if (document.visibilityState === "visible") void reg.update();
|
|
1623
|
+
});
|
|
1624
|
+
if (updateIntervalMs > 0) {
|
|
1625
|
+
intervalId = window.setInterval(() => {
|
|
1626
|
+
void reg.update();
|
|
1627
|
+
}, updateIntervalMs);
|
|
1628
|
+
}
|
|
1629
|
+
}).catch(() => {
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
if (document.readyState === "complete") start();
|
|
1633
|
+
else window.addEventListener("load", start, { once: true });
|
|
1634
|
+
return handle;
|
|
1635
|
+
}
|
|
1636
|
+
function networkFirstSwSource(opts) {
|
|
1637
|
+
const { version, networkTimeoutMs = 3e3, passThroughPrefixes = [] } = opts;
|
|
1638
|
+
if (!version) throw new Error("networkFirstSwSource: version is required");
|
|
1639
|
+
const passList = JSON.stringify(["/api/", ...passThroughPrefixes]);
|
|
1640
|
+
const versionStr = JSON.stringify(version);
|
|
1641
|
+
const timeout = Number(networkTimeoutMs) || 3e3;
|
|
1642
|
+
return `// Generated by @etamong-playground/ui networkFirstSwSource \u2014 see
|
|
1643
|
+
// planning concepts/pwa-service-worker. Online-first; cache only as
|
|
1644
|
+
// offline fallback. Edit the source, not this file.
|
|
1645
|
+
|
|
1646
|
+
const VERSION = ${versionStr};
|
|
1647
|
+
const NAV_CACHE = "etu-nav-" + VERSION;
|
|
1648
|
+
const ASSET_CACHE = "etu-asset-" + VERSION;
|
|
1649
|
+
const PASS_THROUGH = ${passList};
|
|
1650
|
+
const NET_TIMEOUT = ${timeout};
|
|
1651
|
+
|
|
1652
|
+
self.addEventListener("install", (event) => {
|
|
1653
|
+
// Take over on the next nav as soon as we're installed.
|
|
1654
|
+
self.skipWaiting();
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
self.addEventListener("activate", (event) => {
|
|
1658
|
+
event.waitUntil((async () => {
|
|
1659
|
+
const keys = await caches.keys();
|
|
1660
|
+
await Promise.all(
|
|
1661
|
+
keys
|
|
1662
|
+
.filter((k) => k !== NAV_CACHE && k !== ASSET_CACHE)
|
|
1663
|
+
.map((k) => caches.delete(k)),
|
|
1664
|
+
);
|
|
1665
|
+
await self.clients.claim();
|
|
1666
|
+
})());
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
self.addEventListener("message", (event) => {
|
|
1670
|
+
// Triggered by registerServiceWorker.applyUpdate().
|
|
1671
|
+
if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting();
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
function shouldPassThrough(url) {
|
|
1675
|
+
if (PASS_THROUGH.some((p) => url.pathname.startsWith(p))) return true;
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
async function networkFirst(req, cacheName) {
|
|
1680
|
+
const cache = await caches.open(cacheName);
|
|
1681
|
+
let timeoutId;
|
|
1682
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1683
|
+
timeoutId = setTimeout(() => reject(new Error("timeout")), NET_TIMEOUT);
|
|
1684
|
+
});
|
|
1685
|
+
try {
|
|
1686
|
+
const res = await Promise.race([fetch(req), timeoutPromise]);
|
|
1687
|
+
clearTimeout(timeoutId);
|
|
1688
|
+
if (res && res.ok && res.type !== "opaqueredirect") {
|
|
1689
|
+
cache.put(req, res.clone());
|
|
1690
|
+
}
|
|
1691
|
+
return res;
|
|
1692
|
+
} catch (_) {
|
|
1693
|
+
clearTimeout(timeoutId);
|
|
1694
|
+
const cached = await cache.match(req);
|
|
1695
|
+
if (cached) return cached;
|
|
1696
|
+
throw _;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
self.addEventListener("fetch", (event) => {
|
|
1701
|
+
const req = event.request;
|
|
1702
|
+
if (req.method !== "GET") return;
|
|
1703
|
+
const url = new URL(req.url);
|
|
1704
|
+
if (url.origin !== self.location.origin) return;
|
|
1705
|
+
if (shouldPassThrough(url)) return;
|
|
1706
|
+
|
|
1707
|
+
if (req.mode === "navigate") {
|
|
1708
|
+
event.respondWith(networkFirst(req, NAV_CACHE));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
// Same-origin GET asset.
|
|
1712
|
+
event.respondWith(networkFirst(req, ASSET_CACHE));
|
|
1713
|
+
});
|
|
1714
|
+
`;
|
|
1715
|
+
}
|
|
1716
|
+
function isAdminLike(input) {
|
|
1717
|
+
const { me, emails, roles, predicate } = input;
|
|
1718
|
+
if (!me) return false;
|
|
1719
|
+
if (me.is_admin) return true;
|
|
1720
|
+
if (emails && me.email && emails.some((e) => e.toLowerCase() === me.email.toLowerCase())) {
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
if (roles && me.roles && me.roles.some((r) => roles.includes(r))) {
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
if (predicate && predicate(me)) return true;
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
function AdminGate(props) {
|
|
1730
|
+
const { children, fallback = null, ...check } = props;
|
|
1731
|
+
return /* @__PURE__ */ jsx(Fragment, { children: isAdminLike(check) ? children : fallback });
|
|
1732
|
+
}
|
|
1733
|
+
function AdminBadge({ label = "\uAD00\uB9AC\uC790 \uC804\uC6A9", className }) {
|
|
1734
|
+
return /* @__PURE__ */ jsxs(
|
|
1735
|
+
"span",
|
|
1736
|
+
{
|
|
1737
|
+
className: "etu-admin-badge" + (className ? " " + className : ""),
|
|
1738
|
+
title: label,
|
|
1739
|
+
children: [
|
|
1740
|
+
/* @__PURE__ */ jsx(DefaultLockIcon, {}),
|
|
1741
|
+
label
|
|
1742
|
+
]
|
|
1743
|
+
}
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
function BackofficeLayout({
|
|
1747
|
+
title,
|
|
1748
|
+
description,
|
|
1749
|
+
actions,
|
|
1750
|
+
badge,
|
|
1751
|
+
children,
|
|
1752
|
+
className
|
|
1753
|
+
}) {
|
|
1754
|
+
return /* @__PURE__ */ jsxs("div", { className: "etu-backoffice" + (className ? " " + className : ""), children: [
|
|
1755
|
+
/* @__PURE__ */ jsxs("header", { className: "etu-backoffice-head", children: [
|
|
1756
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-backoffice-head-text", children: [
|
|
1757
|
+
/* @__PURE__ */ jsxs("h1", { className: "etu-backoffice-title", children: [
|
|
1758
|
+
title,
|
|
1759
|
+
badge === void 0 ? /* @__PURE__ */ jsx(AdminBadge, {}) : badge
|
|
1760
|
+
] }),
|
|
1761
|
+
description && /* @__PURE__ */ jsx("p", { className: "etu-backoffice-description", children: description })
|
|
1762
|
+
] }),
|
|
1763
|
+
actions && /* @__PURE__ */ jsx("div", { className: "etu-backoffice-actions", children: actions })
|
|
1764
|
+
] }),
|
|
1765
|
+
/* @__PURE__ */ jsx("div", { className: "etu-backoffice-body", children })
|
|
1766
|
+
] });
|
|
1767
|
+
}
|
|
1768
|
+
function DefaultLockIcon() {
|
|
1769
|
+
return /* @__PURE__ */ jsxs(
|
|
1770
|
+
"svg",
|
|
1771
|
+
{
|
|
1772
|
+
viewBox: "0 0 24 24",
|
|
1773
|
+
width: "12",
|
|
1774
|
+
height: "12",
|
|
1775
|
+
fill: "none",
|
|
1776
|
+
stroke: "currentColor",
|
|
1777
|
+
strokeWidth: "2",
|
|
1778
|
+
strokeLinecap: "round",
|
|
1779
|
+
strokeLinejoin: "round",
|
|
1780
|
+
"aria-hidden": "true",
|
|
1781
|
+
children: [
|
|
1782
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2" }),
|
|
1783
|
+
/* @__PURE__ */ jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })
|
|
1784
|
+
]
|
|
1785
|
+
}
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
function isExternal(href) {
|
|
1789
|
+
return /^https?:\/\//.test(href);
|
|
1790
|
+
}
|
|
1791
|
+
function AppInfoSection({
|
|
1792
|
+
name,
|
|
1793
|
+
description,
|
|
1794
|
+
icon,
|
|
1795
|
+
appVersion,
|
|
1796
|
+
version,
|
|
1797
|
+
builtAt,
|
|
1798
|
+
links,
|
|
1799
|
+
children,
|
|
1800
|
+
heading = "\uC571 \uC815\uBCF4",
|
|
1801
|
+
className
|
|
1802
|
+
}) {
|
|
1803
|
+
return /* @__PURE__ */ jsxs("section", { className: "etu-app-info" + (className ? " " + className : ""), children: [
|
|
1804
|
+
heading !== null && /* @__PURE__ */ jsx("h2", { className: "etu-app-info-heading", children: heading }),
|
|
1805
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-app-info-card", children: [
|
|
1806
|
+
(name || icon) && /* @__PURE__ */ jsxs("div", { className: "etu-app-info-identity", children: [
|
|
1807
|
+
icon && /* @__PURE__ */ jsx("div", { className: "etu-app-info-icon", children: icon }),
|
|
1808
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-app-info-identity-text", children: [
|
|
1809
|
+
name && /* @__PURE__ */ jsx("div", { className: "etu-app-info-name", children: name }),
|
|
1810
|
+
description && /* @__PURE__ */ jsx("div", { className: "etu-app-info-description", children: description })
|
|
1811
|
+
] })
|
|
1812
|
+
] }),
|
|
1813
|
+
/* @__PURE__ */ jsxs("dl", { className: "etu-app-info-meta", children: [
|
|
1814
|
+
appVersion && /* @__PURE__ */ jsxs("div", { className: "etu-app-info-row", children: [
|
|
1815
|
+
/* @__PURE__ */ jsx("dt", { children: "\uBC84\uC804" }),
|
|
1816
|
+
/* @__PURE__ */ jsx("dd", { children: appVersion })
|
|
1817
|
+
] }),
|
|
1818
|
+
(version || builtAt) && /* @__PURE__ */ jsxs("div", { className: "etu-app-info-row", children: [
|
|
1819
|
+
/* @__PURE__ */ jsx("dt", { children: "\uBE4C\uB4DC" }),
|
|
1820
|
+
/* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(DeployInfo, { version, builtAt }) })
|
|
1821
|
+
] }),
|
|
1822
|
+
children
|
|
1823
|
+
] }),
|
|
1824
|
+
links && links.length > 0 && /* @__PURE__ */ jsx("div", { className: "etu-app-info-links", children: links.map((l) => {
|
|
1825
|
+
const ext = l.external ?? isExternal(l.href);
|
|
1826
|
+
return /* @__PURE__ */ jsx(
|
|
1827
|
+
"a",
|
|
1828
|
+
{
|
|
1829
|
+
className: "etu-app-info-link",
|
|
1830
|
+
href: l.href,
|
|
1831
|
+
target: ext ? "_blank" : void 0,
|
|
1832
|
+
rel: ext ? "noreferrer" : void 0,
|
|
1833
|
+
children: l.label
|
|
1834
|
+
},
|
|
1835
|
+
l.href
|
|
1836
|
+
);
|
|
1837
|
+
}) })
|
|
1838
|
+
] })
|
|
1839
|
+
] });
|
|
1840
|
+
}
|
|
1841
|
+
function pickInitial(fallback) {
|
|
1842
|
+
if (!fallback) return "?";
|
|
1843
|
+
const trimmed = fallback.trim();
|
|
1844
|
+
if (!trimmed) return "?";
|
|
1845
|
+
const local = trimmed.split("@")[0];
|
|
1846
|
+
return local.charAt(0).toUpperCase();
|
|
1847
|
+
}
|
|
1848
|
+
function Avatar({ src, fallback, size = 32, className, alt = "\uD504\uB85C\uD544" }) {
|
|
1849
|
+
const [errored, setErrored] = useState(false);
|
|
1850
|
+
const showPicture = src && !errored;
|
|
1851
|
+
const style = { width: size, height: size };
|
|
1852
|
+
return /* @__PURE__ */ jsx(
|
|
1853
|
+
"span",
|
|
1854
|
+
{
|
|
1855
|
+
className: "etu-avatar" + (className ? " " + className : ""),
|
|
1856
|
+
style,
|
|
1857
|
+
"aria-label": alt,
|
|
1858
|
+
role: "img",
|
|
1859
|
+
children: showPicture ? /* @__PURE__ */ jsx("img", { src, alt: "", onError: () => setErrored(true) }) : /* @__PURE__ */ jsx("span", { className: "etu-avatar-initial", children: pickInitial(fallback) })
|
|
1860
|
+
}
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
function UserMenu({
|
|
1864
|
+
me,
|
|
1865
|
+
avatarSize = 32,
|
|
1866
|
+
myInfoHref = "/me",
|
|
1867
|
+
myInfoLabel = "\uB0B4 \uC815\uBCF4",
|
|
1868
|
+
onSignOut,
|
|
1869
|
+
signOutLabel = "\uB85C\uADF8\uC544\uC6C3",
|
|
1870
|
+
signedOutAction,
|
|
1871
|
+
extraItems,
|
|
1872
|
+
className,
|
|
1873
|
+
showAdminBadge = true,
|
|
1874
|
+
placement = "bottom-right"
|
|
1875
|
+
}) {
|
|
1876
|
+
const [open, setOpen] = useState(false);
|
|
1877
|
+
const [computedPlacement, setComputedPlacement] = useState(placement);
|
|
1878
|
+
const rootRef = useRef(null);
|
|
1879
|
+
const triggerRef = useRef(null);
|
|
1880
|
+
const menuId = useId();
|
|
1881
|
+
useEffect(() => {
|
|
1882
|
+
if (!open) return;
|
|
1883
|
+
const onDocClick = (e) => {
|
|
1884
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
1885
|
+
};
|
|
1886
|
+
const onKey = (e) => {
|
|
1887
|
+
if (e.key === "Escape") setOpen(false);
|
|
1888
|
+
};
|
|
1889
|
+
document.addEventListener("mousedown", onDocClick);
|
|
1890
|
+
document.addEventListener("keydown", onKey);
|
|
1891
|
+
return () => {
|
|
1892
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
1893
|
+
document.removeEventListener("keydown", onKey);
|
|
1894
|
+
};
|
|
1895
|
+
}, [open]);
|
|
1896
|
+
useEffect(() => {
|
|
1897
|
+
if (!open) {
|
|
1898
|
+
setComputedPlacement(placement);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const trigger = triggerRef.current;
|
|
1902
|
+
if (!trigger || typeof window === "undefined") return;
|
|
1903
|
+
const rect = trigger.getBoundingClientRect();
|
|
1904
|
+
const vw = window.innerWidth;
|
|
1905
|
+
const vh = window.innerHeight;
|
|
1906
|
+
const menuW = 240;
|
|
1907
|
+
const menuH = 220;
|
|
1908
|
+
const [vReq, hReq] = placement.split("-");
|
|
1909
|
+
let v = vReq;
|
|
1910
|
+
let h = hReq;
|
|
1911
|
+
const spaceAbove = rect.top;
|
|
1912
|
+
const spaceBelow = vh - rect.bottom;
|
|
1913
|
+
if (v === "top" && spaceAbove < menuH && spaceBelow > spaceAbove) v = "bottom";
|
|
1914
|
+
else if (v === "bottom" && spaceBelow < menuH && spaceAbove > spaceBelow) v = "top";
|
|
1915
|
+
const spaceForRight = rect.right;
|
|
1916
|
+
const spaceForLeft = vw - rect.left;
|
|
1917
|
+
if (h === "right" && spaceForRight < menuW && spaceForLeft > spaceForRight) h = "left";
|
|
1918
|
+
else if (h === "left" && spaceForLeft < menuW && spaceForRight > spaceForLeft) h = "right";
|
|
1919
|
+
setComputedPlacement(`${v}-${h}`);
|
|
1920
|
+
}, [open, placement]);
|
|
1921
|
+
if (!me) {
|
|
1922
|
+
return /* @__PURE__ */ jsx("div", { className: "etu-user-menu" + (className ? " " + className : ""), children: signedOutAction ?? /* @__PURE__ */ jsx("a", { className: "etu-user-menu-sign-in", href: signInUrl(), children: "\uB85C\uADF8\uC778" }) });
|
|
1923
|
+
}
|
|
1924
|
+
const displayName = me.name ?? me.preferred_username ?? me.email;
|
|
1925
|
+
const handleSignOut = onSignOut ?? (() => signOut("/"));
|
|
1926
|
+
return /* @__PURE__ */ jsxs(
|
|
1927
|
+
"div",
|
|
1928
|
+
{
|
|
1929
|
+
ref: rootRef,
|
|
1930
|
+
className: "etu-user-menu" + (className ? " " + className : ""),
|
|
1931
|
+
children: [
|
|
1932
|
+
/* @__PURE__ */ jsx(
|
|
1933
|
+
"button",
|
|
1934
|
+
{
|
|
1935
|
+
ref: triggerRef,
|
|
1936
|
+
type: "button",
|
|
1937
|
+
className: "etu-user-menu-trigger",
|
|
1938
|
+
"aria-haspopup": "menu",
|
|
1939
|
+
"aria-expanded": open,
|
|
1940
|
+
"aria-controls": menuId,
|
|
1941
|
+
onClick: () => setOpen((o) => !o),
|
|
1942
|
+
title: displayName,
|
|
1943
|
+
children: /* @__PURE__ */ jsx(
|
|
1944
|
+
Avatar,
|
|
1945
|
+
{
|
|
1946
|
+
src: me.picture,
|
|
1947
|
+
fallback: me.preferred_username || me.email,
|
|
1948
|
+
size: avatarSize
|
|
1949
|
+
}
|
|
1950
|
+
)
|
|
1951
|
+
}
|
|
1952
|
+
),
|
|
1953
|
+
open && /* @__PURE__ */ jsxs(
|
|
1954
|
+
"div",
|
|
1955
|
+
{
|
|
1956
|
+
id: menuId,
|
|
1957
|
+
className: `etu-user-menu-dropdown etu-user-menu-dropdown--${computedPlacement}`,
|
|
1958
|
+
role: "menu",
|
|
1959
|
+
"aria-label": displayName,
|
|
1960
|
+
children: [
|
|
1961
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-user-menu-header", children: [
|
|
1962
|
+
/* @__PURE__ */ jsx(
|
|
1963
|
+
Avatar,
|
|
1964
|
+
{
|
|
1965
|
+
src: me.picture,
|
|
1966
|
+
fallback: me.preferred_username || me.email,
|
|
1967
|
+
size: 40
|
|
1968
|
+
}
|
|
1969
|
+
),
|
|
1970
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-user-menu-header-text", children: [
|
|
1971
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-user-menu-name", children: [
|
|
1972
|
+
displayName,
|
|
1973
|
+
showAdminBadge && me.is_admin && /* @__PURE__ */ jsx("span", { className: "etu-user-menu-admin", children: "admin" })
|
|
1974
|
+
] }),
|
|
1975
|
+
displayName !== me.email && /* @__PURE__ */ jsx("div", { className: "etu-user-menu-email", children: me.email })
|
|
1976
|
+
] })
|
|
1977
|
+
] }),
|
|
1978
|
+
/* @__PURE__ */ jsx("div", { className: "etu-user-menu-divider" }),
|
|
1979
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-user-menu-items", children: [
|
|
1980
|
+
extraItems?.map((it, i) => /* @__PURE__ */ jsx(MenuItem, { item: it, close: () => setOpen(false) }, i)),
|
|
1981
|
+
myInfoHref && /* @__PURE__ */ jsx(
|
|
1982
|
+
MenuItem,
|
|
1983
|
+
{
|
|
1984
|
+
item: { label: myInfoLabel, href: myInfoHref },
|
|
1985
|
+
close: () => setOpen(false)
|
|
1986
|
+
}
|
|
1987
|
+
),
|
|
1988
|
+
/* @__PURE__ */ jsx(
|
|
1989
|
+
"button",
|
|
1990
|
+
{
|
|
1991
|
+
type: "button",
|
|
1992
|
+
role: "menuitem",
|
|
1993
|
+
className: "etu-user-menu-item etu-user-menu-item--danger",
|
|
1994
|
+
onClick: () => {
|
|
1995
|
+
setOpen(false);
|
|
1996
|
+
handleSignOut();
|
|
1997
|
+
},
|
|
1998
|
+
children: signOutLabel
|
|
1999
|
+
}
|
|
2000
|
+
)
|
|
2001
|
+
] })
|
|
2002
|
+
]
|
|
2003
|
+
}
|
|
2004
|
+
)
|
|
2005
|
+
]
|
|
2006
|
+
}
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
function MenuItem({ item, close }) {
|
|
2010
|
+
const { label, href, onClick, external } = item;
|
|
2011
|
+
if (href) {
|
|
2012
|
+
return /* @__PURE__ */ jsx(
|
|
2013
|
+
"a",
|
|
2014
|
+
{
|
|
2015
|
+
role: "menuitem",
|
|
2016
|
+
className: "etu-user-menu-item",
|
|
2017
|
+
href,
|
|
2018
|
+
target: external ? "_blank" : void 0,
|
|
2019
|
+
rel: external ? "noreferrer" : void 0,
|
|
2020
|
+
onClick: close,
|
|
2021
|
+
children: label
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
return /* @__PURE__ */ jsx(
|
|
2026
|
+
"button",
|
|
2027
|
+
{
|
|
2028
|
+
type: "button",
|
|
2029
|
+
role: "menuitem",
|
|
2030
|
+
className: "etu-user-menu-item",
|
|
2031
|
+
onClick: () => {
|
|
2032
|
+
close();
|
|
2033
|
+
onClick?.();
|
|
2034
|
+
},
|
|
2035
|
+
children: label
|
|
2036
|
+
}
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
function MobileTabBar({ items: items2, ariaLabel = "\uC8FC\uC694 \uBA54\uB274", className }) {
|
|
2040
|
+
return /* @__PURE__ */ jsx(
|
|
2041
|
+
"nav",
|
|
2042
|
+
{
|
|
2043
|
+
className: "etu-mobile-tab-bar etu-glass" + (className ? " " + className : ""),
|
|
2044
|
+
"aria-label": ariaLabel,
|
|
2045
|
+
children: items2.map((it) => {
|
|
2046
|
+
const cls = "etu-mobile-tab-bar-item" + (it.active ? " etu-mobile-tab-bar-item--active" : "");
|
|
2047
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2048
|
+
/* @__PURE__ */ jsx("span", { className: "etu-mobile-tab-bar-icon", "aria-hidden": true, children: it.icon }),
|
|
2049
|
+
/* @__PURE__ */ jsx("span", { className: "etu-mobile-tab-bar-label", children: it.label })
|
|
2050
|
+
] });
|
|
2051
|
+
if (it.href) {
|
|
2052
|
+
return /* @__PURE__ */ jsx(
|
|
2053
|
+
"a",
|
|
2054
|
+
{
|
|
2055
|
+
className: cls,
|
|
2056
|
+
href: it.href,
|
|
2057
|
+
"aria-current": it.active ? "page" : void 0,
|
|
2058
|
+
onClick: (e) => {
|
|
2059
|
+
if (it.onClick) {
|
|
2060
|
+
e.preventDefault();
|
|
2061
|
+
it.onClick();
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
children: inner
|
|
2065
|
+
},
|
|
2066
|
+
it.id
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
return /* @__PURE__ */ jsx(
|
|
2070
|
+
"button",
|
|
2071
|
+
{
|
|
2072
|
+
type: "button",
|
|
2073
|
+
className: cls,
|
|
2074
|
+
"aria-current": it.active ? "page" : void 0,
|
|
2075
|
+
onClick: it.onClick,
|
|
2076
|
+
children: inner
|
|
2077
|
+
},
|
|
2078
|
+
it.id
|
|
2079
|
+
);
|
|
2080
|
+
})
|
|
2081
|
+
}
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/viewportCore.ts
|
|
2086
|
+
var TABLET_MIN = 720;
|
|
2087
|
+
var DESKTOP_MIN = 1024;
|
|
2088
|
+
function tierForWidth(w) {
|
|
2089
|
+
if (w >= DESKTOP_MIN) return "desktop";
|
|
2090
|
+
if (w >= TABLET_MIN) return "tablet";
|
|
2091
|
+
return "mobile";
|
|
2092
|
+
}
|
|
2093
|
+
function getViewport() {
|
|
2094
|
+
if (typeof window === "undefined") return "desktop";
|
|
2095
|
+
return tierForWidth(window.innerWidth);
|
|
2096
|
+
}
|
|
2097
|
+
var noFlashViewportScript = `(function(){try{var w=window.innerWidth||document.documentElement.clientWidth;var v="mobile";if(w>=${DESKTOP_MIN})v="desktop";else if(w>=${TABLET_MIN})v="tablet";document.documentElement.setAttribute("data-vp",v);}catch(e){}})();`;
|
|
2098
|
+
var ViewportContext = createContext(null);
|
|
2099
|
+
function ViewportProvider({ children }) {
|
|
2100
|
+
const tier = useViewportInternal();
|
|
2101
|
+
return /* @__PURE__ */ jsx(ViewportContext.Provider, { value: tier, children });
|
|
2102
|
+
}
|
|
2103
|
+
function useViewportInternal() {
|
|
2104
|
+
const [tier, setTier] = useState(() => getViewport());
|
|
2105
|
+
useEffect(() => {
|
|
2106
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
2107
|
+
const tabletMq = window.matchMedia(`(min-width: ${TABLET_MIN}px)`);
|
|
2108
|
+
const desktopMq = window.matchMedia(`(min-width: ${DESKTOP_MIN}px)`);
|
|
2109
|
+
function recompute() {
|
|
2110
|
+
const next = tierForWidth(window.innerWidth);
|
|
2111
|
+
setTier(next);
|
|
2112
|
+
document.documentElement.setAttribute("data-vp", next);
|
|
2113
|
+
}
|
|
2114
|
+
recompute();
|
|
2115
|
+
tabletMq.addEventListener("change", recompute);
|
|
2116
|
+
desktopMq.addEventListener("change", recompute);
|
|
2117
|
+
return () => {
|
|
2118
|
+
tabletMq.removeEventListener("change", recompute);
|
|
2119
|
+
desktopMq.removeEventListener("change", recompute);
|
|
2120
|
+
};
|
|
2121
|
+
}, []);
|
|
2122
|
+
return tier;
|
|
2123
|
+
}
|
|
2124
|
+
function useViewport() {
|
|
2125
|
+
const ctx = useContext(ViewportContext);
|
|
2126
|
+
const local = useViewportInternal();
|
|
2127
|
+
return ctx ?? local;
|
|
2128
|
+
}
|
|
2129
|
+
function BellIcon() {
|
|
2130
|
+
return /* @__PURE__ */ jsxs(
|
|
2131
|
+
"svg",
|
|
2132
|
+
{
|
|
2133
|
+
width: "20",
|
|
2134
|
+
height: "20",
|
|
2135
|
+
viewBox: "0 0 24 24",
|
|
2136
|
+
fill: "none",
|
|
2137
|
+
stroke: "currentColor",
|
|
2138
|
+
strokeWidth: "2",
|
|
2139
|
+
strokeLinecap: "round",
|
|
2140
|
+
strokeLinejoin: "round",
|
|
2141
|
+
"aria-hidden": "true",
|
|
2142
|
+
children: [
|
|
2143
|
+
/* @__PURE__ */ jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
|
|
2144
|
+
/* @__PURE__ */ jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })
|
|
2145
|
+
]
|
|
2146
|
+
}
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
function NotificationBell({
|
|
2150
|
+
items: items2,
|
|
2151
|
+
count,
|
|
2152
|
+
onOpen,
|
|
2153
|
+
ariaLabel = "\uC54C\uB9BC",
|
|
2154
|
+
title = "\uC54C\uB9BC",
|
|
2155
|
+
emptyMessage = "\uC0C8 \uC54C\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2156
|
+
footer,
|
|
2157
|
+
icon,
|
|
2158
|
+
className,
|
|
2159
|
+
placement = "bottom-right"
|
|
2160
|
+
}) {
|
|
2161
|
+
const [open, setOpen] = useState(false);
|
|
2162
|
+
const [computedPlacement, setComputedPlacement] = useState(placement);
|
|
2163
|
+
const rootRef = useRef(null);
|
|
2164
|
+
const triggerRef = useRef(null);
|
|
2165
|
+
const panelId = useId();
|
|
2166
|
+
const viewport = useViewport();
|
|
2167
|
+
const isMobile = viewport === "mobile";
|
|
2168
|
+
const badge = count ?? items2.length;
|
|
2169
|
+
useEffect(() => {
|
|
2170
|
+
if (!open) return;
|
|
2171
|
+
onOpen?.();
|
|
2172
|
+
}, [open]);
|
|
2173
|
+
useEffect(() => {
|
|
2174
|
+
if (!open) return;
|
|
2175
|
+
const onDocClick = (e) => {
|
|
2176
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
2177
|
+
};
|
|
2178
|
+
const onKey = (e) => {
|
|
2179
|
+
if (e.key === "Escape") setOpen(false);
|
|
2180
|
+
};
|
|
2181
|
+
document.addEventListener("mousedown", onDocClick);
|
|
2182
|
+
document.addEventListener("keydown", onKey);
|
|
2183
|
+
return () => {
|
|
2184
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
2185
|
+
document.removeEventListener("keydown", onKey);
|
|
2186
|
+
};
|
|
2187
|
+
}, [open]);
|
|
2188
|
+
useEffect(() => {
|
|
2189
|
+
if (!open || isMobile) {
|
|
2190
|
+
setComputedPlacement(placement);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const trigger = triggerRef.current;
|
|
2194
|
+
if (!trigger || typeof window === "undefined") return;
|
|
2195
|
+
const rect = trigger.getBoundingClientRect();
|
|
2196
|
+
const vw = window.innerWidth;
|
|
2197
|
+
const vh = window.innerHeight;
|
|
2198
|
+
const menuW = 360;
|
|
2199
|
+
const menuH = 420;
|
|
2200
|
+
const [vReq, hReq] = placement.split("-");
|
|
2201
|
+
let v = vReq;
|
|
2202
|
+
let h = hReq;
|
|
2203
|
+
const spaceAbove = rect.top;
|
|
2204
|
+
const spaceBelow = vh - rect.bottom;
|
|
2205
|
+
if (v === "top" && spaceAbove < menuH && spaceBelow > spaceAbove) v = "bottom";
|
|
2206
|
+
else if (v === "bottom" && spaceBelow < menuH && spaceAbove > spaceBelow) v = "top";
|
|
2207
|
+
const spaceForRight = rect.right;
|
|
2208
|
+
const spaceForLeft = vw - rect.left;
|
|
2209
|
+
if (h === "right" && spaceForRight < menuW && spaceForLeft > spaceForRight) h = "left";
|
|
2210
|
+
else if (h === "left" && spaceForLeft < menuW && spaceForRight > spaceForLeft) h = "right";
|
|
2211
|
+
setComputedPlacement(`${v}-${h}`);
|
|
2212
|
+
}, [open, placement, isMobile]);
|
|
2213
|
+
useEffect(() => {
|
|
2214
|
+
if (!open || !isMobile || typeof document === "undefined") return;
|
|
2215
|
+
const prev = document.body.style.overflow;
|
|
2216
|
+
document.body.style.overflow = "hidden";
|
|
2217
|
+
return () => {
|
|
2218
|
+
document.body.style.overflow = prev;
|
|
2219
|
+
};
|
|
2220
|
+
}, [open, isMobile]);
|
|
2221
|
+
const close = () => setOpen(false);
|
|
2222
|
+
return /* @__PURE__ */ jsxs(
|
|
2223
|
+
"div",
|
|
2224
|
+
{
|
|
2225
|
+
ref: rootRef,
|
|
2226
|
+
className: "etu-notif-bell" + (className ? " " + className : ""),
|
|
2227
|
+
children: [
|
|
2228
|
+
/* @__PURE__ */ jsxs(
|
|
2229
|
+
"button",
|
|
2230
|
+
{
|
|
2231
|
+
ref: triggerRef,
|
|
2232
|
+
type: "button",
|
|
2233
|
+
className: "etu-notif-bell-trigger",
|
|
2234
|
+
"aria-haspopup": "dialog",
|
|
2235
|
+
"aria-expanded": open,
|
|
2236
|
+
"aria-controls": panelId,
|
|
2237
|
+
"aria-label": badge > 0 ? `${ariaLabel} (${badge})` : ariaLabel,
|
|
2238
|
+
onClick: () => setOpen((o) => !o),
|
|
2239
|
+
children: [
|
|
2240
|
+
icon ?? /* @__PURE__ */ jsx(BellIcon, {}),
|
|
2241
|
+
badge > 0 && /* @__PURE__ */ jsx("span", { className: "etu-notif-bell-badge", "aria-hidden": "true", children: badge > 99 ? "99+" : badge })
|
|
2242
|
+
]
|
|
2243
|
+
}
|
|
2244
|
+
),
|
|
2245
|
+
open && isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2246
|
+
/* @__PURE__ */ jsx(
|
|
2247
|
+
"div",
|
|
2248
|
+
{
|
|
2249
|
+
className: "etu-notif-bell-backdrop",
|
|
2250
|
+
onMouseDown: close,
|
|
2251
|
+
"aria-hidden": "true"
|
|
2252
|
+
}
|
|
2253
|
+
),
|
|
2254
|
+
/* @__PURE__ */ jsxs(
|
|
2255
|
+
"div",
|
|
2256
|
+
{
|
|
2257
|
+
id: panelId,
|
|
2258
|
+
role: "dialog",
|
|
2259
|
+
"aria-modal": "true",
|
|
2260
|
+
"aria-label": typeof title === "string" ? title : void 0,
|
|
2261
|
+
className: "etu-notif-bell-sheet",
|
|
2262
|
+
children: [
|
|
2263
|
+
/* @__PURE__ */ jsx("div", { className: "etu-notif-bell-sheet-grabber", "aria-hidden": "true" }),
|
|
2264
|
+
/* @__PURE__ */ jsx(
|
|
2265
|
+
NotifPanelBody,
|
|
2266
|
+
{
|
|
2267
|
+
title,
|
|
2268
|
+
items: items2,
|
|
2269
|
+
emptyMessage,
|
|
2270
|
+
footer,
|
|
2271
|
+
onClose: close
|
|
2272
|
+
}
|
|
2273
|
+
)
|
|
2274
|
+
]
|
|
2275
|
+
}
|
|
2276
|
+
)
|
|
2277
|
+
] }),
|
|
2278
|
+
open && !isMobile && /* @__PURE__ */ jsx(
|
|
2279
|
+
"div",
|
|
2280
|
+
{
|
|
2281
|
+
id: panelId,
|
|
2282
|
+
role: "dialog",
|
|
2283
|
+
"aria-label": typeof title === "string" ? title : void 0,
|
|
2284
|
+
className: `etu-notif-bell-popover etu-notif-bell-popover--${computedPlacement}`,
|
|
2285
|
+
children: /* @__PURE__ */ jsx(
|
|
2286
|
+
NotifPanelBody,
|
|
2287
|
+
{
|
|
2288
|
+
title,
|
|
2289
|
+
items: items2,
|
|
2290
|
+
emptyMessage,
|
|
2291
|
+
footer,
|
|
2292
|
+
onClose: close
|
|
2293
|
+
}
|
|
2294
|
+
)
|
|
2295
|
+
}
|
|
2296
|
+
)
|
|
2297
|
+
]
|
|
2298
|
+
}
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
function NotifPanelBody({
|
|
2302
|
+
title,
|
|
2303
|
+
items: items2,
|
|
2304
|
+
emptyMessage,
|
|
2305
|
+
footer,
|
|
2306
|
+
onClose
|
|
2307
|
+
}) {
|
|
2308
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2309
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-notif-bell-header", children: [
|
|
2310
|
+
/* @__PURE__ */ jsx("span", { className: "etu-notif-bell-title", children: title }),
|
|
2311
|
+
/* @__PURE__ */ jsx(
|
|
2312
|
+
"button",
|
|
2313
|
+
{
|
|
2314
|
+
type: "button",
|
|
2315
|
+
className: "etu-notif-bell-close",
|
|
2316
|
+
"aria-label": "\uB2EB\uAE30",
|
|
2317
|
+
onClick: onClose,
|
|
2318
|
+
children: "\xD7"
|
|
2319
|
+
}
|
|
2320
|
+
)
|
|
2321
|
+
] }),
|
|
2322
|
+
/* @__PURE__ */ jsx("div", { className: "etu-notif-bell-items", children: items2.length === 0 ? /* @__PURE__ */ jsx("div", { className: "etu-notif-bell-empty", children: emptyMessage }) : items2.map((it) => /* @__PURE__ */ jsx("div", { className: "etu-notif-bell-item", children: it.content }, it.id)) }),
|
|
2323
|
+
footer && /* @__PURE__ */ jsx("div", { className: "etu-notif-bell-footer", children: footer })
|
|
2324
|
+
] });
|
|
2325
|
+
}
|
|
2326
|
+
function Item({ item }) {
|
|
2327
|
+
const cls = "etu-sidebar-item" + (item.active ? " etu-sidebar-item--active" : "");
|
|
2328
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2329
|
+
item.icon ? /* @__PURE__ */ jsx("span", { className: "etu-sidebar-item-icon", "aria-hidden": true, children: item.icon }) : null,
|
|
2330
|
+
/* @__PURE__ */ jsx("span", { className: "etu-sidebar-item-label", children: item.label })
|
|
2331
|
+
] });
|
|
2332
|
+
if (item.href) {
|
|
2333
|
+
return /* @__PURE__ */ jsx(
|
|
2334
|
+
"a",
|
|
2335
|
+
{
|
|
2336
|
+
className: cls,
|
|
2337
|
+
href: item.href,
|
|
2338
|
+
"aria-current": item.active ? "page" : void 0,
|
|
2339
|
+
onClick: (e) => {
|
|
2340
|
+
if (item.onClick) {
|
|
2341
|
+
e.preventDefault();
|
|
2342
|
+
item.onClick();
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
children: inner
|
|
2346
|
+
}
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
return /* @__PURE__ */ jsx(
|
|
2350
|
+
"button",
|
|
2351
|
+
{
|
|
2352
|
+
type: "button",
|
|
2353
|
+
className: cls,
|
|
2354
|
+
"aria-current": item.active ? "page" : void 0,
|
|
2355
|
+
onClick: item.onClick,
|
|
2356
|
+
children: inner
|
|
2357
|
+
}
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
function Sidebar({
|
|
2361
|
+
appName,
|
|
2362
|
+
appIcon,
|
|
2363
|
+
appHeaderExtra,
|
|
2364
|
+
primary,
|
|
2365
|
+
secondary,
|
|
2366
|
+
secondarySections,
|
|
2367
|
+
secondaryCaption = "\uB354\uBCF4\uAE30",
|
|
2368
|
+
footer,
|
|
2369
|
+
ariaLabel = "\uC8FC \uBA54\uB274",
|
|
2370
|
+
className,
|
|
2371
|
+
tabletMode = "rail",
|
|
2372
|
+
open,
|
|
2373
|
+
onOpenChange
|
|
2374
|
+
}) {
|
|
2375
|
+
const dataAttrs = {
|
|
2376
|
+
"data-tablet-mode": tabletMode
|
|
2377
|
+
};
|
|
2378
|
+
if (tabletMode === "drawer") {
|
|
2379
|
+
dataAttrs["data-open"] = open ? "true" : "false";
|
|
2380
|
+
}
|
|
2381
|
+
useEffect(() => {
|
|
2382
|
+
if (tabletMode !== "drawer" || !open) return;
|
|
2383
|
+
function onKey(e) {
|
|
2384
|
+
if (e.key === "Escape") onOpenChange?.(false);
|
|
2385
|
+
}
|
|
2386
|
+
window.addEventListener("keydown", onKey);
|
|
2387
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2388
|
+
}, [tabletMode, open, onOpenChange]);
|
|
2389
|
+
const hasSections = secondarySections && secondarySections.length > 0;
|
|
2390
|
+
const hasFlatSecondary = secondary && secondary.length > 0;
|
|
2391
|
+
if (hasSections && hasFlatSecondary) {
|
|
2392
|
+
const proc = globalThis.process;
|
|
2393
|
+
if (!proc || !proc.env || proc.env.NODE_ENV !== "production") {
|
|
2394
|
+
console.warn(
|
|
2395
|
+
"[@etamong-playground/ui] <Sidebar>: both `secondary` and `secondarySections` were passed; `secondarySections` wins. Drop one to silence this warning."
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2400
|
+
tabletMode === "drawer" && open ? /* @__PURE__ */ jsx(
|
|
2401
|
+
"div",
|
|
2402
|
+
{
|
|
2403
|
+
className: "etu-sidebar-scrim",
|
|
2404
|
+
"aria-hidden": true,
|
|
2405
|
+
onClick: () => onOpenChange?.(false)
|
|
2406
|
+
}
|
|
2407
|
+
) : null,
|
|
2408
|
+
/* @__PURE__ */ jsxs(
|
|
2409
|
+
"aside",
|
|
2410
|
+
{
|
|
2411
|
+
className: "etu-sidebar" + (className ? " " + className : ""),
|
|
2412
|
+
"aria-label": ariaLabel,
|
|
2413
|
+
...dataAttrs,
|
|
2414
|
+
children: [
|
|
2415
|
+
(appName || appIcon || appHeaderExtra) && /* @__PURE__ */ jsxs("div", { className: "etu-sidebar-header", children: [
|
|
2416
|
+
(appIcon || appName) && /* @__PURE__ */ jsxs("div", { className: "etu-sidebar-header-app", children: [
|
|
2417
|
+
appIcon ? /* @__PURE__ */ jsx("span", { className: "etu-sidebar-header-icon", "aria-hidden": true, children: appIcon }) : null,
|
|
2418
|
+
appName ? /* @__PURE__ */ jsx("span", { className: "etu-sidebar-header-name", children: appName }) : null
|
|
2419
|
+
] }),
|
|
2420
|
+
appHeaderExtra ? /* @__PURE__ */ jsx("div", { className: "etu-sidebar-header-extra", children: appHeaderExtra }) : null
|
|
2421
|
+
] }),
|
|
2422
|
+
/* @__PURE__ */ jsx("nav", { className: "etu-sidebar-section etu-sidebar-section--primary", children: primary.map((it) => /* @__PURE__ */ jsx(Item, { item: it }, it.id)) }),
|
|
2423
|
+
hasSections ? secondarySections.map((section, idx) => /* @__PURE__ */ jsxs(
|
|
2424
|
+
"nav",
|
|
2425
|
+
{
|
|
2426
|
+
className: "etu-sidebar-section etu-sidebar-section--secondary",
|
|
2427
|
+
"aria-label": typeof section.caption === "string" ? section.caption : void 0,
|
|
2428
|
+
children: [
|
|
2429
|
+
section.caption ? /* @__PURE__ */ jsx("div", { className: "etu-sidebar-section-caption", children: section.caption }) : null,
|
|
2430
|
+
section.items.map((it) => /* @__PURE__ */ jsx(Item, { item: it }, it.id))
|
|
2431
|
+
]
|
|
2432
|
+
},
|
|
2433
|
+
section.id ?? idx
|
|
2434
|
+
)) : hasFlatSecondary ? /* @__PURE__ */ jsxs(
|
|
2435
|
+
"nav",
|
|
2436
|
+
{
|
|
2437
|
+
className: "etu-sidebar-section etu-sidebar-section--secondary",
|
|
2438
|
+
"aria-label": typeof secondaryCaption === "string" ? secondaryCaption : void 0,
|
|
2439
|
+
children: [
|
|
2440
|
+
secondaryCaption ? /* @__PURE__ */ jsx("div", { className: "etu-sidebar-caption", children: secondaryCaption }) : null,
|
|
2441
|
+
secondary.map((it) => /* @__PURE__ */ jsx(Item, { item: it }, it.id))
|
|
2442
|
+
]
|
|
2443
|
+
}
|
|
2444
|
+
) : null,
|
|
2445
|
+
footer ? /* @__PURE__ */ jsx("div", { className: "etu-sidebar-footer", children: footer }) : null
|
|
2446
|
+
]
|
|
2447
|
+
}
|
|
2448
|
+
)
|
|
2449
|
+
] });
|
|
2450
|
+
}
|
|
2451
|
+
function SidebarToggle({
|
|
2452
|
+
open,
|
|
2453
|
+
onOpenChange,
|
|
2454
|
+
labelOpen = "\uBA54\uB274 \uC5F4\uAE30",
|
|
2455
|
+
labelClose = "\uBA54\uB274 \uB2EB\uAE30",
|
|
2456
|
+
className
|
|
2457
|
+
}) {
|
|
2458
|
+
const onClick = useCallback(
|
|
2459
|
+
() => onOpenChange(!open),
|
|
2460
|
+
[open, onOpenChange]
|
|
2461
|
+
);
|
|
2462
|
+
return /* @__PURE__ */ jsx(
|
|
2463
|
+
"button",
|
|
2464
|
+
{
|
|
2465
|
+
type: "button",
|
|
2466
|
+
className: "etu-sidebar-toggle" + (className ? " " + className : ""),
|
|
2467
|
+
"aria-label": open ? labelClose : labelOpen,
|
|
2468
|
+
"aria-expanded": open,
|
|
2469
|
+
onClick,
|
|
2470
|
+
children: /* @__PURE__ */ jsx(
|
|
2471
|
+
"svg",
|
|
2472
|
+
{
|
|
2473
|
+
width: "20",
|
|
2474
|
+
height: "20",
|
|
2475
|
+
viewBox: "0 0 24 24",
|
|
2476
|
+
fill: "none",
|
|
2477
|
+
stroke: "currentColor",
|
|
2478
|
+
strokeWidth: "2",
|
|
2479
|
+
strokeLinecap: "round",
|
|
2480
|
+
"aria-hidden": true,
|
|
2481
|
+
children: open ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2482
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
2483
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
2484
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2485
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6" }),
|
|
2486
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }),
|
|
2487
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18" })
|
|
2488
|
+
] })
|
|
2489
|
+
}
|
|
2490
|
+
)
|
|
2491
|
+
}
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
function useSidebarDrawer(appKey, routeKey) {
|
|
2495
|
+
const storage = `${appKey}-sidebar-drawer-open`;
|
|
2496
|
+
const [open, setOpen] = useState(() => {
|
|
2497
|
+
try {
|
|
2498
|
+
return sessionStorage.getItem(storage) === "1";
|
|
2499
|
+
} catch {
|
|
2500
|
+
return false;
|
|
2501
|
+
}
|
|
2502
|
+
});
|
|
2503
|
+
const set = useCallback(
|
|
2504
|
+
(next) => {
|
|
2505
|
+
setOpen(next);
|
|
2506
|
+
try {
|
|
2507
|
+
if (next) sessionStorage.setItem(storage, "1");
|
|
2508
|
+
else sessionStorage.removeItem(storage);
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
},
|
|
2512
|
+
[storage]
|
|
2513
|
+
);
|
|
2514
|
+
useEffect(() => {
|
|
2515
|
+
if (routeKey === void 0) return;
|
|
2516
|
+
set(false);
|
|
2517
|
+
}, [routeKey]);
|
|
2518
|
+
return [open, set];
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// src/i18nCore.ts
|
|
2522
|
+
var SUPPORTED_LOCALES = ["ko", "en"];
|
|
2523
|
+
function storageKey2(appKey) {
|
|
2524
|
+
return `${appKey}-locale`;
|
|
2525
|
+
}
|
|
2526
|
+
function detectSystemLocale() {
|
|
2527
|
+
if (typeof navigator === "undefined") return "en";
|
|
2528
|
+
const candidates = [];
|
|
2529
|
+
if (Array.isArray(navigator.languages)) {
|
|
2530
|
+
candidates.push(...navigator.languages);
|
|
2531
|
+
} else if (typeof navigator.language === "string") {
|
|
2532
|
+
candidates.push(navigator.language);
|
|
2533
|
+
}
|
|
2534
|
+
for (const raw of candidates) {
|
|
2535
|
+
const tag = raw.toLowerCase();
|
|
2536
|
+
if (tag === "ko" || tag.startsWith("ko-")) return "ko";
|
|
2537
|
+
if (tag === "en" || tag.startsWith("en-")) return "en";
|
|
2538
|
+
}
|
|
2539
|
+
return "en";
|
|
2540
|
+
}
|
|
2541
|
+
function getLocale(appKey) {
|
|
2542
|
+
try {
|
|
2543
|
+
const saved = localStorage.getItem(storageKey2(appKey));
|
|
2544
|
+
if (saved === "ko" || saved === "en") return saved;
|
|
2545
|
+
} catch {
|
|
2546
|
+
}
|
|
2547
|
+
return detectSystemLocale();
|
|
2548
|
+
}
|
|
2549
|
+
function setLocale(appKey, locale) {
|
|
2550
|
+
try {
|
|
2551
|
+
localStorage.setItem(storageKey2(appKey), locale);
|
|
2552
|
+
} catch {
|
|
2553
|
+
}
|
|
2554
|
+
if (typeof document !== "undefined") {
|
|
2555
|
+
document.documentElement.setAttribute("lang", locale);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
function noFlashLocaleScript(appKey) {
|
|
2559
|
+
return `(function(){try{var k=${JSON.stringify(storageKey2(appKey))};var s=localStorage.getItem(k);var l=s;if(l!=="ko"&&l!=="en"){l="en";var langs=(navigator.languages&&navigator.languages.length?navigator.languages:[navigator.language||""]);for(var i=0;i<langs.length;i++){var t=(langs[i]||"").toLowerCase();if(t==="ko"||t.indexOf("ko-")===0){l="ko";break;}if(t==="en"||t.indexOf("en-")===0){l="en";break;}}}document.documentElement.setAttribute("lang",l);}catch(e){}})();`;
|
|
2560
|
+
}
|
|
2561
|
+
function interpolate(template, vars) {
|
|
2562
|
+
if (!vars) return template;
|
|
2563
|
+
return template.replace(/\{(\w+)\}/g, (m, k) => {
|
|
2564
|
+
const v = vars[k];
|
|
2565
|
+
return v === void 0 || v === null ? m : String(v);
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
var I18nContext = createContext(null);
|
|
2569
|
+
function I18nProvider({ appKey, messages, children }) {
|
|
2570
|
+
const [locale, setLocaleState] = useState(() => getLocale(appKey));
|
|
2571
|
+
useEffect(() => {
|
|
2572
|
+
if (typeof document !== "undefined") {
|
|
2573
|
+
document.documentElement.setAttribute("lang", locale);
|
|
2574
|
+
}
|
|
2575
|
+
}, [locale]);
|
|
2576
|
+
useEffect(() => {
|
|
2577
|
+
if (typeof window === "undefined") return;
|
|
2578
|
+
function onChange() {
|
|
2579
|
+
try {
|
|
2580
|
+
const saved = localStorage.getItem(storageKey2(appKey));
|
|
2581
|
+
if (saved === "ko" || saved === "en") return;
|
|
2582
|
+
} catch {
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
setLocaleState(detectSystemLocale());
|
|
2586
|
+
}
|
|
2587
|
+
window.addEventListener("languagechange", onChange);
|
|
2588
|
+
return () => window.removeEventListener("languagechange", onChange);
|
|
2589
|
+
}, [appKey]);
|
|
2590
|
+
const setLocaleCb = useCallback(
|
|
2591
|
+
(next) => {
|
|
2592
|
+
setLocale(appKey, next);
|
|
2593
|
+
setLocaleState(next);
|
|
2594
|
+
},
|
|
2595
|
+
[appKey]
|
|
2596
|
+
);
|
|
2597
|
+
const t = useCallback(
|
|
2598
|
+
(key, vars) => {
|
|
2599
|
+
const localized = messages[locale]?.[key];
|
|
2600
|
+
const fallback = messages.en?.[key];
|
|
2601
|
+
const raw = localized ?? fallback ?? key;
|
|
2602
|
+
return interpolate(raw, vars);
|
|
2603
|
+
},
|
|
2604
|
+
[messages, locale]
|
|
2605
|
+
);
|
|
2606
|
+
const value = useMemo(
|
|
2607
|
+
() => ({ locale, setLocale: setLocaleCb, t }),
|
|
2608
|
+
[locale, setLocaleCb, t]
|
|
2609
|
+
);
|
|
2610
|
+
return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
|
|
2611
|
+
}
|
|
2612
|
+
function useI18n() {
|
|
2613
|
+
const ctx = useContext(I18nContext);
|
|
2614
|
+
if (!ctx) {
|
|
2615
|
+
throw new Error(
|
|
2616
|
+
"[@etamong-playground/ui] useI18n / useT must be used inside <I18nProvider>"
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
return ctx;
|
|
2620
|
+
}
|
|
2621
|
+
function useT() {
|
|
2622
|
+
return useI18n().t;
|
|
2623
|
+
}
|
|
2624
|
+
function useLocale() {
|
|
2625
|
+
const { locale, setLocale: setLocale2 } = useI18n();
|
|
2626
|
+
return [locale, setLocale2];
|
|
2627
|
+
}
|
|
2628
|
+
function ChevronLeft() {
|
|
2629
|
+
return /* @__PURE__ */ jsx("span", { className: "etu-navbar-chevron", "aria-hidden": true, children: "\u2039" });
|
|
2630
|
+
}
|
|
2631
|
+
function resolveBack(back) {
|
|
2632
|
+
if (!back) return null;
|
|
2633
|
+
if (typeof back === "function") return back;
|
|
2634
|
+
if (typeof back === "string") {
|
|
2635
|
+
return () => {
|
|
2636
|
+
if (typeof window === "undefined") return;
|
|
2637
|
+
window.history.pushState(null, "", back);
|
|
2638
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
return () => {
|
|
2642
|
+
if (typeof window !== "undefined") window.history.back();
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
function isDevBuild2() {
|
|
2646
|
+
try {
|
|
2647
|
+
const proc = globalThis.process;
|
|
2648
|
+
if (proc?.env?.NODE_ENV === "production") return false;
|
|
2649
|
+
} catch {
|
|
2650
|
+
}
|
|
2651
|
+
return true;
|
|
2652
|
+
}
|
|
2653
|
+
function findStickyAncestorAbove(el) {
|
|
2654
|
+
let p = el.parentElement;
|
|
2655
|
+
while (p && p !== document.body) {
|
|
2656
|
+
const cs = window.getComputedStyle(p);
|
|
2657
|
+
if (cs.position === "sticky" || cs.position === "fixed") {
|
|
2658
|
+
const pt = parseFloat(cs.paddingTop || "0");
|
|
2659
|
+
if (pt >= 20) return p;
|
|
2660
|
+
}
|
|
2661
|
+
p = p.parentElement;
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
function assertNavbarFits(el) {
|
|
2666
|
+
if (typeof window === "undefined" || !el.isConnected) return;
|
|
2667
|
+
const rect = el.getBoundingClientRect();
|
|
2668
|
+
const viewportH = window.innerHeight;
|
|
2669
|
+
if (rect.bottom > viewportH + 0.5) {
|
|
2670
|
+
console.warn(
|
|
2671
|
+
`[@etamong-playground/ui] <NavigationBar>: bottom edge is off-screen \u2014 bar.bottom=${rect.bottom.toFixed(1)}px, viewport.h=${viewportH}px. Likely cause: an outer sticky/fixed bar already eats env(safe-area-inset-top) and this <NavigationBar> stacks its own. Pass safeAreaTop={false} to opt out, or hoist the bar.`,
|
|
2672
|
+
el
|
|
2673
|
+
);
|
|
2674
|
+
} else if (rect.bottom > viewportH - 1) {
|
|
2675
|
+
console.warn(
|
|
2676
|
+
`[@etamong-playground/ui] <NavigationBar>: bottom edge clipped by ~${(rect.bottom - viewportH).toFixed(1)}px. Check for double safe-area stacking on iOS PWA.`,
|
|
2677
|
+
el
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
const titleEl = el.querySelector(".etu-navbar-title");
|
|
2681
|
+
if (titleEl) {
|
|
2682
|
+
const fs = parseFloat(window.getComputedStyle(titleEl).fontSize || "0");
|
|
2683
|
+
if (fs > 0 && fs < 15.5) {
|
|
2684
|
+
console.warn(
|
|
2685
|
+
`[@etamong-playground/ui] <NavigationBar>: title font-size is ${fs.toFixed(1)}px, below the 16px floor. Root font-size may be set < 16px in your app's CSS. The bar uses px not rem to insulate itself; check for an override.`,
|
|
2686
|
+
titleEl
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const above = findStickyAncestorAbove(el);
|
|
2691
|
+
if (above) {
|
|
2692
|
+
console.warn(
|
|
2693
|
+
"[@etamong-playground/ui] <NavigationBar>: detected a sticky/fixed ancestor with padding-top \u2265 20px above this bar. Both will consume env(safe-area-inset-top) \u2014 pass safeAreaTop={false} to this bar (or to the outer one) so the inset is only applied once.",
|
|
2694
|
+
el,
|
|
2695
|
+
above
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
function NavigationBar({
|
|
2700
|
+
title,
|
|
2701
|
+
back,
|
|
2702
|
+
backLabel = "\uB4A4\uB85C",
|
|
2703
|
+
leading,
|
|
2704
|
+
trailing,
|
|
2705
|
+
sticky = true,
|
|
2706
|
+
safeAreaTop = true,
|
|
2707
|
+
borderless = false,
|
|
2708
|
+
fadeOnScroll = true,
|
|
2709
|
+
ariaLabel,
|
|
2710
|
+
className
|
|
2711
|
+
}) {
|
|
2712
|
+
const [scrolled, setScrolled] = useState(false);
|
|
2713
|
+
const rootRef = useRef(null);
|
|
2714
|
+
useEffect(() => {
|
|
2715
|
+
if (!fadeOnScroll || typeof window === "undefined") return;
|
|
2716
|
+
const onScroll = () => setScrolled(window.scrollY > 24);
|
|
2717
|
+
onScroll();
|
|
2718
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
2719
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
2720
|
+
}, [fadeOnScroll]);
|
|
2721
|
+
useEffect(() => {
|
|
2722
|
+
if (!isDevBuild2()) return;
|
|
2723
|
+
const el = rootRef.current;
|
|
2724
|
+
if (!el) return;
|
|
2725
|
+
const target = el;
|
|
2726
|
+
let f1, f2;
|
|
2727
|
+
f1 = requestAnimationFrame(() => {
|
|
2728
|
+
f2 = requestAnimationFrame(() => assertNavbarFits(target));
|
|
2729
|
+
});
|
|
2730
|
+
let resizeT;
|
|
2731
|
+
function onResize() {
|
|
2732
|
+
clearTimeout(resizeT);
|
|
2733
|
+
resizeT = setTimeout(() => assertNavbarFits(target), 150);
|
|
2734
|
+
}
|
|
2735
|
+
window.addEventListener("resize", onResize);
|
|
2736
|
+
return () => {
|
|
2737
|
+
cancelAnimationFrame(f1);
|
|
2738
|
+
cancelAnimationFrame(f2);
|
|
2739
|
+
clearTimeout(resizeT);
|
|
2740
|
+
window.removeEventListener("resize", onResize);
|
|
2741
|
+
};
|
|
2742
|
+
}, []);
|
|
2743
|
+
const onBack = resolveBack(back);
|
|
2744
|
+
const cls = "etu-navbar etu-glass" + (sticky ? " etu-navbar--sticky" : "") + (sticky && !safeAreaTop ? " etu-navbar--no-safe-top" : "") + (borderless ? " etu-navbar--borderless" : "") + (fadeOnScroll && scrolled ? " etu-navbar--scrolled" : "") + (className ? " " + className : "");
|
|
2745
|
+
return /* @__PURE__ */ jsx("header", { className: cls, role: "banner", ref: rootRef, children: /* @__PURE__ */ jsxs("nav", { className: "etu-navbar-inner", "aria-label": ariaLabel ?? title, children: [
|
|
2746
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-navbar-leading", children: [
|
|
2747
|
+
onBack ? /* @__PURE__ */ jsxs(
|
|
2748
|
+
"button",
|
|
2749
|
+
{
|
|
2750
|
+
type: "button",
|
|
2751
|
+
className: "etu-navbar-back",
|
|
2752
|
+
onClick: (e) => {
|
|
2753
|
+
e.preventDefault();
|
|
2754
|
+
onBack();
|
|
2755
|
+
},
|
|
2756
|
+
"aria-label": backLabel,
|
|
2757
|
+
children: [
|
|
2758
|
+
/* @__PURE__ */ jsx(ChevronLeft, {}),
|
|
2759
|
+
/* @__PURE__ */ jsx("span", { className: "etu-navbar-back-label", children: backLabel })
|
|
2760
|
+
]
|
|
2761
|
+
}
|
|
2762
|
+
) : null,
|
|
2763
|
+
leading
|
|
2764
|
+
] }),
|
|
2765
|
+
/* @__PURE__ */ jsx("h1", { className: "etu-navbar-title", title, children: title }),
|
|
2766
|
+
/* @__PURE__ */ jsx("div", { className: "etu-navbar-trailing", children: trailing })
|
|
2767
|
+
] }) });
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/installIOSPwaShell.ts
|
|
2771
|
+
var PWA_CLASS = "etu-pwa-standalone";
|
|
2772
|
+
var IOS_PWA_CLASS = "etu-ios-pwa";
|
|
2773
|
+
function isIOS2() {
|
|
2774
|
+
if (typeof navigator === "undefined") return false;
|
|
2775
|
+
const ua = navigator.userAgent || "";
|
|
2776
|
+
if (/iPad|iPhone|iPod/.test(ua)) return true;
|
|
2777
|
+
return ua.includes("Mac") && typeof document !== "undefined" && navigator.maxTouchPoints > 1;
|
|
2778
|
+
}
|
|
2779
|
+
function isStandalone2() {
|
|
2780
|
+
if (typeof window === "undefined") return false;
|
|
2781
|
+
const nav = window.navigator;
|
|
2782
|
+
if (nav.standalone === true) return true;
|
|
2783
|
+
try {
|
|
2784
|
+
return window.matchMedia("(display-mode: standalone)").matches;
|
|
2785
|
+
} catch {
|
|
2786
|
+
return false;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function installIOSPwaShell() {
|
|
2790
|
+
if (typeof document === "undefined") return;
|
|
2791
|
+
const html = document.documentElement;
|
|
2792
|
+
if (!html) return;
|
|
2793
|
+
const standalone = isStandalone2();
|
|
2794
|
+
const ios = isIOS2();
|
|
2795
|
+
if (standalone) html.classList.add(PWA_CLASS);
|
|
2796
|
+
if (standalone && ios) html.classList.add(IOS_PWA_CLASS);
|
|
2797
|
+
if (standalone && ios) {
|
|
2798
|
+
html.style.setProperty("-webkit-text-size-adjust", "100%");
|
|
2799
|
+
html.style.setProperty("text-size-adjust", "100%");
|
|
2800
|
+
}
|
|
2801
|
+
if (standalone && ios && html.hasAttribute("data-etu-lock-zoom")) {
|
|
2802
|
+
const meta = document.querySelector('meta[name="viewport"]');
|
|
2803
|
+
if (meta) {
|
|
2804
|
+
const content = meta.getAttribute("content") || "";
|
|
2805
|
+
if (!/maximum-scale/.test(content)) {
|
|
2806
|
+
meta.setAttribute("content", content.replace(/\s*$/, "") + ", maximum-scale=1");
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
var LEGAL_HUB_BASE_URL = "https://legal.m.etamong.com";
|
|
2812
|
+
var LEGAL_MANIFEST_URL = `${LEGAL_HUB_BASE_URL}/api/public-manifest`;
|
|
2813
|
+
var KIND_ORDER = { terms: 1, privacy: 2 };
|
|
2814
|
+
var DEFAULT_KIND_LABELS = {
|
|
2815
|
+
terms: "\uC774\uC6A9\uC57D\uAD00",
|
|
2816
|
+
privacy: "\uAC1C\uC778\uC815\uBCF4\uCC98\uB9AC\uBC29\uCE68"
|
|
2817
|
+
};
|
|
2818
|
+
var SOFT_TTL_MS = 60 * 60 * 1e3;
|
|
2819
|
+
var HARD_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2820
|
+
function cacheKey(appSlug, manifestUrl) {
|
|
2821
|
+
return `etu:legal-availability:${manifestUrl}:${appSlug}`;
|
|
2822
|
+
}
|
|
2823
|
+
function readCache(key) {
|
|
2824
|
+
if (typeof window === "undefined") return null;
|
|
2825
|
+
try {
|
|
2826
|
+
const raw = window.localStorage.getItem(key);
|
|
2827
|
+
if (!raw) return null;
|
|
2828
|
+
const parsed = JSON.parse(raw);
|
|
2829
|
+
if (!Array.isArray(parsed.kinds) || typeof parsed.fetchedAt !== "number") return null;
|
|
2830
|
+
return parsed;
|
|
2831
|
+
} catch {
|
|
2832
|
+
return null;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
function writeCache(key, entry) {
|
|
2836
|
+
if (typeof window === "undefined") return;
|
|
2837
|
+
try {
|
|
2838
|
+
window.localStorage.setItem(key, JSON.stringify(entry));
|
|
2839
|
+
} catch {
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
function sortKinds(kinds) {
|
|
2843
|
+
return [...kinds].sort((a, b) => {
|
|
2844
|
+
const ra = KIND_ORDER[a] ?? 99;
|
|
2845
|
+
const rb = KIND_ORDER[b] ?? 99;
|
|
2846
|
+
if (ra !== rb) return ra - rb;
|
|
2847
|
+
return a.localeCompare(b);
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
function pickKinds(manifest, appSlug) {
|
|
2851
|
+
const entry = manifest.services?.[appSlug];
|
|
2852
|
+
if (!entry) return [];
|
|
2853
|
+
const out = [];
|
|
2854
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
2855
|
+
if (k === "identity") continue;
|
|
2856
|
+
if (v) out.push(k);
|
|
2857
|
+
}
|
|
2858
|
+
return sortKinds(out);
|
|
2859
|
+
}
|
|
2860
|
+
function useLegalAvailability(appSlug, options = {}) {
|
|
2861
|
+
const { manifestUrl = LEGAL_MANIFEST_URL, disabled = false } = options;
|
|
2862
|
+
const key = cacheKey(appSlug, manifestUrl);
|
|
2863
|
+
const initial = readCache(key);
|
|
2864
|
+
const initialFresh = initial !== null && Date.now() - initial.fetchedAt < HARD_TTL_MS;
|
|
2865
|
+
const [kinds, setKinds] = useState(initialFresh ? initial.kinds : []);
|
|
2866
|
+
const [status, setStatus] = useState(() => {
|
|
2867
|
+
if (disabled) return "loading";
|
|
2868
|
+
if (!initial) return "loading";
|
|
2869
|
+
if (Date.now() - initial.fetchedAt < SOFT_TTL_MS) return "loaded";
|
|
2870
|
+
return "stale";
|
|
2871
|
+
});
|
|
2872
|
+
const [tick, setTick] = useState(0);
|
|
2873
|
+
const aliveRef = useRef(true);
|
|
2874
|
+
useEffect(() => {
|
|
2875
|
+
aliveRef.current = true;
|
|
2876
|
+
return () => {
|
|
2877
|
+
aliveRef.current = false;
|
|
2878
|
+
};
|
|
2879
|
+
}, []);
|
|
2880
|
+
useEffect(() => {
|
|
2881
|
+
if (disabled) return;
|
|
2882
|
+
const cached = readCache(key);
|
|
2883
|
+
if (cached && Date.now() - cached.fetchedAt < SOFT_TTL_MS && tick === 0) {
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
let cancelled = false;
|
|
2887
|
+
fetch(manifestUrl, { credentials: "omit" }).then(async (res) => {
|
|
2888
|
+
if (!res.ok) throw new Error(`manifest ${res.status}`);
|
|
2889
|
+
const body = await res.json();
|
|
2890
|
+
if (cancelled || !aliveRef.current) return;
|
|
2891
|
+
const next = pickKinds(body, appSlug);
|
|
2892
|
+
setKinds(next);
|
|
2893
|
+
setStatus("loaded");
|
|
2894
|
+
writeCache(key, { kinds: next, fetchedAt: Date.now() });
|
|
2895
|
+
}).catch(() => {
|
|
2896
|
+
if (cancelled || !aliveRef.current) return;
|
|
2897
|
+
setStatus((prev) => prev === "loading" ? "error" : prev);
|
|
2898
|
+
});
|
|
2899
|
+
return () => {
|
|
2900
|
+
cancelled = true;
|
|
2901
|
+
};
|
|
2902
|
+
}, [appSlug, key, manifestUrl, disabled, tick]);
|
|
2903
|
+
const refresh = useCallback(() => setTick((t) => t + 1), []);
|
|
2904
|
+
return { kinds, status, refresh };
|
|
2905
|
+
}
|
|
2906
|
+
function LegalRow({
|
|
2907
|
+
icon,
|
|
2908
|
+
label,
|
|
2909
|
+
trailing,
|
|
2910
|
+
href,
|
|
2911
|
+
onClick,
|
|
2912
|
+
external,
|
|
2913
|
+
isFirst
|
|
2914
|
+
}) {
|
|
2915
|
+
const trailingNode = trailing ?? (external ? "\u2197" : "\u203A");
|
|
2916
|
+
return /* @__PURE__ */ jsxs(
|
|
2917
|
+
"a",
|
|
2918
|
+
{
|
|
2919
|
+
className: "etu-legal-row" + (isFirst ? "" : " etu-legal-row--divided"),
|
|
2920
|
+
href,
|
|
2921
|
+
onClick: (e) => {
|
|
2922
|
+
if (onClick) {
|
|
2923
|
+
e.preventDefault();
|
|
2924
|
+
onClick(e);
|
|
2925
|
+
}
|
|
2926
|
+
},
|
|
2927
|
+
target: external ? "_blank" : void 0,
|
|
2928
|
+
rel: external ? "noopener noreferrer" : void 0,
|
|
2929
|
+
children: [
|
|
2930
|
+
/* @__PURE__ */ jsx("span", { className: "etu-legal-row-icon", "aria-hidden": true, children: icon }),
|
|
2931
|
+
/* @__PURE__ */ jsx("span", { className: "etu-legal-row-label", children: label }),
|
|
2932
|
+
/* @__PURE__ */ jsx("span", { className: "etu-legal-row-trailing", "aria-hidden": true, children: trailingNode })
|
|
2933
|
+
]
|
|
2934
|
+
}
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
function LegalMenuItem({
|
|
2938
|
+
appSlug: _appSlug,
|
|
2939
|
+
to = "/more/legal",
|
|
2940
|
+
onNavigate,
|
|
2941
|
+
icon = "\u2696\uFE0F",
|
|
2942
|
+
label = "\uBC95\uB960 \uC815\uBCF4",
|
|
2943
|
+
isFirst
|
|
2944
|
+
}) {
|
|
2945
|
+
return /* @__PURE__ */ jsx(
|
|
2946
|
+
LegalRow,
|
|
2947
|
+
{
|
|
2948
|
+
icon,
|
|
2949
|
+
label,
|
|
2950
|
+
trailing: "\u203A",
|
|
2951
|
+
href: to,
|
|
2952
|
+
onClick: onNavigate ? () => onNavigate(to) : void 0,
|
|
2953
|
+
isFirst
|
|
2954
|
+
}
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
var KIND_ICONS = {
|
|
2958
|
+
terms: "\u{1F4C4}",
|
|
2959
|
+
privacy: "\u{1F512}"
|
|
2960
|
+
};
|
|
2961
|
+
function LegalPage({
|
|
2962
|
+
appSlug,
|
|
2963
|
+
identityLabel = "\uB85C\uADF8\uC778 \uC815\uCC45",
|
|
2964
|
+
hubBaseUrl = LEGAL_HUB_BASE_URL,
|
|
2965
|
+
kindLabels,
|
|
2966
|
+
anchorOverride,
|
|
2967
|
+
manifestUrl,
|
|
2968
|
+
emptyMessage = "\uD604\uC7AC \uB4F1\uB85D\uB41C \uBC95\uB960 \uBB38\uC11C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2969
|
+
className
|
|
2970
|
+
}) {
|
|
2971
|
+
const { kinds, status } = useLegalAvailability(appSlug, manifestUrl ? { manifestUrl } : void 0);
|
|
2972
|
+
const labelFor = (kind) => kindLabels?.[kind] ?? DEFAULT_KIND_LABELS[kind] ?? kind;
|
|
2973
|
+
const iconFor = (kind) => KIND_ICONS[kind] ?? "\u{1F4DC}";
|
|
2974
|
+
const anchorFor = (kind) => anchorOverride?.[kind] ?? `${appSlug}-${kind}`;
|
|
2975
|
+
const hasL2 = kinds.length > 0;
|
|
2976
|
+
const showEmptyHint = !hasL2 && (status === "loaded" || status === "stale");
|
|
2977
|
+
return /* @__PURE__ */ jsxs("div", { className: "etu-legal-card" + (className ? " " + className : ""), children: [
|
|
2978
|
+
kinds.map((kind, i) => /* @__PURE__ */ jsx(
|
|
2979
|
+
LegalRow,
|
|
2980
|
+
{
|
|
2981
|
+
icon: iconFor(kind),
|
|
2982
|
+
label: labelFor(kind),
|
|
2983
|
+
trailing: "\u2197",
|
|
2984
|
+
href: `${hubBaseUrl}/#${anchorFor(kind)}`,
|
|
2985
|
+
external: true,
|
|
2986
|
+
isFirst: i === 0
|
|
2987
|
+
},
|
|
2988
|
+
kind
|
|
2989
|
+
)),
|
|
2990
|
+
/* @__PURE__ */ jsx(
|
|
2991
|
+
LegalRow,
|
|
2992
|
+
{
|
|
2993
|
+
icon: "\u{1FAAA}",
|
|
2994
|
+
label: identityLabel,
|
|
2995
|
+
trailing: "\u2197",
|
|
2996
|
+
href: `${hubBaseUrl}/#identity`,
|
|
2997
|
+
external: true,
|
|
2998
|
+
isFirst: !hasL2
|
|
2999
|
+
}
|
|
3000
|
+
),
|
|
3001
|
+
showEmptyHint && /* @__PURE__ */ jsx("p", { className: "etu-legal-empty", role: "status", children: emptyMessage })
|
|
3002
|
+
] });
|
|
3003
|
+
}
|
|
3004
|
+
var KIND_LABELS = {
|
|
3005
|
+
privacy: { ko: "\uAC1C\uC778\uC815\uBCF4\uCC98\uB9AC\uBC29\uCE68", en: "Privacy Policy" },
|
|
3006
|
+
terms: { ko: "\uC774\uC6A9\uC57D\uAD00", en: "Terms of Service" },
|
|
3007
|
+
identity: { ko: "\uB85C\uADF8\uC778 \uC815\uCC45", en: "Login Policy" }
|
|
3008
|
+
};
|
|
3009
|
+
function kindLabel(kind, locale) {
|
|
3010
|
+
return KIND_LABELS[kind]?.[locale] ?? kind;
|
|
3011
|
+
}
|
|
3012
|
+
function dismissKey(docs) {
|
|
3013
|
+
return "legal-policy-notice:" + docs.map((d) => `${d.kind}@${d.upcoming.version}`).sort().join(",");
|
|
3014
|
+
}
|
|
3015
|
+
function earliestDate(docs) {
|
|
3016
|
+
return docs.map((d) => d.upcoming.effectiveDate).sort()[0] ?? "";
|
|
3017
|
+
}
|
|
3018
|
+
function isAdverse(docs) {
|
|
3019
|
+
return docs.some((d) => d.upcoming.adverse === true);
|
|
3020
|
+
}
|
|
3021
|
+
var subscribe = () => () => {
|
|
3022
|
+
};
|
|
3023
|
+
function PolicyChangeBanner({
|
|
3024
|
+
appSlug,
|
|
3025
|
+
locale = "ko",
|
|
3026
|
+
hubBaseUrl = LEGAL_HUB_BASE_URL,
|
|
3027
|
+
renderLink,
|
|
3028
|
+
className
|
|
3029
|
+
}) {
|
|
3030
|
+
const isClient = useSyncExternalStore(subscribe, () => true, () => false);
|
|
3031
|
+
const [docs, setDocs] = useState(null);
|
|
3032
|
+
const [dismissed, setDismissed] = useState(false);
|
|
3033
|
+
useEffect(() => {
|
|
3034
|
+
let live = true;
|
|
3035
|
+
fetch(`${hubBaseUrl}/status.json`, { credentials: "omit" }).then((r) => r.ok ? r.json() : Promise.reject(r.status)).then((data) => {
|
|
3036
|
+
if (!live) return;
|
|
3037
|
+
setDocs((data.docs ?? []).filter((d) => d.serviceId === appSlug));
|
|
3038
|
+
}).catch(() => {
|
|
3039
|
+
if (live) setDocs([]);
|
|
3040
|
+
});
|
|
3041
|
+
return () => {
|
|
3042
|
+
live = false;
|
|
3043
|
+
};
|
|
3044
|
+
}, [appSlug, hubBaseUrl]);
|
|
3045
|
+
useEffect(() => {
|
|
3046
|
+
if (!docs || docs.length === 0) return;
|
|
3047
|
+
try {
|
|
3048
|
+
setDismissed(localStorage.getItem(dismissKey(docs)) === "1");
|
|
3049
|
+
} catch {
|
|
3050
|
+
}
|
|
3051
|
+
}, [docs]);
|
|
3052
|
+
if (!isClient || !docs || docs.length === 0 || dismissed) return null;
|
|
3053
|
+
const date = earliestDate(docs);
|
|
3054
|
+
const adverse = isAdverse(docs);
|
|
3055
|
+
const docHref = `${hubBaseUrl}/#${docs[0].docAnchor}`;
|
|
3056
|
+
const docNames = locale === "ko" ? docs.map((d) => kindLabel(d.kind, "ko")).join("\xB7") : docs.map((d) => kindLabel(d.kind, "en")).join(" and ");
|
|
3057
|
+
const bodyText = locale === "ko" ? adverse ? `${docNames}\uC774(\uAC00) ${date}\uBD80\uD130 \uBCC0\uACBD\uB429\uB2C8\uB2E4. \uC774\uC6A9\uC790\uC5D0\uAC8C \uC601\uD5A5\uC774 \uC788\uB294 \uC911\uC694\uD55C \uBCC0\uACBD\uC785\uB2C8\uB2E4.` : `${docNames}\uC774(\uAC00) ${date}\uBD80\uD130 \uBCC0\uACBD\uB429\uB2C8\uB2E4.` : adverse ? `Important change: our ${docNames} will change on ${date}.` : `Our ${docNames} will change on ${date}.`;
|
|
3058
|
+
const linkLabel = locale === "ko" ? "\uC790\uC138\uD788 \uBCF4\uAE30" : "Learn more";
|
|
3059
|
+
const closeLabel = locale === "ko" ? "\uB2EB\uAE30" : "Dismiss";
|
|
3060
|
+
const link = renderLink ? renderLink(docHref, linkLabel) : /* @__PURE__ */ jsx("a", { href: docHref, target: "_blank", rel: "noopener noreferrer", children: linkLabel });
|
|
3061
|
+
function handleDismiss() {
|
|
3062
|
+
try {
|
|
3063
|
+
localStorage.setItem(dismissKey(docs), "1");
|
|
3064
|
+
} catch {
|
|
3065
|
+
}
|
|
3066
|
+
setDismissed(true);
|
|
3067
|
+
}
|
|
3068
|
+
return /* @__PURE__ */ jsxs(
|
|
3069
|
+
"div",
|
|
3070
|
+
{
|
|
3071
|
+
role: "region",
|
|
3072
|
+
"aria-label": locale === "ko" ? "\uBC95\uB960 \uC815\uCC45 \uBCC0\uACBD \uC548\uB0B4" : "Policy change notice",
|
|
3073
|
+
className: [
|
|
3074
|
+
"etu-policy-change-banner",
|
|
3075
|
+
adverse ? "etu-policy-change-banner--adverse" : "",
|
|
3076
|
+
className
|
|
3077
|
+
].filter(Boolean).join(" "),
|
|
3078
|
+
children: [
|
|
3079
|
+
/* @__PURE__ */ jsxs("span", { className: "etu-policy-change-banner-body", children: [
|
|
3080
|
+
bodyText,
|
|
3081
|
+
" ",
|
|
3082
|
+
link
|
|
3083
|
+
] }),
|
|
3084
|
+
/* @__PURE__ */ jsx(
|
|
3085
|
+
"button",
|
|
3086
|
+
{
|
|
3087
|
+
type: "button",
|
|
3088
|
+
className: "etu-policy-change-banner-close",
|
|
3089
|
+
"aria-label": closeLabel,
|
|
3090
|
+
onClick: handleDismiss,
|
|
3091
|
+
children: "\u2715"
|
|
3092
|
+
}
|
|
3093
|
+
)
|
|
3094
|
+
]
|
|
3095
|
+
}
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
function getCell(col, row) {
|
|
3099
|
+
if (col.render) return col.render(row);
|
|
3100
|
+
const v = row[col.key];
|
|
3101
|
+
if (v === null || v === void 0 || v === "") return "\u2014";
|
|
3102
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
3103
|
+
return String(v);
|
|
3104
|
+
}
|
|
3105
|
+
return v;
|
|
3106
|
+
}
|
|
3107
|
+
function cellClass(col) {
|
|
3108
|
+
const out = ["etu-dt-cell"];
|
|
3109
|
+
if (col.nowrap) out.push("etu-dt-nowrap");
|
|
3110
|
+
if (col.align && col.align !== "left") out.push(`etu-dt-align-${col.align}`);
|
|
3111
|
+
if (col.className) out.push(col.className);
|
|
3112
|
+
return out.join(" ");
|
|
3113
|
+
}
|
|
3114
|
+
function DataTable(props) {
|
|
3115
|
+
const { rows, rowKey, rowActions, emptyState, className, mode = "auto" } = props;
|
|
3116
|
+
const cols = props.columns.filter((c) => !c.hidden);
|
|
3117
|
+
const primaryKey = props.primaryColumn ?? cols[0]?.key;
|
|
3118
|
+
const outerCls = [
|
|
3119
|
+
"etu-dt",
|
|
3120
|
+
mode === "wide" ? "etu-dt--force-wide" : "",
|
|
3121
|
+
mode === "cards" ? "etu-dt--force-cards" : "",
|
|
3122
|
+
className ?? ""
|
|
3123
|
+
].filter(Boolean).join(" ");
|
|
3124
|
+
if (rows.length === 0 && emptyState) {
|
|
3125
|
+
return /* @__PURE__ */ jsx("div", { className: outerCls, children: emptyState });
|
|
3126
|
+
}
|
|
3127
|
+
const wideCols = cols.filter((c) => !c.hiddenWide);
|
|
3128
|
+
const cardCols = cols.filter((c) => !c.hiddenNarrow);
|
|
3129
|
+
return /* @__PURE__ */ jsxs("div", { className: outerCls, children: [
|
|
3130
|
+
/* @__PURE__ */ jsxs("table", { className: "etu-dt-table", role: "table", children: [
|
|
3131
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
3132
|
+
wideCols.map((c) => /* @__PURE__ */ jsx(
|
|
3133
|
+
"th",
|
|
3134
|
+
{
|
|
3135
|
+
scope: "col",
|
|
3136
|
+
className: cellClass(c),
|
|
3137
|
+
style: c.width ? { width: c.width } : void 0,
|
|
3138
|
+
children: c.label
|
|
3139
|
+
},
|
|
3140
|
+
c.key
|
|
3141
|
+
)),
|
|
3142
|
+
rowActions ? /* @__PURE__ */ jsx("th", { className: "etu-dt-cell etu-dt-nowrap", "aria-label": "actions" }) : null
|
|
3143
|
+
] }) }),
|
|
3144
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
3145
|
+
wideCols.map((c) => /* @__PURE__ */ jsx("td", { className: cellClass(c), children: getCell(c, row) }, c.key)),
|
|
3146
|
+
rowActions ? /* @__PURE__ */ jsx("td", { className: "etu-dt-cell etu-dt-nowrap etu-dt-actions", children: rowActions(row) }) : null
|
|
3147
|
+
] }, rowKey(row, i))) })
|
|
3148
|
+
] }),
|
|
3149
|
+
/* @__PURE__ */ jsx("ul", { className: "etu-dt-cards", role: "list", children: rows.map((row, i) => {
|
|
3150
|
+
const primary = primaryKey ? cardCols.find((c) => c.key === primaryKey) : void 0;
|
|
3151
|
+
const rest = cardCols.filter((c) => c.key !== primaryKey);
|
|
3152
|
+
return /* @__PURE__ */ jsxs("li", { className: "etu-dt-card", children: [
|
|
3153
|
+
primary ? /* @__PURE__ */ jsx("div", { className: "etu-dt-card-header", children: getCell(primary, row) }) : null,
|
|
3154
|
+
/* @__PURE__ */ jsx("dl", { className: "etu-dt-card-fields", children: rest.map((c) => /* @__PURE__ */ jsxs("div", { className: "etu-dt-card-field", children: [
|
|
3155
|
+
/* @__PURE__ */ jsx("dt", { className: "etu-dt-card-label", children: c.label }),
|
|
3156
|
+
/* @__PURE__ */ jsx("dd", { className: "etu-dt-card-value" + (c.nowrap ? " etu-dt-nowrap" : ""), children: getCell(c, row) })
|
|
3157
|
+
] }, c.key)) }),
|
|
3158
|
+
rowActions ? /* @__PURE__ */ jsx("div", { className: "etu-dt-card-actions", children: rowActions(row) }) : null
|
|
3159
|
+
] }, rowKey(row, i));
|
|
3160
|
+
}) })
|
|
3161
|
+
] });
|
|
3162
|
+
}
|
|
3163
|
+
function buildSkillMarkdown(skill) {
|
|
3164
|
+
const fm = `---
|
|
3165
|
+
name: ${skill.name}
|
|
3166
|
+
description: ${skill.description.replace(/\n/g, " ")}
|
|
3167
|
+
---`;
|
|
3168
|
+
const body = skill.body.trim();
|
|
3169
|
+
return `${fm}
|
|
3170
|
+
|
|
3171
|
+
${body}
|
|
3172
|
+
`;
|
|
3173
|
+
}
|
|
3174
|
+
function downloadSkill(skill) {
|
|
3175
|
+
if (typeof document === "undefined") return;
|
|
3176
|
+
const blob = new Blob([buildSkillMarkdown(skill)], { type: "text/markdown" });
|
|
3177
|
+
const url = URL.createObjectURL(blob);
|
|
3178
|
+
const a = document.createElement("a");
|
|
3179
|
+
a.href = url;
|
|
3180
|
+
a.download = `${skill.name}.md`;
|
|
3181
|
+
document.body.appendChild(a);
|
|
3182
|
+
a.click();
|
|
3183
|
+
a.remove();
|
|
3184
|
+
URL.revokeObjectURL(url);
|
|
3185
|
+
}
|
|
3186
|
+
function curlOneLiner(publicUrl, slug) {
|
|
3187
|
+
return `curl -fsSL ${publicUrl} -o ~/.claude/skills/${slug}/SKILL.md`;
|
|
3188
|
+
}
|
|
3189
|
+
function defaultSkillUsageSection(skill) {
|
|
3190
|
+
const filename = `${skill.name}.md`;
|
|
3191
|
+
const oneLiner = skill.publicUrl ? curlOneLiner(skill.publicUrl, skill.name) : void 0;
|
|
3192
|
+
return {
|
|
3193
|
+
id: "__skill_usage__",
|
|
3194
|
+
label: "Claude skill \uC0AC\uC6A9\uBC95",
|
|
3195
|
+
summary: oneLiner ? "\uD55C \uC904 install \uB610\uB294 \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC\uB85C LLM\uACFC \uD568\uAED8 \uC4F0\uB294 \uBC95." : "\uB2E4\uC6B4\uB85C\uB4DC\uD55C .md \uD30C\uC77C\uC744 LLM\uACFC \uD568\uAED8 \uC4F0\uB294 \uBC95.",
|
|
3196
|
+
content: /* @__PURE__ */ jsxs("div", { className: "etu-docs-hub-skill-usage", children: [
|
|
3197
|
+
oneLiner ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3198
|
+
/* @__PURE__ */ jsx("p", { children: "\uC544\uB798 \uD55C \uC904\uC774\uBA74 Claude Code \uAC00 \uB2E4\uC74C \uC138\uC158\uBD80\uD130 \uC774 \uC2A4\uD0AC\uC744 \uC778\uC2DD\uD574\uC694. \uD5E4\uB4DC\uB9AC\uC2A4/CI/SSH \uC5B4\uB514\uC11C\uB098 \uAC19\uC740 \uBA85\uB839\uC73C\uB85C \uAE54\uB9AC\uACE0, \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uCD5C\uC2E0 \uBC30\uD3EC\uBCF8\uC73C\uB85C \uB36E\uC5B4\uC368\uC838\uC694." }),
|
|
3199
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-docs-hub-skill-install", children: [
|
|
3200
|
+
/* @__PURE__ */ jsx("code", { className: "etu-docs-hub-skill-install-cmd", children: oneLiner }),
|
|
3201
|
+
/* @__PURE__ */ jsx(CopyButton, { value: oneLiner, label: "\uBA85\uB839 \uBCF5\uC0AC" })
|
|
3202
|
+
] }),
|
|
3203
|
+
/* @__PURE__ */ jsxs("p", { className: "etu-docs-hub-skill-install-note", children: [
|
|
3204
|
+
'\uD30C\uC77C\uC774 \uD544\uC694\uD558\uBA74 \uC6B0\uCE21 \uC704 "\u{1F4E5}" \uBC84\uD2BC\uC73C\uB85C ',
|
|
3205
|
+
/* @__PURE__ */ jsx("code", { children: filename }),
|
|
3206
|
+
" \uC744 \uC9C1\uC811 \uBC1B\uC544 ",
|
|
3207
|
+
/* @__PURE__ */ jsxs("code", { children: [
|
|
3208
|
+
"~/.claude/skills/",
|
|
3209
|
+
skill.name,
|
|
3210
|
+
"/SKILL.md"
|
|
3211
|
+
] }),
|
|
3212
|
+
" \uC5D0 \uB450\uC154\uB3C4 \uB3FC\uC694 (\uC624\uD504\uB77C\uC778/\uC5D0\uC5B4\uAC2D)."
|
|
3213
|
+
] })
|
|
3214
|
+
] }) : /* @__PURE__ */ jsxs("p", { children: [
|
|
3215
|
+
'\uC704 "\u{1F4E5}" \uBC84\uD2BC\uC774 ',
|
|
3216
|
+
/* @__PURE__ */ jsx("code", { children: filename }),
|
|
3217
|
+
" \uD30C\uC77C \uD55C \uAC1C\uB97C \uB0B4\uB824\uBC1B\uC544\uC694. \uADF8 \uC548\uC5D0 \uC774 \uD398\uC774\uC9C0\uC758 \uD575\uC2EC\uC774 \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uB4E4\uC5B4 \uC788\uC5B4\uC11C, \uC5B4\uB290 LLM\uACFC\uB3C4 \uADF8\uB300\uB85C \uAC19\uC774 \uC4F8 \uC218 \uC788\uC5B4\uC694."
|
|
3218
|
+
] }),
|
|
3219
|
+
/* @__PURE__ */ jsx("h4", { children: "Claude Code (CLI)" }),
|
|
3220
|
+
oneLiner ? /* @__PURE__ */ jsxs("p", { children: [
|
|
3221
|
+
'\uD55C \uC904 install \uD6C4 Claude Code \uB97C \uC7AC\uC2DC\uC791\uD558\uAC70\uB098 \uC0C8 \uC138\uC158\uC744 \uC5F4\uBA74 \uC2A4\uD0AC\uC774 \uC790\uB3D9\uC73C\uB85C \uC778\uC2DD\uB3FC\uC694. \uB300\uD654 \uC911\uC5D0 "',
|
|
3222
|
+
/* @__PURE__ */ jsx("em", { children: skill.name }),
|
|
3223
|
+
' \uC2A4\uD0AC \uC368\uC11C\u2026" \uAC19\uC774 \uBD80\uB974\uBA74 Claude \uAC00 \uC774 \uAC00\uC774\uB4DC\uB97C \uBC14\uD0D5\uC73C\uB85C \uB3D9\uC791\uD574\uC694.'
|
|
3224
|
+
] }) : /* @__PURE__ */ jsxs("ol", { children: [
|
|
3225
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
3226
|
+
/* @__PURE__ */ jsxs("code", { children: [
|
|
3227
|
+
"~/.claude/skills/",
|
|
3228
|
+
skill.name,
|
|
3229
|
+
"/SKILL.md"
|
|
3230
|
+
] }),
|
|
3231
|
+
" \uACBD\uB85C\uB85C \uC62E\uAE30\uC138\uC694 (\uB514\uB809\uD130\uB9AC \uC774\uB984\uACFC \uD30C\uC77C \uC774\uB984\uC740 \uC704\uC640 \uADF8\uB300\uB85C)."
|
|
3232
|
+
] }),
|
|
3233
|
+
/* @__PURE__ */ jsx("li", { children: "Claude Code\uB97C \uC7AC\uC2DC\uC791\uD558\uAC70\uB098 \uC0C8 \uC138\uC158\uC744 \uC5F4\uBA74 \uC2A4\uD0AC\uC774 \uC790\uB3D9\uC73C\uB85C \uC778\uC2DD\uB3FC\uC694." }),
|
|
3234
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
3235
|
+
'\uB300\uD654 \uC911\uC5D0 "',
|
|
3236
|
+
/* @__PURE__ */ jsx("em", { children: skill.name }),
|
|
3237
|
+
' \uC2A4\uD0AC \uC368\uC11C\u2026" \uAC19\uC774 \uBD80\uB974\uBA74 Claude\uAC00 \uC774 \uAC00\uC774\uB4DC\uB97C \uBC14\uD0D5\uC73C\uB85C \uB3D9\uC791\uD574\uC694.'
|
|
3238
|
+
] })
|
|
3239
|
+
] }),
|
|
3240
|
+
/* @__PURE__ */ jsx("h4", { children: "Codex / \uAE30\uD0C0 \uCF54\uB529 \uC5D0\uC774\uC804\uD2B8" }),
|
|
3241
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
3242
|
+
"\uD234\uC774 \uAD8C\uC7A5\uD558\uB294 \uC704\uCE58(\uC608: ",
|
|
3243
|
+
/* @__PURE__ */ jsx("code", { children: "~/.codex/skills/" }),
|
|
3244
|
+
",",
|
|
3245
|
+
/* @__PURE__ */ jsx("code", { children: "~/.codex/prompts/" }),
|
|
3246
|
+
")\uC5D0 \uAC19\uC740 \uD30C\uC77C\uC744 \uB450\uAC70\uB098, \uC138\uC158\uC744 \uC2DC\uC791\uD560 \uB54C \uCCA8\uBD80 \uD30C\uC77C\uB85C \uC62C\uB9AC\uC138\uC694. \uD504\uB860\uD2B8\uB9E4\uD130\uB294 \uC774\uB984\xB7\uC124\uBA85\uB9CC \uB2F4\uACA8 \uC788\uC5B4\uC11C \uC2DC\uC2A4\uD15C \uD504\uB86C\uD504\uD2B8 / \uC9C0\uC2DC\uBB38 \uC5B4\uB290 \uC790\uB9AC\uC5D0 \uB123\uC5B4\uB3C4 \uBB34\uD574\uD574\uC694."
|
|
3247
|
+
] }),
|
|
3248
|
+
/* @__PURE__ */ jsx("h4", { children: "\uADF8 \uC678 LLM (ChatGPT, Gemini, \uC790\uCCB4 \uBD07 \uB4F1)" }),
|
|
3249
|
+
/* @__PURE__ */ jsx("p", { children: skill.publicUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3250
|
+
"\uD30C\uC77C\uC744 \uC5F4\uC5B4 \uBCF8\uBB38\uC744 \uD1B5\uC9F8\uB85C \uCCAB \uBA54\uC2DC\uC9C0(\uB610\uB294 \uC2DC\uC2A4\uD15C \uD504\uB86C\uD504\uD2B8)\uC5D0 \uBD99\uC5EC \uB123\uAC70\uB098, ",
|
|
3251
|
+
/* @__PURE__ */ jsx("code", { children: skill.publicUrl }),
|
|
3252
|
+
" \uC744 \uAC00\uC838\uC624\uAC8C \uD574\uB3C4 \uB3FC\uC694. \uC9E7\uC740 \uD55C \uD30C\uC77C\uC774\uB77C \uCEE8\uD14D\uC2A4\uD2B8 \uBD80\uB2F4\uC774 \uAC70\uC758 \uC5C6\uC5B4\uC694."
|
|
3253
|
+
] }) : /* @__PURE__ */ jsx(Fragment, { children: "\uD30C\uC77C\uC744 \uC5F4\uC5B4 \uBCF8\uBB38\uC744 \uD1B5\uC9F8\uB85C \uCCAB \uBA54\uC2DC\uC9C0(\uB610\uB294 \uC2DC\uC2A4\uD15C \uD504\uB86C\uD504\uD2B8)\uC5D0 \uBD99\uC5EC \uB123\uC73C\uBA74 \uB3FC\uC694. \uC9E7\uC740 \uD55C \uD30C\uC77C\uC774\uB77C \uCEE8\uD14D\uC2A4\uD2B8 \uBD80\uB2F4\uC774 \uAC70\uC758 \uC5C6\uC5B4\uC694." }) }),
|
|
3254
|
+
/* @__PURE__ */ jsx("h4", { children: "\uC5C5\uB370\uC774\uD2B8" }),
|
|
3255
|
+
/* @__PURE__ */ jsx("p", { children: oneLiner ? "\uC774 \uD398\uC774\uC9C0\uC5D0 \uC0C8 \uAE30\uB2A5\uC774 \uBC18\uC601\uB418\uBA74 \uAC19\uC740 \uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uB36E\uC5B4\uC4F0\uBA74 \uB3FC\uC694. URL \uC740 \uB298 \uCD5C\uC2E0 \uBC30\uD3EC\uBCF8\uC744 \uAC00\uB9AC\uCF1C\uC694." : "\uC774 \uD398\uC774\uC9C0\uC5D0 \uC0C8 \uAE30\uB2A5\uC774 \uBC18\uC601\uB418\uBA74 \uAC19\uC740 \uBC84\uD2BC\uC73C\uB85C \uB2E4\uC2DC \uBC1B\uC544 \uB36E\uC5B4\uC4F0\uBA74 \uB3FC\uC694. \uC2A4\uD0AC\uC740 \uC571 \uBCC0\uACBD\uACFC \uAC19\uC740 \uCEE4\uBC0B\uC5D0\uC11C \uAC31\uC2E0\uB3FC\uC694 \u2014 \uB298 \uCD5C\uC2E0." })
|
|
3256
|
+
] })
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
function DocsHub({
|
|
3260
|
+
appName,
|
|
3261
|
+
description,
|
|
3262
|
+
sections,
|
|
3263
|
+
skill,
|
|
3264
|
+
sectionId,
|
|
3265
|
+
onSectionChange,
|
|
3266
|
+
defaultSectionId,
|
|
3267
|
+
className
|
|
3268
|
+
}) {
|
|
3269
|
+
const allSections = skill ? skill.usageSection === null ? sections : [...sections, skill.usageSection ?? defaultSkillUsageSection(skill)] : sections;
|
|
3270
|
+
const first = allSections[0]?.id ?? "";
|
|
3271
|
+
const [uncontrolled, setUncontrolled] = useState(defaultSectionId ?? first);
|
|
3272
|
+
const activeId = sectionId ?? uncontrolled;
|
|
3273
|
+
const active = allSections.find((s) => s.id === activeId) ?? allSections[0];
|
|
3274
|
+
const select = (id) => {
|
|
3275
|
+
if (sectionId === void 0) setUncontrolled(id);
|
|
3276
|
+
onSectionChange?.(id);
|
|
3277
|
+
};
|
|
3278
|
+
return /* @__PURE__ */ jsxs("div", { className: "etu-docs-hub" + (className ? " " + className : ""), children: [
|
|
3279
|
+
/* @__PURE__ */ jsxs("header", { className: "etu-docs-hub-head", children: [
|
|
3280
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-docs-hub-head-text", children: [
|
|
3281
|
+
appName && /* @__PURE__ */ jsx("h1", { className: "etu-docs-hub-app-name", children: appName }),
|
|
3282
|
+
description && /* @__PURE__ */ jsx("p", { className: "etu-docs-hub-description", children: description })
|
|
3283
|
+
] }),
|
|
3284
|
+
skill && /* @__PURE__ */ jsx(
|
|
3285
|
+
"button",
|
|
3286
|
+
{
|
|
3287
|
+
type: "button",
|
|
3288
|
+
className: "etu-docs-hub-skill-btn",
|
|
3289
|
+
onClick: () => downloadSkill(skill),
|
|
3290
|
+
title: `${skill.name}.md`,
|
|
3291
|
+
children: skill.buttonLabel ?? "\u{1F4E5} Claude skill \uBC1B\uAE30"
|
|
3292
|
+
}
|
|
3293
|
+
)
|
|
3294
|
+
] }),
|
|
3295
|
+
/* @__PURE__ */ jsxs("div", { className: "etu-docs-hub-body", children: [
|
|
3296
|
+
/* @__PURE__ */ jsx("nav", { className: "etu-docs-hub-nav", "aria-label": "\uBB38\uC11C \uC139\uC158", children: allSections.map((s) => /* @__PURE__ */ jsx(
|
|
3297
|
+
"button",
|
|
3298
|
+
{
|
|
3299
|
+
type: "button",
|
|
3300
|
+
className: "etu-docs-hub-nav-item" + (s.id === active?.id ? " etu-docs-hub-nav-item--active" : ""),
|
|
3301
|
+
onClick: () => select(s.id),
|
|
3302
|
+
"aria-current": s.id === active?.id ? "page" : void 0,
|
|
3303
|
+
children: s.label
|
|
3304
|
+
},
|
|
3305
|
+
s.id
|
|
3306
|
+
)) }),
|
|
3307
|
+
/* @__PURE__ */ jsxs("article", { className: "etu-docs-hub-section", id: `docs-section-${active?.id}`, children: [
|
|
3308
|
+
active?.summary && /* @__PURE__ */ jsx("p", { className: "etu-docs-hub-section-summary", children: active.summary }),
|
|
3309
|
+
active?.content
|
|
3310
|
+
] })
|
|
3311
|
+
] })
|
|
3312
|
+
] });
|
|
3313
|
+
}
|
|
3314
|
+
function buildSkillMarkdownText(skill) {
|
|
3315
|
+
return buildSkillMarkdown(skill);
|
|
3316
|
+
}
|
|
3317
|
+
var FLEET_LOGIN = "/auth/login";
|
|
3318
|
+
var FLEET_LOGOUT = "/auth/logout";
|
|
3319
|
+
var FLEET_ME = "/api/me";
|
|
3320
|
+
var REFRESH_EVENT2 = "etu:me-refresh";
|
|
3321
|
+
var SESSION_EXPIRED_EVENT = "etu:session-expired";
|
|
3322
|
+
var SHARE_CRAWLER_UA_SUBSTRINGS = [
|
|
3323
|
+
"kakaotalk-scrap",
|
|
3324
|
+
"slackbot",
|
|
3325
|
+
"facebookexternalhit",
|
|
3326
|
+
"twitterbot",
|
|
3327
|
+
"discordbot",
|
|
3328
|
+
"linkedinbot",
|
|
3329
|
+
"telegrambot",
|
|
3330
|
+
"whatsapp",
|
|
3331
|
+
"line-poker"
|
|
3332
|
+
];
|
|
3333
|
+
function isShareCrawler(ua) {
|
|
3334
|
+
if (!ua) return false;
|
|
3335
|
+
const u = ua.toLowerCase();
|
|
3336
|
+
for (const needle of SHARE_CRAWLER_UA_SUBSTRINGS) {
|
|
3337
|
+
if (u.includes(needle)) return true;
|
|
3338
|
+
}
|
|
3339
|
+
return false;
|
|
3340
|
+
}
|
|
3341
|
+
function currentRd2() {
|
|
3342
|
+
if (typeof window === "undefined") return "/";
|
|
3343
|
+
return window.location.pathname + window.location.search + window.location.hash;
|
|
3344
|
+
}
|
|
3345
|
+
function fleetLoginUrl(rd) {
|
|
3346
|
+
return FLEET_LOGIN + "?rd=" + encodeURIComponent(rd ?? currentRd2());
|
|
3347
|
+
}
|
|
3348
|
+
function fleetLogoutUrl(rd) {
|
|
3349
|
+
return FLEET_LOGOUT + "?rd=" + encodeURIComponent(rd ?? "/");
|
|
3350
|
+
}
|
|
3351
|
+
function fleetSignIn(rd) {
|
|
3352
|
+
if (typeof window !== "undefined") window.location.href = fleetLoginUrl(rd);
|
|
3353
|
+
}
|
|
3354
|
+
function fleetSignOut(rd) {
|
|
3355
|
+
if (typeof window !== "undefined") window.location.href = fleetLogoutUrl(rd);
|
|
3356
|
+
}
|
|
3357
|
+
function useIdentity(opts = {}) {
|
|
3358
|
+
const base = useMe({ endpoint: FLEET_ME, treat401AsAnonymous: true, ...opts });
|
|
3359
|
+
return { ...base, signIn: fleetSignIn, signOut: fleetSignOut };
|
|
3360
|
+
}
|
|
3361
|
+
function AuthGate({
|
|
3362
|
+
children,
|
|
3363
|
+
rd,
|
|
3364
|
+
loadingFallback = null,
|
|
3365
|
+
userAgent,
|
|
3366
|
+
disableRedirect
|
|
3367
|
+
}) {
|
|
3368
|
+
const ua = userAgent ?? (typeof navigator !== "undefined" ? navigator.userAgent : void 0);
|
|
3369
|
+
const crawler = isShareCrawler(ua);
|
|
3370
|
+
const { me, loading } = useIdentity();
|
|
3371
|
+
useEffect(() => {
|
|
3372
|
+
if (crawler) return;
|
|
3373
|
+
if (loading) return;
|
|
3374
|
+
if (me) return;
|
|
3375
|
+
if (disableRedirect) return;
|
|
3376
|
+
fleetSignIn(rd);
|
|
3377
|
+
}, [crawler, loading, me, rd, disableRedirect]);
|
|
3378
|
+
if (crawler) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
3379
|
+
if (loading) return /* @__PURE__ */ jsx(Fragment, { children: loadingFallback });
|
|
3380
|
+
if (!me) return null;
|
|
3381
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
3382
|
+
}
|
|
3383
|
+
var baseBtn = {
|
|
3384
|
+
display: "inline-flex",
|
|
3385
|
+
alignItems: "center",
|
|
3386
|
+
gap: "0.4rem",
|
|
3387
|
+
padding: "0.45rem 0.85rem",
|
|
3388
|
+
borderRadius: "var(--etu-radius-md, 0.5rem)",
|
|
3389
|
+
border: "1px solid var(--etu-border, rgba(255,255,255,0.12))",
|
|
3390
|
+
background: "var(--etu-surface, transparent)",
|
|
3391
|
+
color: "var(--etu-fg, inherit)",
|
|
3392
|
+
cursor: "pointer",
|
|
3393
|
+
fontSize: "0.875rem",
|
|
3394
|
+
lineHeight: 1
|
|
3395
|
+
};
|
|
3396
|
+
function LoginButton({
|
|
3397
|
+
rd,
|
|
3398
|
+
label = "Sign in",
|
|
3399
|
+
style,
|
|
3400
|
+
...rest
|
|
3401
|
+
}) {
|
|
3402
|
+
return /* @__PURE__ */ jsx(
|
|
3403
|
+
"button",
|
|
3404
|
+
{
|
|
3405
|
+
type: "button",
|
|
3406
|
+
"data-etu-auth": "login",
|
|
3407
|
+
style: { ...baseBtn, ...style },
|
|
3408
|
+
onClick: () => fleetSignIn(rd),
|
|
3409
|
+
...rest,
|
|
3410
|
+
children: label
|
|
3411
|
+
}
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
function LogoutButton({
|
|
3415
|
+
rd,
|
|
3416
|
+
label = "Sign out",
|
|
3417
|
+
style,
|
|
3418
|
+
...rest
|
|
3419
|
+
}) {
|
|
3420
|
+
return /* @__PURE__ */ jsx(
|
|
3421
|
+
"button",
|
|
3422
|
+
{
|
|
3423
|
+
type: "button",
|
|
3424
|
+
"data-etu-auth": "logout",
|
|
3425
|
+
style: { ...baseBtn, ...style },
|
|
3426
|
+
onClick: () => fleetSignOut(rd),
|
|
3427
|
+
...rest,
|
|
3428
|
+
children: label
|
|
3429
|
+
}
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
function SessionBadge({
|
|
3433
|
+
me: overrideMe,
|
|
3434
|
+
onClick,
|
|
3435
|
+
className
|
|
3436
|
+
}) {
|
|
3437
|
+
const { me: hookMe } = useIdentity();
|
|
3438
|
+
const me = overrideMe ?? hookMe;
|
|
3439
|
+
if (!me) return null;
|
|
3440
|
+
const label = me.name || me.preferred_username || me.email;
|
|
3441
|
+
const initial = (label || "?").trim().charAt(0).toUpperCase();
|
|
3442
|
+
return /* @__PURE__ */ jsxs(
|
|
3443
|
+
"button",
|
|
3444
|
+
{
|
|
3445
|
+
type: "button",
|
|
3446
|
+
"data-etu-auth": "session-badge",
|
|
3447
|
+
className,
|
|
3448
|
+
onClick,
|
|
3449
|
+
style: {
|
|
3450
|
+
display: "flex",
|
|
3451
|
+
alignItems: "center",
|
|
3452
|
+
gap: "0.55rem",
|
|
3453
|
+
padding: "0.35rem 0.55rem",
|
|
3454
|
+
borderRadius: "var(--etu-radius-md, 0.5rem)",
|
|
3455
|
+
border: "1px solid transparent",
|
|
3456
|
+
background: "transparent",
|
|
3457
|
+
color: "var(--etu-fg, inherit)",
|
|
3458
|
+
cursor: onClick ? "pointer" : "default",
|
|
3459
|
+
width: "100%",
|
|
3460
|
+
textAlign: "left",
|
|
3461
|
+
fontSize: "0.85rem",
|
|
3462
|
+
lineHeight: 1.2
|
|
3463
|
+
},
|
|
3464
|
+
children: [
|
|
3465
|
+
/* @__PURE__ */ jsx(
|
|
3466
|
+
"span",
|
|
3467
|
+
{
|
|
3468
|
+
"aria-hidden": true,
|
|
3469
|
+
style: {
|
|
3470
|
+
width: "1.6rem",
|
|
3471
|
+
height: "1.6rem",
|
|
3472
|
+
borderRadius: "999px",
|
|
3473
|
+
background: "var(--etu-accent, #0d9488)",
|
|
3474
|
+
color: "#fff",
|
|
3475
|
+
display: "inline-flex",
|
|
3476
|
+
alignItems: "center",
|
|
3477
|
+
justifyContent: "center",
|
|
3478
|
+
fontWeight: 600,
|
|
3479
|
+
fontSize: "0.85rem",
|
|
3480
|
+
flex: "0 0 auto"
|
|
3481
|
+
},
|
|
3482
|
+
children: initial
|
|
3483
|
+
}
|
|
3484
|
+
),
|
|
3485
|
+
/* @__PURE__ */ jsx(
|
|
3486
|
+
"span",
|
|
3487
|
+
{
|
|
3488
|
+
style: {
|
|
3489
|
+
overflow: "hidden",
|
|
3490
|
+
textOverflow: "ellipsis",
|
|
3491
|
+
whiteSpace: "nowrap",
|
|
3492
|
+
minWidth: 0,
|
|
3493
|
+
flex: 1
|
|
3494
|
+
},
|
|
3495
|
+
children: label
|
|
3496
|
+
}
|
|
3497
|
+
)
|
|
3498
|
+
]
|
|
3499
|
+
}
|
|
3500
|
+
);
|
|
3501
|
+
}
|
|
3502
|
+
function SessionExpiredDialog({
|
|
3503
|
+
title = "Session expired",
|
|
3504
|
+
message = "Your session ended. Sign in again to continue.",
|
|
3505
|
+
signInLabel = "Sign in",
|
|
3506
|
+
className
|
|
3507
|
+
}) {
|
|
3508
|
+
const [open, setOpen] = useState(false);
|
|
3509
|
+
useEffect(() => {
|
|
3510
|
+
if (typeof window === "undefined") return;
|
|
3511
|
+
const onExpired = () => setOpen(true);
|
|
3512
|
+
window.addEventListener(SESSION_EXPIRED_EVENT, onExpired);
|
|
3513
|
+
return () => window.removeEventListener(SESSION_EXPIRED_EVENT, onExpired);
|
|
3514
|
+
}, []);
|
|
3515
|
+
const onSignIn = useCallback(() => fleetSignIn(), []);
|
|
3516
|
+
if (!open) return null;
|
|
3517
|
+
return /* @__PURE__ */ jsx(
|
|
3518
|
+
"div",
|
|
3519
|
+
{
|
|
3520
|
+
role: "dialog",
|
|
3521
|
+
"aria-modal": "true",
|
|
3522
|
+
"aria-labelledby": "etu-session-expired-title",
|
|
3523
|
+
className,
|
|
3524
|
+
style: {
|
|
3525
|
+
position: "fixed",
|
|
3526
|
+
inset: 0,
|
|
3527
|
+
background: "rgba(0,0,0,0.45)",
|
|
3528
|
+
display: "flex",
|
|
3529
|
+
alignItems: "center",
|
|
3530
|
+
justifyContent: "center",
|
|
3531
|
+
zIndex: 9999
|
|
3532
|
+
},
|
|
3533
|
+
children: /* @__PURE__ */ jsxs(
|
|
3534
|
+
"div",
|
|
3535
|
+
{
|
|
3536
|
+
style: {
|
|
3537
|
+
maxWidth: "22rem",
|
|
3538
|
+
width: "calc(100% - 2rem)",
|
|
3539
|
+
background: "var(--etu-surface, #1f2937)",
|
|
3540
|
+
color: "var(--etu-fg, #f9fafb)",
|
|
3541
|
+
borderRadius: "var(--etu-radius-lg, 0.75rem)",
|
|
3542
|
+
padding: "1.25rem 1.25rem 1rem",
|
|
3543
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.45)"
|
|
3544
|
+
},
|
|
3545
|
+
children: [
|
|
3546
|
+
/* @__PURE__ */ jsx(
|
|
3547
|
+
"h2",
|
|
3548
|
+
{
|
|
3549
|
+
id: "etu-session-expired-title",
|
|
3550
|
+
style: { margin: "0 0 0.5rem", fontSize: "1.05rem" },
|
|
3551
|
+
children: title
|
|
3552
|
+
}
|
|
3553
|
+
),
|
|
3554
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "0 0 1rem", fontSize: "0.9rem", opacity: 0.85 }, children: message }),
|
|
3555
|
+
/* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-end" }, children: /* @__PURE__ */ jsx(
|
|
3556
|
+
"button",
|
|
3557
|
+
{
|
|
3558
|
+
type: "button",
|
|
3559
|
+
"data-etu-auth": "session-expired-signin",
|
|
3560
|
+
style: {
|
|
3561
|
+
...baseBtn,
|
|
3562
|
+
background: "var(--etu-accent, #0d9488)",
|
|
3563
|
+
borderColor: "transparent",
|
|
3564
|
+
color: "#fff"
|
|
3565
|
+
},
|
|
3566
|
+
onClick: onSignIn,
|
|
3567
|
+
children: signInLabel
|
|
3568
|
+
}
|
|
3569
|
+
) })
|
|
3570
|
+
]
|
|
3571
|
+
}
|
|
3572
|
+
)
|
|
3573
|
+
}
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
function notifySessionExpired() {
|
|
3577
|
+
if (typeof window === "undefined") return;
|
|
3578
|
+
window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT));
|
|
3579
|
+
}
|
|
3580
|
+
function refreshIdentity() {
|
|
3581
|
+
if (typeof window === "undefined") return;
|
|
3582
|
+
window.dispatchEvent(new Event(REFRESH_EVENT2));
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
export { AdminBadge, AdminGate, AppInfoSection, AuthGate, Avatar, BackButton, BackofficeLayout, CODE_TO_KEY, COMMAND_PALETTE_OPEN_EVENT, CommandPalette, CommandPaletteTrigger, CopyButton, DESKTOP_MIN, DataTable, DeployInfo, DialogHost, DocsHub, EmptyState, ErrorPage, HttpError, I18nProvider, InstallBanner, LEGAL_HUB_BASE_URL, LEGAL_MANIFEST_URL, LegalMenuItem, LegalPage, LegalRow, LoginButton, LogoutButton, MobileTabBar, NavigationBar, NotificationBell, OpenInBrowserButton, PolicyChangeBanner, RelTime, SHARE_CRAWLER_UA_SUBSTRINGS, SUPPORTED_LOCALES, SessionBadge, SessionExpiredDialog, Sidebar, SidebarToggle, StatusBanner, TABLET_MIN, Toaster, UserMenu, ViewportProvider, buildSkillMarkdownText, createFetch, crossLocaleKeywords, dismissToast, fleetLoginUrl, fleetLogoutUrl, fleetSignIn, fleetSignOut, formatAbsTime, formatRelTime, getLocale, getTheme, getViewport, installIOSPwaShell, interpolate, isAdminLike, isInputTarget, isShareCrawler, networkFirstSwSource, noFlashLocaleScript, noFlashThemeScript, noFlashViewportScript, notifySessionExpired, openCommandPalette, refreshIdentity, registerServiceWorker, runInAppBackFallback, setLocale, setTheme, shortcutKey, signIn, signInUrl, signOut, signOutUrl, toast, uiConfirm, uiPrompt, useClipboard, useGoToShortcuts, useI18n, useIdentity, useInAppBack, useInstallPrompt, useLegalAvailability, useLocale, useMe, useRouteState, useSessionState, useSidebarDrawer, useStatusBanner, useT, useViewport };
|