@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/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 };