@endfield-ui/react 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/NOTICE.md +32 -2
  2. package/README.md +37 -2
  3. package/dist/game/LICENSE.lucide +43 -0
  4. package/dist/game/assets/alert_banner_bg_left.png +0 -0
  5. package/dist/game/assets/alert_banner_bg_right.png +0 -0
  6. package/dist/game/assets/alert_bg.png +0 -0
  7. package/dist/game/assets/deco_common_logo_bg.png +0 -0
  8. package/dist/game/assets/detail.png +0 -0
  9. package/dist/game/assets/enemies_background.webp +0 -0
  10. package/dist/game/assets/enemies_background_dark.webp +0 -0
  11. package/dist/game/assets/header.webp +0 -0
  12. package/dist/game/assets/icon_card_rarity_1_bottom_edge.webp +0 -0
  13. package/dist/game/assets/icon_card_rarity_2_bottom_edge.webp +0 -0
  14. package/dist/game/assets/icon_card_rarity_3_bottom_edge.webp +0 -0
  15. package/dist/game/assets/icon_card_rarity_4_bottom_edge.webp +0 -0
  16. package/dist/game/assets/icon_card_rarity_5_bottom_edge.webp +0 -0
  17. package/dist/game/assets/icon_card_rarity_6_bottom_edge.webp +0 -0
  18. package/dist/game/assets/items_background.webp +0 -0
  19. package/dist/game/assets/items_background_dark.webp +0 -0
  20. package/dist/game/assets/link.webp +0 -0
  21. package/dist/game/assets/pag-button-texture.png +0 -0
  22. package/dist/game/assets/weapons_background.webp +0 -0
  23. package/dist/game/assets/weapons_background_dark.webp +0 -0
  24. package/dist/game/resource-provenance.json +384 -0
  25. package/dist/game/styles.css +2 -0
  26. package/dist/game.d.ts +372 -0
  27. package/dist/game.js +2042 -0
  28. package/package.json +9 -2
  29. package/src/components/README.md +2 -0
  30. package/src/styles/README.md +5 -1
  31. package/src/styles/index.css +5 -0
package/dist/game.js ADDED
@@ -0,0 +1,2042 @@
1
+ "use client";
2
+ import { Dialog } from "@base-ui/react/dialog";
3
+ import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
+ import { cn, useControllableState, useFieldContext, useFieldControl, usePortalScope, useReducedMotion } from "./index.js";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { ScrollArea } from "@base-ui/react/scroll-area";
7
+ import { Popover } from "@base-ui/react/popover";
8
+ import { flushSync } from "react-dom";
9
+ import { Select } from "@base-ui/react/select";
10
+ import { Radio } from "@base-ui/react/radio";
11
+ import { RadioGroup } from "@base-ui/react/radio-group";
12
+ //#region src/game/lib/first-focusable.ts
13
+ const FOCUSABLE = [
14
+ "a[href]",
15
+ "button:not([disabled])",
16
+ "input:not([disabled]):not([type=\"hidden\"])",
17
+ "select:not([disabled])",
18
+ "textarea:not([disabled])",
19
+ "[tabindex]:not([tabindex=\"-1\"])"
20
+ ].join(",");
21
+ /**
22
+ * 弹窗打开时焦点落在哪:内容区里第一个能聚焦的元素;没有时返回 `true`,交还给默认行为。
23
+ *
24
+ * 和主库 `components/dialog/initial-focus.ts` 是同一条规则。那边不是公开导出,
25
+ * 这一类又只经主入口用主库(见 `core.ts`),所以这里自己留一份。
26
+ */
27
+ function firstFocusableIn(content) {
28
+ return () => content.current?.querySelector(FOCUSABLE) ?? true;
29
+ }
30
+ //#endregion
31
+ //#region src/game/animated-dialog/AnimatedDialog.tsx
32
+ function slotClass(value, open) {
33
+ return typeof value === "function" ? value(open) : value;
34
+ }
35
+ function AnimatedDialog({ trigger, open: controlledOpen, defaultOpen = false, onOpenChange, title, description, icon, width, height, className, classNames, dismissible = true, keyboardCloseable, onClose, onDismiss, closeLabel = "关闭", initialFocus, finalFocus, keepMounted = false, modal, container, children, ...popupProps }) {
36
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
37
+ const open = controlledOpen ?? internalOpen;
38
+ const [pending, setPending] = useState(false);
39
+ const lifecycle = useRef({
40
+ open,
41
+ session: 0,
42
+ locked: false,
43
+ mounted: true
44
+ });
45
+ const contentRef = useRef(null);
46
+ const { anchorRef, portalRef } = usePortalScope();
47
+ const focusContent = useCallback(() => firstFocusableIn(contentRef)(), []);
48
+ useLayoutEffect(() => {
49
+ const current = lifecycle.current;
50
+ current.mounted = true;
51
+ return () => {
52
+ current.mounted = false;
53
+ current.session += 1;
54
+ current.locked = false;
55
+ };
56
+ }, []);
57
+ useLayoutEffect(() => {
58
+ if (lifecycle.current.open === open) return;
59
+ lifecycle.current.open = open;
60
+ lifecycle.current.session += 1;
61
+ lifecycle.current.locked = false;
62
+ setPending(false);
63
+ }, [open]);
64
+ const updateOpen = useCallback((next) => {
65
+ if (controlledOpen === void 0) setInternalOpen(next);
66
+ onOpenChange?.(next);
67
+ }, [controlledOpen, onOpenChange]);
68
+ const requestClose = useCallback(async (dismiss) => {
69
+ const current = lifecycle.current;
70
+ if (!current.open || current.locked || !current.mounted) return;
71
+ current.locked = true;
72
+ const session = current.session;
73
+ setPending(true);
74
+ const active = () => lifecycle.current.mounted && lifecycle.current.open && lifecycle.current.session === session;
75
+ try {
76
+ if (dismiss) {
77
+ if (await onDismiss?.() === false || !active()) return;
78
+ }
79
+ if (await onClose?.() !== false && active()) updateOpen(false);
80
+ } catch {} finally {
81
+ if (active()) {
82
+ lifecycle.current.locked = false;
83
+ setPending(false);
84
+ }
85
+ }
86
+ }, [
87
+ onClose,
88
+ onDismiss,
89
+ updateOpen
90
+ ]);
91
+ const handleOpenChange = (next, details) => {
92
+ if (next) {
93
+ updateOpen(true);
94
+ return;
95
+ }
96
+ details.cancel();
97
+ if (details.reason === "escape-key" && !(keyboardCloseable ?? dismissible)) return;
98
+ const dismiss = details.reason === "escape-key" || details.reason === "outside-press" || details.reason === "focus-out";
99
+ if (dismiss && details.reason !== "escape-key" && !dismissible) return;
100
+ requestClose(dismiss);
101
+ };
102
+ return /* @__PURE__ */ jsxs(Dialog.Root, {
103
+ open,
104
+ onOpenChange: handleOpenChange,
105
+ disablePointerDismissal: !dismissible,
106
+ modal,
107
+ children: [trigger && /* @__PURE__ */ jsx(Dialog.Trigger, {
108
+ ref: anchorRef,
109
+ render: trigger
110
+ }), /* @__PURE__ */ jsxs(Dialog.Portal, {
111
+ ref: portalRef,
112
+ keepMounted,
113
+ container,
114
+ children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: cn("ef-animated-dialog-backdrop", slotClass(classNames?.backdrop, open)) }), /* @__PURE__ */ jsx(Dialog.Viewport, {
115
+ className: "ef-animated-dialog-viewport",
116
+ children: /* @__PURE__ */ jsxs(Dialog.Popup, {
117
+ ...popupProps,
118
+ "aria-busy": pending || void 0,
119
+ initialFocus: initialFocus ?? focusContent,
120
+ finalFocus,
121
+ style: {
122
+ width,
123
+ height
124
+ },
125
+ className: cn("ef-animated-dialog", className, slotClass(classNames?.alert, open)),
126
+ children: [
127
+ /* @__PURE__ */ jsxs("header", {
128
+ className: cn("ef-animated-dialog__header", slotClass(classNames?.header, open)),
129
+ children: [
130
+ /* @__PURE__ */ jsxs("div", {
131
+ "aria-hidden": "true",
132
+ className: "ef-animated-dialog__top-strip",
133
+ children: [/* @__PURE__ */ jsx("span", { className: "ef-animated-dialog__yellow-block" }), /* @__PURE__ */ jsxs("span", {
134
+ className: "ef-animated-dialog__top-deco",
135
+ children: [/* @__PURE__ */ jsxs("svg", {
136
+ viewBox: "0 0 120 10",
137
+ children: [
138
+ /* @__PURE__ */ jsx("polygon", {
139
+ points: "0,2 7,2 3.5,8",
140
+ fill: "currentColor"
141
+ }),
142
+ /* @__PURE__ */ jsx("text", {
143
+ x: "7",
144
+ y: "7",
145
+ fontSize: "7",
146
+ fontStyle: "italic",
147
+ children: "//"
148
+ }),
149
+ /* @__PURE__ */ jsx("text", {
150
+ x: "16",
151
+ y: "7",
152
+ fontSize: "7",
153
+ children: "ARKNIGHTS:ENDFIELD"
154
+ })
155
+ ]
156
+ }), /* @__PURE__ */ jsxs("svg", {
157
+ viewBox: "0 0 10 10",
158
+ className: "ef-animated-dialog__plus",
159
+ children: [/* @__PURE__ */ jsx("rect", {
160
+ x: "0.5",
161
+ y: "0.5",
162
+ width: "9",
163
+ height: "9",
164
+ fill: "none",
165
+ stroke: "currentColor"
166
+ }), /* @__PURE__ */ jsx("path", {
167
+ d: "M5 2.5v5M2.5 5h5",
168
+ stroke: "currentColor"
169
+ })]
170
+ })]
171
+ })]
172
+ }),
173
+ /* @__PURE__ */ jsxs("div", {
174
+ className: "ef-animated-dialog__title-band",
175
+ children: [
176
+ /* @__PURE__ */ jsx("div", {
177
+ "aria-hidden": "true",
178
+ className: "ef-animated-dialog__icon-slot",
179
+ children: /* @__PURE__ */ jsx("span", {
180
+ className: "ef-animated-dialog__icon",
181
+ children: icon
182
+ })
183
+ }),
184
+ /* @__PURE__ */ jsx(Dialog.Title, {
185
+ className: cn("ef-animated-dialog__title", slotClass(classNames?.title, open)),
186
+ children: /* @__PURE__ */ jsx("span", { children: title })
187
+ }),
188
+ /* @__PURE__ */ jsx("span", {
189
+ "aria-hidden": "true",
190
+ className: "ef-animated-dialog__title-deco",
191
+ children: "ENDFIELD"
192
+ })
193
+ ]
194
+ }),
195
+ /* @__PURE__ */ jsx("div", {
196
+ className: "ef-animated-dialog__close-motion",
197
+ children: /* @__PURE__ */ jsx(Dialog.Close, {
198
+ "aria-label": closeLabel,
199
+ "aria-disabled": pending || void 0,
200
+ className: cn("ef-animated-dialog__close", slotClass(classNames?.close, open)),
201
+ children: /* @__PURE__ */ jsx("svg", {
202
+ "aria-hidden": "true",
203
+ viewBox: "0 0 205 205",
204
+ fill: "currentColor",
205
+ fillRule: "evenodd",
206
+ children: /* @__PURE__ */ jsx("path", { d: "M0 25L25 0L205 180L180 205ZM25 205L0 180L180 0L205 25Z" })
207
+ })
208
+ })
209
+ }),
210
+ /* @__PURE__ */ jsx("div", {
211
+ "aria-hidden": "true",
212
+ className: "ef-animated-dialog__separator"
213
+ })
214
+ ]
215
+ }),
216
+ /* @__PURE__ */ jsxs("div", {
217
+ "data-theme": "light",
218
+ className: cn("ef-animated-dialog__content", slotClass(classNames?.contentContainer, open)),
219
+ children: [/* @__PURE__ */ jsxs("div", {
220
+ ref: contentRef,
221
+ className: "ef-animated-dialog__body",
222
+ children: [description && /* @__PURE__ */ jsx(Dialog.Description, {
223
+ className: "ef-animated-dialog__description",
224
+ children: description
225
+ }), children]
226
+ }), /* @__PURE__ */ jsx("span", {
227
+ "aria-hidden": "true",
228
+ className: "ef-animated-dialog__registration"
229
+ })]
230
+ }),
231
+ /* @__PURE__ */ jsxs("footer", {
232
+ "aria-hidden": "true",
233
+ className: cn("ef-animated-dialog__footer", slotClass(classNames?.footer, open)),
234
+ children: [
235
+ /* @__PURE__ */ jsx("span", { className: "ef-animated-dialog__footer-square" }),
236
+ /* @__PURE__ */ jsx("span", { className: "ef-animated-dialog__footer-outline" }),
237
+ /* @__PURE__ */ jsx("span", { className: "ef-animated-dialog__footer-hatch" })
238
+ ]
239
+ })
240
+ ]
241
+ })
242
+ })]
243
+ })]
244
+ });
245
+ }
246
+ //#endregion
247
+ //#region src/game/rounded-button/RoundedButton.tsx
248
+ const hoverColors = {
249
+ primary: {
250
+ enter: [
251
+ "#2a272877",
252
+ "#2a2728b5",
253
+ "#2a272877",
254
+ "#2a2728b5"
255
+ ],
256
+ leave: [
257
+ "#2a2728b5",
258
+ "#2a2728",
259
+ "#2a2728b5",
260
+ "#2a2728"
261
+ ],
262
+ enterBorder: [
263
+ "#81807f",
264
+ "#81807f",
265
+ "#81807f",
266
+ "#81807f"
267
+ ],
268
+ leaveBorder: [
269
+ "#81807f",
270
+ "#81807f",
271
+ "#81807f",
272
+ "#81807f"
273
+ ]
274
+ },
275
+ default: {
276
+ enter: [
277
+ "#ececec77",
278
+ "#e8e8e8b5",
279
+ "#ececec77",
280
+ "#e8e8e8b5"
281
+ ],
282
+ leave: [
283
+ "#e8e8e8b5",
284
+ "#f2f1ef",
285
+ "#e8e8e8b5",
286
+ "#f2f1ef"
287
+ ],
288
+ enterBorder: [
289
+ "#a5a5a5",
290
+ "#a5a5a5",
291
+ "#a5a5a5",
292
+ "#a5a5a5"
293
+ ],
294
+ leaveBorder: [
295
+ "#a5a5a5",
296
+ "#a0a2a3",
297
+ "#a5a5a5",
298
+ "#a0a2a3"
299
+ ]
300
+ }
301
+ };
302
+ /** 原灰色圆角按钮;悬停闪动由浏览器动画执行。 */
303
+ function RoundedButton({ variant = "primary", type = "button", text = "", children, icon, showIcon = true, iconPosition = "right", loading = false, disabled = false, className, classNames, onClick, onPointerEnter, onPointerLeave, ...props }) {
304
+ const reducedMotion = useReducedMotion();
305
+ const animationRef = useRef(null);
306
+ const isPrimary = variant === "primary";
307
+ const slotClass = (slot) => typeof slot === "function" ? slot(isPrimary) : slot;
308
+ useEffect(() => {
309
+ if (disabled || loading || reducedMotion) animationRef.current?.cancel();
310
+ return () => animationRef.current?.cancel();
311
+ }, [
312
+ disabled,
313
+ loading,
314
+ reducedMotion,
315
+ variant
316
+ ]);
317
+ const animateHover = (event, entering) => {
318
+ const button = event.currentTarget;
319
+ if (disabled || loading || reducedMotion || event.defaultPrevented || event.pointerType !== "mouse" || typeof window.matchMedia !== "function" || !window.matchMedia("(hover: hover) and (pointer: fine)").matches || typeof button.animate !== "function") return;
320
+ animationRef.current?.cancel();
321
+ const colors = hoverColors[variant];
322
+ const backgrounds = entering ? colors.enter : colors.leave;
323
+ const borders = entering ? colors.enterBorder : colors.leaveBorder;
324
+ animationRef.current = button.animate(backgrounds.map((backgroundColor, index) => ({
325
+ backgroundColor,
326
+ boxShadow: `0 0 0 0.1em ${backgroundColor}, 0 0 0 0.1em ${borders[index]} inset`
327
+ })), {
328
+ duration: 200,
329
+ easing: "ease-out"
330
+ });
331
+ };
332
+ const iconElement = showIcon && /* @__PURE__ */ jsx("span", {
333
+ "aria-hidden": "true",
334
+ className: cn("ef-rounded-button-icon", slotClass(classNames?.icon)),
335
+ children: loading ? /* @__PURE__ */ jsx("span", { className: "ef-rounded-button-spinner" }) : icon
336
+ });
337
+ return /* @__PURE__ */ jsxs("button", {
338
+ ...props,
339
+ type,
340
+ disabled,
341
+ "aria-busy": loading || props["aria-busy"] || void 0,
342
+ "aria-disabled": loading || props["aria-disabled"] || void 0,
343
+ "data-variant": variant,
344
+ "data-icon-position": iconPosition,
345
+ "data-loading": loading ? "" : void 0,
346
+ className: cn("ef-rounded-button", className, slotClass(classNames?.button)),
347
+ onClick: (event) => {
348
+ if (disabled || loading) {
349
+ event.preventDefault();
350
+ return;
351
+ }
352
+ onClick?.(event);
353
+ },
354
+ onPointerEnter: (event) => {
355
+ onPointerEnter?.(event);
356
+ animateHover(event, true);
357
+ },
358
+ onPointerLeave: (event) => {
359
+ onPointerLeave?.(event);
360
+ animateHover(event, false);
361
+ },
362
+ children: [
363
+ iconPosition === "left" && iconElement,
364
+ /* @__PURE__ */ jsx("span", {
365
+ className: cn("ef-rounded-button-text", slotClass(classNames?.text)),
366
+ children: children ?? text
367
+ }),
368
+ iconPosition === "right" && iconElement
369
+ ]
370
+ });
371
+ }
372
+ //#endregion
373
+ //#region src/game/banner-alert/AlertIcons.tsx
374
+ function AlertTickIcon() {
375
+ return /* @__PURE__ */ jsx("svg", {
376
+ viewBox: "0 0 24 24",
377
+ fill: "none",
378
+ width: "1em",
379
+ height: "1em",
380
+ "aria-hidden": "true",
381
+ children: /* @__PURE__ */ jsx("path", {
382
+ d: "M4.89163 13.2687L9.16582 17.5427L18.7085 8",
383
+ stroke: "currentColor",
384
+ strokeWidth: "3",
385
+ strokeLinecap: "round",
386
+ strokeLinejoin: "round"
387
+ })
388
+ });
389
+ }
390
+ function AlertCloseIcon() {
391
+ return /* @__PURE__ */ jsx("svg", {
392
+ viewBox: "0 0 1024 1024",
393
+ width: "1em",
394
+ height: "1em",
395
+ "aria-hidden": "true",
396
+ children: /* @__PURE__ */ jsx("path", {
397
+ fill: "currentColor",
398
+ d: "M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z"
399
+ })
400
+ });
401
+ }
402
+ //#endregion
403
+ //#region src/game/banner-alert/types.ts
404
+ function alertSlotClass(value, open) {
405
+ return typeof value === "function" ? value(open) : value;
406
+ }
407
+ //#endregion
408
+ //#region src/game/banner-alert/useAlertActions.ts
409
+ function useAlertActions({ open: controlledOpen, defaultOpen = false, onOpenChange, loading = false, onConfirm, onCancel, onDismiss, onClose }) {
410
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
411
+ const [pending, setPending] = useState(false);
412
+ const open = controlledOpen ?? internalOpen;
413
+ const lifecycle = useRef({
414
+ open,
415
+ loading,
416
+ mounted: true,
417
+ locked: false,
418
+ session: 0
419
+ });
420
+ useLayoutEffect(() => {
421
+ const current = lifecycle.current;
422
+ current.mounted = true;
423
+ return () => {
424
+ current.mounted = false;
425
+ current.locked = false;
426
+ current.session += 1;
427
+ };
428
+ }, []);
429
+ useLayoutEffect(() => {
430
+ lifecycle.current.loading = loading;
431
+ if (lifecycle.current.open === open) return;
432
+ lifecycle.current.open = open;
433
+ lifecycle.current.session += 1;
434
+ lifecycle.current.locked = false;
435
+ setPending(false);
436
+ }, [open, loading]);
437
+ const updateOpen = useCallback((next) => {
438
+ if (controlledOpen === void 0) setInternalOpen(next);
439
+ onOpenChange?.(next);
440
+ }, [controlledOpen, onOpenChange]);
441
+ const run = useCallback(async (kind) => {
442
+ const current = lifecycle.current;
443
+ if (!current.open || !current.mounted || current.loading || current.locked) return;
444
+ current.locked = true;
445
+ setPending(true);
446
+ const session = current.session;
447
+ const active = () => lifecycle.current.mounted && lifecycle.current.open && lifecycle.current.session === session;
448
+ try {
449
+ const action = {
450
+ confirm: onConfirm,
451
+ cancel: onCancel,
452
+ dismiss: onDismiss,
453
+ close: void 0
454
+ }[kind];
455
+ if (await action?.() === false || !active()) return;
456
+ if (await onClose?.() !== false && active()) updateOpen(false);
457
+ } catch {} finally {
458
+ if (active()) {
459
+ lifecycle.current.locked = false;
460
+ setPending(false);
461
+ }
462
+ }
463
+ }, [
464
+ onConfirm,
465
+ onCancel,
466
+ onDismiss,
467
+ onClose,
468
+ updateOpen
469
+ ]);
470
+ return {
471
+ open,
472
+ busy: loading || pending,
473
+ updateOpen,
474
+ run
475
+ };
476
+ }
477
+ //#endregion
478
+ //#region src/game/banner-alert/AlertModal.tsx
479
+ function hasTabbableContent(popup) {
480
+ const candidates = popup.querySelectorAll("a[href],button,input:not([type=\"hidden\"]),select,textarea,[tabindex],[contenteditable=\"true\"]");
481
+ return Array.from(candidates).some((node) => {
482
+ if (node.matches(":disabled") || node.hasAttribute("tabindex") && node.tabIndex < 0 || node.closest("[hidden],[inert],[aria-hidden=\"true\"]")) return false;
483
+ const style = getComputedStyle(node);
484
+ return style.display !== "none" && style.visibility !== "hidden";
485
+ });
486
+ }
487
+ function AlertModal({ trigger, open: controlledOpen, defaultOpen, onOpenChange, title, description, dismissible = true, keyboardCloseable, loading, hideCancel = false, cancelText: _cancelText, confirmText: _confirmText, showIcon: _showIcon, width, height, className, classNames, onConfirm, onCancel, onDismiss, onClose, initialFocus, finalFocus, keepMounted = false, modal, container, popupClassName, backdropClassName, viewportClassName, children, ref: forwardedRef, ...popupProps }) {
488
+ const { open, busy, updateOpen, run } = useAlertActions({
489
+ open: controlledOpen,
490
+ defaultOpen,
491
+ onOpenChange,
492
+ loading,
493
+ onConfirm,
494
+ onCancel,
495
+ onDismiss,
496
+ onClose
497
+ });
498
+ const { anchorRef, portalRef } = usePortalScope();
499
+ const popupRef = useRef(null);
500
+ const cancelRef = useRef(null);
501
+ const confirmRef = useRef(null);
502
+ const lastFocus = useRef(null);
503
+ const wasBusy = useRef(false);
504
+ const focusAction = useCallback(() => {
505
+ const button = hideCancel ? confirmRef.current : cancelRef.current;
506
+ return button && !button.disabled ? button : popupRef.current ?? true;
507
+ }, [hideCancel]);
508
+ const assignRef = useCallback((node) => {
509
+ popupRef.current = node;
510
+ if (typeof forwardedRef === "function") return forwardedRef(node);
511
+ if (forwardedRef) forwardedRef.current = node;
512
+ }, [forwardedRef]);
513
+ useLayoutEffect(() => {
514
+ const popup = popupRef.current;
515
+ const previousBusy = wasBusy.current;
516
+ wasBusy.current = busy;
517
+ if (!open || !popup) return;
518
+ const focused = popup.ownerDocument.activeElement;
519
+ if (busy) {
520
+ if (focused === popup.ownerDocument.body || focused instanceof HTMLElement && popup.contains(focused) && focused.matches(":disabled")) popup.focus({ preventScroll: true });
521
+ } else if (previousBusy && focused === popup) {
522
+ const previousFocus = lastFocus.current;
523
+ if (previousFocus && popup.contains(previousFocus) && !previousFocus.matches(":disabled")) previousFocus.focus({ preventScroll: true });
524
+ }
525
+ }, [busy, open]);
526
+ const changeOpen = (next, details) => {
527
+ if (next) {
528
+ updateOpen(true);
529
+ return;
530
+ }
531
+ details.cancel();
532
+ if (details.reason === "escape-key" && !(keyboardCloseable ?? dismissible)) return;
533
+ const dismiss = details.reason === "escape-key" || details.reason === "outside-press" || details.reason === "focus-out";
534
+ if (dismiss && details.reason !== "escape-key" && !dismissible) return;
535
+ run(dismiss ? "dismiss" : "close");
536
+ };
537
+ return /* @__PURE__ */ jsxs(Dialog.Root, {
538
+ open,
539
+ onOpenChange: changeOpen,
540
+ disablePointerDismissal: !dismissible || busy,
541
+ modal,
542
+ children: [trigger && /* @__PURE__ */ jsx(Dialog.Trigger, {
543
+ ref: anchorRef,
544
+ render: trigger
545
+ }), /* @__PURE__ */ jsxs(Dialog.Portal, {
546
+ ref: portalRef,
547
+ keepMounted,
548
+ container,
549
+ children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: cn(backdropClassName, alertSlotClass(classNames?.backdrop, open)) }), /* @__PURE__ */ jsx(Dialog.Viewport, {
550
+ className: cn("ef-alert-modal-viewport", viewportClassName),
551
+ children: /* @__PURE__ */ jsxs(Dialog.Popup, {
552
+ ...popupProps,
553
+ onFocusCapture: (event) => {
554
+ popupProps.onFocusCapture?.(event);
555
+ if (event.target instanceof HTMLElement && event.target !== event.currentTarget && event.currentTarget.contains(event.target)) lastFocus.current = event.target;
556
+ },
557
+ onKeyDownCapture: (event) => {
558
+ popupProps.onKeyDownCapture?.(event);
559
+ if (open && busy && event.key === "Tab" && event.target instanceof Node && event.currentTarget.contains(event.target) && !hasTabbableContent(event.currentTarget)) {
560
+ event.preventDefault();
561
+ event.currentTarget.focus({ preventScroll: true });
562
+ }
563
+ },
564
+ ref: assignRef,
565
+ role: "alertdialog",
566
+ "aria-busy": busy || void 0,
567
+ "data-theme": "light",
568
+ initialFocus: initialFocus ?? focusAction,
569
+ finalFocus,
570
+ style: {
571
+ width,
572
+ height
573
+ },
574
+ className: cn(popupClassName, className, alertSlotClass(classNames?.alert, open)),
575
+ children: [
576
+ /* @__PURE__ */ jsx(Dialog.Title, {
577
+ className: "ef-alert-modal-sr-only",
578
+ children: title
579
+ }),
580
+ description && /* @__PURE__ */ jsx(Dialog.Description, {
581
+ className: "ef-alert-modal-sr-only",
582
+ children: description
583
+ }),
584
+ children({
585
+ open,
586
+ busy,
587
+ cancelRef,
588
+ confirmRef,
589
+ confirm: () => {
590
+ run("confirm");
591
+ },
592
+ cancel: () => {
593
+ run("cancel");
594
+ }
595
+ })
596
+ ]
597
+ })
598
+ })]
599
+ })]
600
+ });
601
+ }
602
+ //#endregion
603
+ //#region src/game/banner-alert/useAlertGlitch.ts
604
+ const OPACITY = [
605
+ 0,
606
+ 1,
607
+ 1,
608
+ 0,
609
+ 0,
610
+ 1,
611
+ 1,
612
+ 0,
613
+ 0,
614
+ 1
615
+ ];
616
+ const OFFSETS = [
617
+ 0,
618
+ .48,
619
+ .82,
620
+ .85,
621
+ .87,
622
+ .9,
623
+ .92,
624
+ .95,
625
+ .97,
626
+ 1
627
+ ];
628
+ const frames = OPACITY.map((opacity, index) => ({
629
+ opacity,
630
+ offset: OFFSETS[index]
631
+ }));
632
+ function useAlertGlitch(open) {
633
+ const contentRef = useRef(null);
634
+ const actionsRef = useRef(null);
635
+ useLayoutEffect(() => {
636
+ if (!open) return;
637
+ const nodes = [contentRef.current, actionsRef.current].filter((node) => node !== null);
638
+ const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
639
+ const animations = nodes.flatMap((node) => {
640
+ node.style.removeProperty("--ef-game-alert-exit-opacity");
641
+ node.style.opacity = "1";
642
+ if (media?.matches || typeof node.animate !== "function") return [];
643
+ node.style.opacity = "0";
644
+ const animation = node.animate(frames, {
645
+ duration: 205,
646
+ easing: "ease-out",
647
+ fill: "both"
648
+ });
649
+ animation.onfinish = () => {
650
+ node.style.opacity = "1";
651
+ animation.cancel();
652
+ };
653
+ return [{
654
+ node,
655
+ animation
656
+ }];
657
+ });
658
+ const settle = () => {
659
+ for (const { node, animation } of animations) {
660
+ node.style.opacity = "1";
661
+ animation.cancel();
662
+ }
663
+ };
664
+ const reduced = () => {
665
+ if (media?.matches) settle();
666
+ };
667
+ media?.addEventListener("change", reduced);
668
+ return () => {
669
+ media?.removeEventListener("change", reduced);
670
+ for (const { node, animation } of animations) {
671
+ node.style.setProperty("--ef-game-alert-exit-opacity", getComputedStyle(node).opacity);
672
+ animation.cancel();
673
+ node.style.opacity = "1";
674
+ }
675
+ };
676
+ }, [open]);
677
+ return {
678
+ contentRef,
679
+ actionsRef
680
+ };
681
+ }
682
+ //#endregion
683
+ //#region src/game/banner-alert/BannerAlert.tsx
684
+ function BannerAlert({ children, width = "30em", hideCancel = false, cancelText = "取消", confirmText = "确认", showIcon = true, classNames, ...props }) {
685
+ return /* @__PURE__ */ jsx(AlertModal, {
686
+ ...props,
687
+ width,
688
+ hideCancel,
689
+ classNames,
690
+ popupClassName: "ef-banner-alert",
691
+ backdropClassName: "ef-banner-alert-backdrop",
692
+ children: (state) => /* @__PURE__ */ jsx(BannerAlertContent, {
693
+ ...state,
694
+ hideCancel,
695
+ cancelText,
696
+ confirmText,
697
+ showIcon,
698
+ classNames,
699
+ children
700
+ })
701
+ });
702
+ }
703
+ function BannerAlertContent({ open, busy, cancelRef, confirmRef, cancel, confirm, hideCancel, cancelText, confirmText, showIcon, classNames, children }) {
704
+ const { contentRef, actionsRef } = useAlertGlitch(open);
705
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
706
+ /* @__PURE__ */ jsxs("div", {
707
+ "aria-hidden": "true",
708
+ className: "ef-banner-alert__banner",
709
+ children: [/* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsx("span", {})]
710
+ }),
711
+ /* @__PURE__ */ jsx("div", {
712
+ ref: contentRef,
713
+ className: cn("ef-banner-alert__content", alertSlotClass(classNames?.content, open)),
714
+ children: /* @__PURE__ */ jsx("div", {
715
+ className: "ef-banner-alert__body",
716
+ children
717
+ })
718
+ }),
719
+ /* @__PURE__ */ jsxs("div", {
720
+ ref: actionsRef,
721
+ className: "ef-banner-alert__actions",
722
+ "data-hide-cancel": hideCancel ? "" : void 0,
723
+ children: [!hideCancel && /* @__PURE__ */ jsx(RoundedButton, {
724
+ ref: cancelRef,
725
+ variant: "default",
726
+ text: cancelText,
727
+ showIcon,
728
+ disabled: busy,
729
+ onClick: cancel,
730
+ icon: /* @__PURE__ */ jsx(AlertCloseIcon, {}),
731
+ className: "ef-banner-alert__button"
732
+ }), /* @__PURE__ */ jsx(RoundedButton, {
733
+ ref: confirmRef,
734
+ variant: "primary",
735
+ text: confirmText,
736
+ showIcon,
737
+ disabled: busy,
738
+ loading: busy,
739
+ onClick: confirm,
740
+ icon: /* @__PURE__ */ jsx(AlertTickIcon, {}),
741
+ className: "ef-banner-alert__button"
742
+ })]
743
+ })
744
+ ] });
745
+ }
746
+ //#endregion
747
+ //#region src/game/full-width-alert/FullWidthAlert.tsx
748
+ /** 横贯视口的确认面板,底部按钮突出于面板下沿。 */
749
+ function FullWidthAlert({ hideCancel = false, cancelText = "取消", confirmText = "确认", showIcon = true, height = "auto", classNames, children, ...props }) {
750
+ return /* @__PURE__ */ jsx(AlertModal, {
751
+ ...props,
752
+ hideCancel,
753
+ height,
754
+ classNames,
755
+ popupClassName: "ef-full-width-alert",
756
+ backdropClassName: "ef-full-width-alert-backdrop",
757
+ viewportClassName: "ef-full-width-alert-viewport",
758
+ children: ({ open, busy, cancelRef, confirmRef, cancel, confirm }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
759
+ /* @__PURE__ */ jsxs("div", {
760
+ "aria-hidden": "true",
761
+ className: "ef-full-width-alert-decoration",
762
+ children: [
763
+ /* @__PURE__ */ jsx("span", { className: "ef-full-width-alert-logo" }),
764
+ /* @__PURE__ */ jsx("span", { className: "ef-full-width-alert-color-bar" }),
765
+ /* @__PURE__ */ jsxs("div", {
766
+ className: "ef-full-width-alert-banner",
767
+ children: [/* @__PURE__ */ jsx("span", { className: "ef-full-width-alert-banner-left" }), /* @__PURE__ */ jsx("span", { className: "ef-full-width-alert-banner-right" })]
768
+ })
769
+ ]
770
+ }),
771
+ /* @__PURE__ */ jsx("div", {
772
+ className: cn("ef-full-width-alert-content", alertSlotClass(classNames?.content, open)),
773
+ children: /* @__PURE__ */ jsxs(ScrollArea.Root, {
774
+ className: "ef-full-width-alert-scroll-area",
775
+ children: [/* @__PURE__ */ jsx(ScrollArea.Viewport, {
776
+ className: "ef-full-width-alert-viewport-content",
777
+ style: { overflowX: "hidden" },
778
+ render: (viewportProps, state) => /* @__PURE__ */ jsx("div", {
779
+ ...viewportProps,
780
+ role: state.hasOverflowY ? "region" : "presentation",
781
+ "aria-label": state.hasOverflowY ? "内容" : void 0,
782
+ tabIndex: state.hasOverflowY ? 0 : -1
783
+ }),
784
+ children: /* @__PURE__ */ jsx(ScrollArea.Content, {
785
+ className: "ef-full-width-alert-body",
786
+ children
787
+ })
788
+ }), /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
789
+ orientation: "vertical",
790
+ className: "ef-full-width-alert-scrollbar",
791
+ children: /* @__PURE__ */ jsx(ScrollArea.Thumb, { className: "ef-full-width-alert-thumb" })
792
+ })]
793
+ })
794
+ }),
795
+ /* @__PURE__ */ jsxs("div", {
796
+ className: "ef-full-width-alert-actions",
797
+ children: [!hideCancel && /* @__PURE__ */ jsxs("div", {
798
+ className: "ef-full-width-alert-action",
799
+ children: [/* @__PURE__ */ jsx("span", {
800
+ "aria-hidden": "true",
801
+ className: "ef-full-width-alert-action-backing"
802
+ }), /* @__PURE__ */ jsx(RoundedButton, {
803
+ ref: cancelRef,
804
+ variant: "default",
805
+ text: cancelText,
806
+ showIcon,
807
+ disabled: busy,
808
+ onClick: cancel,
809
+ icon: /* @__PURE__ */ jsx(AlertCloseIcon, {}),
810
+ className: "ef-full-width-alert-button"
811
+ })]
812
+ }), /* @__PURE__ */ jsxs("div", {
813
+ className: "ef-full-width-alert-action",
814
+ children: [/* @__PURE__ */ jsx("span", {
815
+ "aria-hidden": "true",
816
+ className: "ef-full-width-alert-action-backing"
817
+ }), /* @__PURE__ */ jsx(RoundedButton, {
818
+ ref: confirmRef,
819
+ variant: "primary",
820
+ text: confirmText,
821
+ showIcon,
822
+ loading: busy,
823
+ disabled: busy,
824
+ onClick: confirm,
825
+ icon: /* @__PURE__ */ jsx(AlertTickIcon, {}),
826
+ className: "ef-full-width-alert-button"
827
+ })]
828
+ })]
829
+ })
830
+ ] })
831
+ });
832
+ }
833
+ //#endregion
834
+ //#region src/game/item-icon/helpers.ts
835
+ const EMPTY_ITEM_ICON_LAYERS = {
836
+ iconUrl: "",
837
+ iconBgUrl: "",
838
+ iconMarkUrl: "",
839
+ iconOverlayUrl: "",
840
+ iconContentUrl: "",
841
+ iconTrans: "",
842
+ iconShowRarity: true
843
+ };
844
+ const ITEM_ICON_TRANSFORMS = {
845
+ formula: {
846
+ scale: .72,
847
+ pivotX: 1,
848
+ pivotY: .5
849
+ },
850
+ bigGem: {
851
+ scale: 1,
852
+ offsetX: 3,
853
+ offsetY: 4
854
+ }
855
+ };
856
+ function itemIconTransformStyle(key) {
857
+ const variable = (part, fallback) => `var(--ef-game-iicon-${key}-${part}, ${fallback})`;
858
+ if (key === "formula") {
859
+ const values = ITEM_ICON_TRANSFORMS.formula;
860
+ return {
861
+ transform: `scale(${variable("scale", values.scale)})`,
862
+ transformOrigin: `calc(${variable("pivot-x", values.pivotX)} * 100%) calc((1 - ${variable("pivot-y", values.pivotY)}) * 100%)`
863
+ };
864
+ }
865
+ const values = ITEM_ICON_TRANSFORMS.bigGem;
866
+ return {
867
+ transform: `translate(calc(${variable("offset-x", values.offsetX)} * 1%), calc(${variable("offset-y", values.offsetY)} * -1%)) scale(${variable("scale", values.scale)})`,
868
+ transformOrigin: "50% 50%"
869
+ };
870
+ }
871
+ function coerceItemIconTrans(value) {
872
+ return value === "formula" || value === "bigGem" ? value : "";
873
+ }
874
+ function pickItemIconLayers(source, prefix = "icon") {
875
+ const values = source ?? {};
876
+ const key = (name) => prefix === "icon" ? name : `${prefix}${name.slice(0, 1).toUpperCase()}${name.slice(1)}`;
877
+ const stringValue = (name) => {
878
+ const value = values[key(name)];
879
+ return typeof value === "string" ? value : "";
880
+ };
881
+ const showRarity = values[key("iconShowRarity")];
882
+ return {
883
+ iconUrl: stringValue("iconUrl"),
884
+ iconBgUrl: stringValue("iconBgUrl"),
885
+ iconMarkUrl: stringValue("iconMarkUrl"),
886
+ iconOverlayUrl: stringValue("iconOverlayUrl"),
887
+ iconContentUrl: stringValue("iconContentUrl"),
888
+ iconTrans: coerceItemIconTrans(values[key("iconTrans")]),
889
+ iconShowRarity: typeof showRarity === "boolean" ? showRarity : true
890
+ };
891
+ }
892
+ //#endregion
893
+ //#region src/game/item-icon/ItemIcon.tsx
894
+ function ItemImage({ src, alt, className, style, loading = "lazy", fallback = null }) {
895
+ const [failed, setFailed] = useState(false);
896
+ if (!src.trim() || failed) return /* @__PURE__ */ jsx(Fragment$1, { children: fallback });
897
+ return /* @__PURE__ */ jsx("img", {
898
+ src,
899
+ alt,
900
+ className,
901
+ style,
902
+ loading,
903
+ decoding: "async",
904
+ draggable: false,
905
+ onError: () => setFailed(true)
906
+ });
907
+ }
908
+ function ItemIcon({ layers, alt, className, style, fallback = null, loading, renderImage }) {
909
+ const trans = coerceItemIconTrans(layers.iconTrans);
910
+ const hasOverlay = Boolean(layers.iconOverlayUrl);
911
+ const iconStyle = trans === "formula" ? itemIconTransformStyle("formula") : trans === "bigGem" && !hasOverlay ? itemIconTransformStyle("bigGem") : void 0;
912
+ const overlayStyle = trans === "bigGem" ? itemIconTransformStyle("bigGem") : void 0;
913
+ const renderLayer = (role, props) => /* @__PURE__ */ jsx(Fragment, { children: renderImage ? renderImage(props) : /* @__PURE__ */ jsx(ItemImage, { ...props }) }, `${role}:${props.src}`);
914
+ return /* @__PURE__ */ jsxs("span", {
915
+ className: cn("ef-composite-item-icon", className),
916
+ style,
917
+ children: [
918
+ layers.iconBgUrl && renderLayer("background", {
919
+ src: layers.iconBgUrl,
920
+ alt: "",
921
+ className: "ef-composite-item-icon__layer",
922
+ loading
923
+ }),
924
+ renderLayer("art", {
925
+ src: layers.iconUrl,
926
+ alt,
927
+ className: "ef-composite-item-icon__layer ef-composite-item-icon__art",
928
+ style: iconStyle,
929
+ loading,
930
+ fallback
931
+ }),
932
+ hasOverlay && renderLayer("overlay", {
933
+ src: layers.iconOverlayUrl ?? "",
934
+ alt: "",
935
+ className: "ef-composite-item-icon__layer ef-composite-item-icon__overlay",
936
+ style: overlayStyle,
937
+ loading
938
+ }),
939
+ layers.iconContentUrl && renderLayer("content", {
940
+ src: layers.iconContentUrl,
941
+ alt: "",
942
+ className: "ef-composite-item-icon__layer",
943
+ style: {
944
+ transform: `scale(${80 / 180})`,
945
+ transformOrigin: "50% 50%"
946
+ },
947
+ loading
948
+ }),
949
+ layers.iconMarkUrl && renderLayer("mark", {
950
+ src: layers.iconMarkUrl,
951
+ alt: "",
952
+ className: "ef-composite-item-icon__layer",
953
+ loading
954
+ })
955
+ ]
956
+ });
957
+ }
958
+ //#endregion
959
+ //#region src/game/item-card/ItemCard.tsx
960
+ const KIND_LABEL = {
961
+ weapon: "武器",
962
+ item: "物品",
963
+ enemy: "敌人"
964
+ };
965
+ function isItemKind(kind) {
966
+ return kind === "weapon" || kind === "item" || kind === "enemy";
967
+ }
968
+ function clampRarity(rarity) {
969
+ return Math.min(Math.max(Math.trunc(rarity), 1), 6);
970
+ }
971
+ function nameFontSize(name) {
972
+ let em = 0;
973
+ for (const character of name) em += (character.codePointAt(0) ?? 0) < 11904 ? .5 : 1;
974
+ return em > 0 ? `clamp(9px, calc(100cqw / ${em}), 14px)` : void 0;
975
+ }
976
+ /** Wiki 物品卡片:名称横条或库存数量横条,共用完整图层图标。 */
977
+ const ItemCard = memo(function ItemCard({ kind, name, imageUrl, rarity, itemId, href, variant = "banner", count = 0, showName = true, className, style, icon, action, badges, ariaLabel, fallback, renderImage, renderLink, renderName, renderCount }) {
978
+ if (!isItemKind(kind)) return null;
979
+ const hasRarity = Number.isFinite(rarity) && rarity > 0 && (icon?.iconShowRarity ?? true);
980
+ const inventory = variant === "inventory";
981
+ const layers = {
982
+ ...EMPTY_ITEM_ICON_LAYERS,
983
+ ...icon,
984
+ iconUrl: imageUrl
985
+ };
986
+ const label = ariaLabel || name || KIND_LABEL[kind];
987
+ const renderedName = renderName ? renderName(name) : name || "—";
988
+ const tile = /* @__PURE__ */ jsxs("div", {
989
+ className: "ef-item-card-tile",
990
+ children: [
991
+ /* @__PURE__ */ jsx("span", {
992
+ "aria-hidden": "true",
993
+ className: "ef-item-card-background"
994
+ }),
995
+ /* @__PURE__ */ jsx("div", {
996
+ className: "ef-item-card-art",
997
+ children: /* @__PURE__ */ jsx(ItemIcon, {
998
+ layers,
999
+ alt: name,
1000
+ className: "ef-item-card-icon",
1001
+ renderImage,
1002
+ fallback: fallback === void 0 ? /* @__PURE__ */ jsx("span", {
1003
+ className: "ef-item-card-fallback",
1004
+ children: "NO IMAGE"
1005
+ }) : fallback
1006
+ })
1007
+ }),
1008
+ badges ? /* @__PURE__ */ jsx("div", {
1009
+ className: "ef-item-card-badges",
1010
+ children: badges
1011
+ }) : null,
1012
+ /* @__PURE__ */ jsxs("div", {
1013
+ className: "ef-item-card-strip",
1014
+ children: [/* @__PURE__ */ jsx("span", {
1015
+ "aria-hidden": "true",
1016
+ className: "ef-item-card-edge"
1017
+ }), /* @__PURE__ */ jsx("div", {
1018
+ className: "ef-item-card-strip-content",
1019
+ children: inventory ? /* @__PURE__ */ jsx("span", {
1020
+ className: "ef-item-card-count",
1021
+ children: renderCount ? renderCount(count) : count > 0 ? count : "\xA0"
1022
+ }) : /* @__PURE__ */ jsx("span", {
1023
+ className: "ef-item-card-name",
1024
+ style: { fontSize: nameFontSize(name) },
1025
+ title: name,
1026
+ children: showName ? renderedName : "\xA0"
1027
+ })
1028
+ })]
1029
+ })
1030
+ ]
1031
+ });
1032
+ const inner = /* @__PURE__ */ jsxs("div", {
1033
+ className: cn("ef-item-card", className),
1034
+ style,
1035
+ "data-kind": kind,
1036
+ "data-variant": variant,
1037
+ "data-rarity": hasRarity ? clampRarity(rarity) : void 0,
1038
+ "data-item-tooltip-id": !action && !href && kind === "item" ? itemId : void 0,
1039
+ children: [tile, inventory && showName && /* @__PURE__ */ jsx("span", {
1040
+ className: "ef-item-card-caption",
1041
+ title: name,
1042
+ children: renderedName
1043
+ })]
1044
+ });
1045
+ if (action) return /* @__PURE__ */ jsxs("button", {
1046
+ type: "button",
1047
+ onClick: action.onSelect,
1048
+ disabled: action.disabled,
1049
+ "aria-pressed": action.selected,
1050
+ "aria-label": [action.ariaLabel || label, action.disabledReason].filter(Boolean).join(" · "),
1051
+ className: "ef-item-card-action",
1052
+ "data-selected": action.selected ? "" : void 0,
1053
+ children: [inner, action.disabledReason && /* @__PURE__ */ jsx("span", {
1054
+ className: "ef-item-card-disabled-reason",
1055
+ children: action.disabledReason
1056
+ })]
1057
+ });
1058
+ if (href) {
1059
+ const linkProps = {
1060
+ href,
1061
+ children: inner,
1062
+ className: "ef-item-card-link",
1063
+ "aria-label": label,
1064
+ "data-item-tooltip-id": kind === "item" ? itemId : void 0
1065
+ };
1066
+ return renderLink ? renderLink(linkProps) : /* @__PURE__ */ jsx("a", { ...linkProps });
1067
+ }
1068
+ return inner;
1069
+ });
1070
+ //#endregion
1071
+ //#region src/game/item-tooltip-card/icons.tsx
1072
+ function ObtainFallbackIcon() {
1073
+ return /* @__PURE__ */ jsxs("svg", {
1074
+ "aria-hidden": "true",
1075
+ viewBox: "0 0 24 24",
1076
+ fill: "none",
1077
+ stroke: "currentColor",
1078
+ strokeWidth: "2",
1079
+ strokeLinecap: "round",
1080
+ strokeLinejoin: "round",
1081
+ children: [
1082
+ /* @__PURE__ */ jsx("path", { d: "M12 22V12M20.27 18.27 22 20" }),
1083
+ /* @__PURE__ */ jsx("path", { d: "M21 10.498V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.729l7 4a2 2 0 0 0 2 .001l.98-.559" }),
1084
+ /* @__PURE__ */ jsx("path", { d: "M3.29 7 12 12l8.71-5M7.5 4.27l8.997 5.148" }),
1085
+ /* @__PURE__ */ jsx("circle", {
1086
+ cx: "18.5",
1087
+ cy: "16.5",
1088
+ r: "2.5"
1089
+ })
1090
+ ]
1091
+ });
1092
+ }
1093
+ function ObtainLinkIcon() {
1094
+ return /* @__PURE__ */ jsx("svg", {
1095
+ "aria-hidden": "true",
1096
+ className: "ef-item-tooltip-card__chevrons",
1097
+ viewBox: "0 0 24 24",
1098
+ fill: "none",
1099
+ stroke: "currentColor",
1100
+ strokeWidth: "3",
1101
+ strokeLinecap: "round",
1102
+ strokeLinejoin: "round",
1103
+ children: /* @__PURE__ */ jsx("path", { d: "m6 17 5-5-5-5m7 10 5-5-5-5" })
1104
+ });
1105
+ }
1106
+ //#endregion
1107
+ //#region src/game/item-tooltip-card/ItemTooltipCard.tsx
1108
+ const plainText = (text) => text;
1109
+ const defaultLink = (props) => /* @__PURE__ */ jsx("a", { ...props });
1110
+ /** 固定灰色游戏皮肤的物品详情;文章路由与富文本由使用方接入。 */
1111
+ function ItemTooltipCard({ item, className, style, fontFamily, obtainLabel = "获取途径", renderText = plainText, renderImage, renderLink = defaultLink }) {
1112
+ const rarity = Number.isFinite(item.rarity) ? Math.min(6, Math.max(1, Math.trunc(item.rarity))) : 1;
1113
+ const obtainWays = item.obtainWays ?? [];
1114
+ return /* @__PURE__ */ jsxs("article", {
1115
+ className: cn("ef-item-tooltip-card", className),
1116
+ style: {
1117
+ fontFamily,
1118
+ ...style
1119
+ },
1120
+ "data-rarity": rarity,
1121
+ children: [
1122
+ /* @__PURE__ */ jsxs("header", {
1123
+ className: "ef-item-tooltip-card__header",
1124
+ children: [
1125
+ /* @__PURE__ */ jsx("span", {
1126
+ "aria-hidden": "true",
1127
+ className: "ef-item-tooltip-card__header-texture"
1128
+ }),
1129
+ /* @__PURE__ */ jsx("h2", { children: item.name }),
1130
+ item.href && renderLink({
1131
+ href: item.href,
1132
+ className: "ef-item-tooltip-card__detail-link",
1133
+ "aria-label": item.name,
1134
+ children: /* @__PURE__ */ jsx("span", {
1135
+ "aria-hidden": "true",
1136
+ className: "ef-item-tooltip-card__detail-link-icon"
1137
+ })
1138
+ })
1139
+ ]
1140
+ }),
1141
+ /* @__PURE__ */ jsxs("div", {
1142
+ className: "ef-item-tooltip-card__body",
1143
+ children: [
1144
+ /* @__PURE__ */ jsx("span", {
1145
+ "aria-hidden": "true",
1146
+ className: "ef-item-tooltip-card__detail-texture"
1147
+ }),
1148
+ /* @__PURE__ */ jsxs("div", {
1149
+ className: "ef-item-tooltip-card__hero",
1150
+ children: [
1151
+ /* @__PURE__ */ jsx("p", {
1152
+ className: "ef-item-tooltip-card__category",
1153
+ children: item.category
1154
+ }),
1155
+ /* @__PURE__ */ jsx("span", {
1156
+ "aria-hidden": "true",
1157
+ className: "ef-item-tooltip-card__marker"
1158
+ }),
1159
+ /* @__PURE__ */ jsx("span", {
1160
+ "aria-hidden": "true",
1161
+ className: "ef-item-tooltip-card__barcode"
1162
+ }),
1163
+ /* @__PURE__ */ jsx(ItemIcon, {
1164
+ layers: {
1165
+ ...pickItemIconLayers(item),
1166
+ iconBgUrl: "",
1167
+ iconMarkUrl: ""
1168
+ },
1169
+ alt: item.name,
1170
+ loading: "eager",
1171
+ className: "ef-item-tooltip-card__icon",
1172
+ renderImage
1173
+ })
1174
+ ]
1175
+ }),
1176
+ /* @__PURE__ */ jsx("span", {
1177
+ "aria-hidden": "true",
1178
+ className: "ef-item-tooltip-card__color-strip"
1179
+ }),
1180
+ (item.usage || item.description) && /* @__PURE__ */ jsxs("div", {
1181
+ className: "ef-item-tooltip-card__text",
1182
+ children: [item.usage && /* @__PURE__ */ jsx("p", { children: renderText(item.usage) }), item.description && /* @__PURE__ */ jsx("p", {
1183
+ className: "ef-item-tooltip-card__description",
1184
+ children: renderText(item.description)
1185
+ })]
1186
+ })
1187
+ ]
1188
+ }),
1189
+ (obtainWays.length > 0 || item.noObtainWayHint) && /* @__PURE__ */ jsxs("section", {
1190
+ "aria-label": obtainLabel,
1191
+ children: [
1192
+ /* @__PURE__ */ jsx("h3", {
1193
+ className: "ef-item-tooltip-card__obtain-title",
1194
+ children: obtainLabel
1195
+ }),
1196
+ /* @__PURE__ */ jsx("ul", {
1197
+ className: "ef-item-tooltip-card__obtain-list",
1198
+ children: obtainWays.map((way, index) => {
1199
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1200
+ /* @__PURE__ */ jsx(ItemIcon, {
1201
+ layers: { iconUrl: way.iconUrl ?? "" },
1202
+ alt: "",
1203
+ loading: "eager",
1204
+ className: "ef-item-tooltip-card__obtain-icon",
1205
+ renderImage,
1206
+ fallback: /* @__PURE__ */ jsx(ObtainFallbackIcon, {})
1207
+ }),
1208
+ /* @__PURE__ */ jsx("span", {
1209
+ className: "ef-item-tooltip-card__obtain-name",
1210
+ children: renderText(way.name)
1211
+ }),
1212
+ way.href && /* @__PURE__ */ jsx(ObtainLinkIcon, {})
1213
+ ] });
1214
+ return /* @__PURE__ */ jsx("li", { children: way.href ? renderLink({
1215
+ href: way.href,
1216
+ className: "ef-item-tooltip-card__obtain-row",
1217
+ children: content
1218
+ }) : /* @__PURE__ */ jsx("div", {
1219
+ className: "ef-item-tooltip-card__obtain-row",
1220
+ children: content
1221
+ }) }, `${way.id ?? index}-${index}`);
1222
+ })
1223
+ }),
1224
+ item.noObtainWayHint && /* @__PURE__ */ jsx("p", {
1225
+ className: "ef-item-tooltip-card__obtain-hint",
1226
+ children: renderText(item.noObtainWayHint)
1227
+ })
1228
+ ]
1229
+ })
1230
+ ]
1231
+ });
1232
+ }
1233
+ //#endregion
1234
+ //#region src/game/item-hover-card/events.ts
1235
+ function bindItemHoverEvents(options, select, panel) {
1236
+ const doc = options.root.ownerDocument ?? options.root;
1237
+ const win = doc.defaultView;
1238
+ let active = null;
1239
+ let dismissed = null;
1240
+ let focusedTrigger = null;
1241
+ let pointerTarget = null;
1242
+ let openTimer;
1243
+ let closeTimer;
1244
+ let shown = false;
1245
+ let activeId;
1246
+ let activeTarget = null;
1247
+ const cancel = () => {
1248
+ win.clearTimeout(openTimer);
1249
+ win.clearTimeout(closeTimer);
1250
+ openTimer = closeTimer = void 0;
1251
+ };
1252
+ const close = () => {
1253
+ cancel();
1254
+ active = null;
1255
+ activeId = void 0;
1256
+ activeTarget = null;
1257
+ shown = false;
1258
+ unwatch();
1259
+ select(null);
1260
+ };
1261
+ const inPanel = (node) => node instanceof Node && Boolean(panel()?.contains(node));
1262
+ const inside = (node) => inPanel(node) || node instanceof Node && Boolean(active?.contains(node));
1263
+ const hostOf = (node) => {
1264
+ if (!(node instanceof Element) || inPanel(node)) return null;
1265
+ if (node.closest("[inert], [aria-disabled='true'], :disabled")) return null;
1266
+ const host = node.closest("[data-item-tooltip-id]");
1267
+ const scope = options.rootOf ? options.rootOf() : options.root;
1268
+ if (!host || !scope?.contains(host)) return null;
1269
+ if (host.closest("[inert], [aria-disabled='true'], :disabled")) return null;
1270
+ return options.anchorOf(host) ? host : null;
1271
+ };
1272
+ const open = (host, delay, target) => {
1273
+ if (host === dismissed) return;
1274
+ dismissed = null;
1275
+ win.clearTimeout(closeTimer);
1276
+ closeTimer = void 0;
1277
+ if (host === active && shown) {
1278
+ activeTarget = target;
1279
+ if (activeId !== host.dataset.itemTooltipId) {
1280
+ activeId = host.dataset.itemTooltipId;
1281
+ select(options.anchorOf(host));
1282
+ }
1283
+ return;
1284
+ }
1285
+ if (host === active && openTimer !== void 0 && delay > 0) return;
1286
+ cancel();
1287
+ active = host;
1288
+ activeId = host.dataset.itemTooltipId;
1289
+ activeTarget = target;
1290
+ shown = false;
1291
+ watch();
1292
+ select(null);
1293
+ openTimer = win.setTimeout(() => {
1294
+ openTimer = void 0;
1295
+ const anchor = host.isConnected && hostOf(target) === host ? options.anchorOf(host) : null;
1296
+ if (active !== host) return;
1297
+ if (!anchor) return close();
1298
+ shown = true;
1299
+ activeId = host.dataset.itemTooltipId;
1300
+ select(anchor);
1301
+ }, Math.max(0, delay));
1302
+ };
1303
+ const leave = () => {
1304
+ if (inside(pointerTarget) || inside(doc.activeElement)) {
1305
+ win.clearTimeout(closeTimer);
1306
+ closeTimer = void 0;
1307
+ return;
1308
+ }
1309
+ cancel();
1310
+ closeTimer = win.setTimeout(close, Math.max(0, options.closeDelay));
1311
+ };
1312
+ const clearDismissed = (target, next) => {
1313
+ if (target instanceof Node && dismissed?.contains(target) && !(next instanceof Node && dismissed.contains(next))) dismissed = null;
1314
+ };
1315
+ const pointerOver = (event) => {
1316
+ if (event.pointerType === "touch") return;
1317
+ pointerTarget = event.target;
1318
+ if (inPanel(event.target)) return cancel();
1319
+ const host = hostOf(event.target);
1320
+ if (host) open(host, options.openDelay, event.target);
1321
+ };
1322
+ const pointerOut = (event) => {
1323
+ if (event.pointerType === "touch") return;
1324
+ pointerTarget = event.relatedTarget;
1325
+ clearDismissed(event.target, event.relatedTarget);
1326
+ if (active && inside(event.target) && !inside(event.relatedTarget)) leave();
1327
+ };
1328
+ const focusIn = (event) => {
1329
+ if (inPanel(event.target)) return cancel();
1330
+ const host = hostOf(event.target);
1331
+ if (host) {
1332
+ focusedTrigger = event.target instanceof HTMLElement ? event.target : host;
1333
+ open(host, 0, event.target);
1334
+ }
1335
+ };
1336
+ const focusOut = (event) => {
1337
+ clearDismissed(event.target, event.relatedTarget);
1338
+ if (active && inside(event.target) && !inside(event.relatedTarget)) leave();
1339
+ };
1340
+ const scroll = (event) => {
1341
+ if (active && !inPanel(event.target)) close();
1342
+ };
1343
+ const dismiss = (restoreFocus = false) => {
1344
+ const available = (node) => Boolean(node?.isConnected && !node.closest("[hidden],[inert],[aria-disabled='true'],:disabled"));
1345
+ const restore = restoreFocus && inPanel(doc.activeElement) && active ? focusedTrigger && available(focusedTrigger) && active.contains(focusedTrigger) ? focusedTrigger : Array.from(active.querySelectorAll("a[href],button:not(:disabled),[tabindex='0']")).find(available) ?? (available(active) && active.matches("a[href],button,[tabindex]") ? active : null) ?? (available(active.closest("[role='dialog']")) ? active.closest("[role='dialog']") : null) : null;
1346
+ dismissed = active;
1347
+ close();
1348
+ if (restore?.isConnected) restore.focus({ preventScroll: true });
1349
+ };
1350
+ const listeners = [
1351
+ ["pointerover", pointerOver],
1352
+ ["pointerout", pointerOut],
1353
+ ["focusin", focusIn],
1354
+ ["focusout", focusOut]
1355
+ ];
1356
+ for (const [type, listener] of listeners) doc.addEventListener(type, listener);
1357
+ const mutation = new MutationObserver(() => {
1358
+ if (!active) return;
1359
+ if (hostOf(activeTarget) !== active || !active.isConnected) return dismiss(true);
1360
+ if (shown && activeId !== active.dataset.itemTooltipId) {
1361
+ activeId = active.dataset.itemTooltipId;
1362
+ const anchor = options.anchorOf(active);
1363
+ if (anchor) select(anchor);
1364
+ else dismiss(true);
1365
+ }
1366
+ });
1367
+ let watching = false;
1368
+ function watch() {
1369
+ if (watching) return;
1370
+ watching = true;
1371
+ mutation.observe(doc, {
1372
+ subtree: true,
1373
+ childList: true,
1374
+ attributes: true,
1375
+ attributeFilter: [
1376
+ "data-item-tooltip-id",
1377
+ "disabled",
1378
+ "inert",
1379
+ "aria-disabled"
1380
+ ]
1381
+ });
1382
+ win.addEventListener("scroll", scroll, true);
1383
+ win.addEventListener("resize", close);
1384
+ }
1385
+ function unwatch() {
1386
+ if (!watching) return;
1387
+ watching = false;
1388
+ mutation.disconnect();
1389
+ win.removeEventListener("scroll", scroll, true);
1390
+ win.removeEventListener("resize", close);
1391
+ }
1392
+ return {
1393
+ close,
1394
+ dismiss,
1395
+ dispose() {
1396
+ cancel();
1397
+ unwatch();
1398
+ for (const [type, listener] of listeners) doc.removeEventListener(type, listener);
1399
+ }
1400
+ };
1401
+ }
1402
+ //#endregion
1403
+ //#region src/game/item-hover-card/ItemHoverCard.tsx
1404
+ /** 一个监听层为网格内所有 data-item-tooltip-id 提供物品预览。 */
1405
+ function ItemHoverCardLayer({ resolveItem, root, rootRef, disabled = false, disableOnMobile = true, openDelay = 180, closeDelay = 160, gap = 8, width = 320, className, style, cardProps, onOpenChange }) {
1406
+ const [anchor, setAnchor] = useState(null);
1407
+ const panelRef = useRef(null);
1408
+ const controls = useRef(null);
1409
+ const binding = useRef(null);
1410
+ const resolveRef = useRef(resolveItem);
1411
+ const scopeRef = useRef({
1412
+ root,
1413
+ rootRef
1414
+ });
1415
+ const changeRef = useRef(onOpenChange);
1416
+ useLayoutEffect(() => {
1417
+ resolveRef.current = resolveItem;
1418
+ scopeRef.current = {
1419
+ root,
1420
+ rootRef
1421
+ };
1422
+ changeRef.current = onOpenChange;
1423
+ });
1424
+ const close = useCallback(() => controls.current?.close(), []);
1425
+ useEffect(() => {
1426
+ const delegateRoot = disabled ? null : rootRef?.current ?? root ?? document;
1427
+ const previous = binding.current;
1428
+ const delegateDocument = delegateRoot ? delegateRoot.ownerDocument ?? delegateRoot : null;
1429
+ const scope = rootRef ? rootRef.current : root ?? delegateRoot;
1430
+ if (anchor && !scope?.contains(anchor.host)) controls.current?.close();
1431
+ if (previous && previous.root === delegateDocument && previous.disableOnMobile === disableOnMobile && previous.openDelay === openDelay && previous.closeDelay === closeDelay) return;
1432
+ previous?.dispose();
1433
+ binding.current = null;
1434
+ if (!delegateRoot) return;
1435
+ const media = (delegateRoot.ownerDocument ?? delegateRoot).defaultView.matchMedia("(min-width: 768px) and (hover: hover) and (pointer: fine)");
1436
+ const bind = () => {
1437
+ controls.current?.close();
1438
+ controls.current?.dispose();
1439
+ controls.current = null;
1440
+ if (disableOnMobile && !media.matches) return;
1441
+ controls.current = bindItemHoverEvents({
1442
+ root: delegateRoot,
1443
+ rootOf: () => {
1444
+ const scope = scopeRef.current;
1445
+ return scope.rootRef ? scope.rootRef.current : scope.root ?? delegateDocument;
1446
+ },
1447
+ anchorOf: (host) => {
1448
+ const itemId = host.dataset.itemTooltipId;
1449
+ return itemId && resolveRef.current(itemId, host) ? {
1450
+ host,
1451
+ itemId
1452
+ } : null;
1453
+ },
1454
+ openDelay,
1455
+ closeDelay
1456
+ }, setAnchor, () => panelRef.current);
1457
+ };
1458
+ bind();
1459
+ media.addEventListener("change", bind);
1460
+ binding.current = {
1461
+ root: delegateDocument,
1462
+ disableOnMobile,
1463
+ openDelay,
1464
+ closeDelay,
1465
+ dispose: () => {
1466
+ media.removeEventListener("change", bind);
1467
+ controls.current?.close();
1468
+ controls.current?.dispose();
1469
+ controls.current = null;
1470
+ }
1471
+ };
1472
+ });
1473
+ useEffect(() => () => {
1474
+ binding.current?.dispose();
1475
+ binding.current = null;
1476
+ }, []);
1477
+ const currentId = anchor?.host.dataset.itemTooltipId;
1478
+ const item = !disabled && anchor && currentId ? resolveItem(currentId, anchor.host) : null;
1479
+ const activeId = item ? currentId ?? null : null;
1480
+ useEffect(() => {
1481
+ changeRef.current?.(Boolean(activeId), activeId);
1482
+ }, [activeId]);
1483
+ if (!anchor || !item) return null;
1484
+ return /* @__PURE__ */ jsx(ItemHoverPanel, {
1485
+ anchor,
1486
+ item,
1487
+ panelRef,
1488
+ close,
1489
+ dismiss: (restore) => controls.current?.dismiss(restore),
1490
+ gap,
1491
+ width,
1492
+ className,
1493
+ style,
1494
+ cardProps
1495
+ });
1496
+ }
1497
+ function ItemHoverPanel({ anchor, item, panelRef, close, dismiss, gap, width, className, style, cardProps }) {
1498
+ const id = useId();
1499
+ const { anchorRef, portalRef } = usePortalScope();
1500
+ useLayoutEffect(() => {
1501
+ const doc = anchor.host.ownerDocument;
1502
+ const attributes = [
1503
+ "aria-controls",
1504
+ "aria-expanded",
1505
+ "aria-haspopup"
1506
+ ];
1507
+ let trigger = null;
1508
+ let previous = [];
1509
+ const restoreAria = () => {
1510
+ if (!trigger) return;
1511
+ attributes.forEach((name, index) => {
1512
+ const value = previous[index];
1513
+ if (value == null) trigger?.removeAttribute(name);
1514
+ else trigger?.setAttribute(name, value);
1515
+ });
1516
+ };
1517
+ const syncAria = () => {
1518
+ const next = anchor.host.contains(doc.activeElement) ? doc.activeElement : anchor.host;
1519
+ if (next === trigger) return;
1520
+ restoreAria();
1521
+ trigger = next;
1522
+ previous = attributes.map((name) => trigger.getAttribute(name));
1523
+ trigger.setAttribute("aria-controls", id);
1524
+ trigger.setAttribute("aria-expanded", "true");
1525
+ trigger.setAttribute("aria-haspopup", "dialog");
1526
+ };
1527
+ syncAria();
1528
+ const focusPanel = (event) => {
1529
+ if (event.defaultPrevented) return;
1530
+ const first = panelRef.current?.querySelector("a[href],button:not(:disabled),[tabindex='0']");
1531
+ if (event.key === "Tab" && !first) {
1532
+ flushSync(close);
1533
+ return;
1534
+ }
1535
+ if (event.key === "ArrowDown" && first) {
1536
+ event.preventDefault();
1537
+ first.focus({ preventScroll: true });
1538
+ }
1539
+ };
1540
+ anchor.host.addEventListener("keydown", focusPanel);
1541
+ anchor.host.addEventListener("focusin", syncAria);
1542
+ return () => {
1543
+ anchor.host.removeEventListener("keydown", focusPanel);
1544
+ anchor.host.removeEventListener("focusin", syncAria);
1545
+ restoreAria();
1546
+ };
1547
+ }, [
1548
+ anchor.host,
1549
+ close,
1550
+ id,
1551
+ panelRef
1552
+ ]);
1553
+ useEffect(() => {
1554
+ const observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
1555
+ if (entries.some((entry) => !entry.isIntersecting)) close();
1556
+ });
1557
+ observer?.observe(anchor.host);
1558
+ return () => observer?.disconnect();
1559
+ }, [anchor.host, close]);
1560
+ return /* @__PURE__ */ jsx(Popover.Root, {
1561
+ open: true,
1562
+ onOpenChange: (open, details) => {
1563
+ if (open) return;
1564
+ if (details.reason === "focus-out") details.cancel();
1565
+ else dismiss(details.reason === "escape-key");
1566
+ },
1567
+ children: /* @__PURE__ */ jsx(Popover.Portal, {
1568
+ ref: (node) => {
1569
+ anchorRef(anchor.host);
1570
+ portalRef(node);
1571
+ },
1572
+ container: anchor.host.closest("dialog,[role='dialog'],[role='alertdialog']") ?? void 0,
1573
+ children: /* @__PURE__ */ jsx(Popover.Positioner, {
1574
+ anchor: anchor.host,
1575
+ positionMethod: "fixed",
1576
+ side: "right",
1577
+ align: "start",
1578
+ sideOffset: gap,
1579
+ collisionBoundary: [],
1580
+ collisionPadding: 8,
1581
+ collisionAvoidance: {
1582
+ side: "flip",
1583
+ align: "shift"
1584
+ },
1585
+ className: "ef-item-hover-positioner",
1586
+ style: {
1587
+ width,
1588
+ ...style
1589
+ },
1590
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
1591
+ ref: panelRef,
1592
+ id,
1593
+ "aria-label": item.name,
1594
+ initialFocus: false,
1595
+ finalFocus: false,
1596
+ "data-item-tooltip-panel": "",
1597
+ className: cn("ef-item-hover-panel", className),
1598
+ children: /* @__PURE__ */ jsx(ItemTooltipCard, {
1599
+ ...cardProps,
1600
+ item
1601
+ })
1602
+ })
1603
+ })
1604
+ })
1605
+ });
1606
+ }
1607
+ function ItemHoverCard({ item, children, triggerProps, ...layerProps }) {
1608
+ const rootRef = useRef(null);
1609
+ const itemId = useId();
1610
+ return /* @__PURE__ */ jsxs("span", {
1611
+ ...triggerProps,
1612
+ className: cn("ef-item-hover-trigger", triggerProps?.className),
1613
+ ref: rootRef,
1614
+ "data-item-tooltip-id": itemId,
1615
+ children: [children, /* @__PURE__ */ jsx(ItemHoverCardLayer, {
1616
+ ...layerProps,
1617
+ rootRef,
1618
+ resolveItem: () => item
1619
+ })]
1620
+ });
1621
+ }
1622
+ //#endregion
1623
+ //#region src/game/rounded-button/RoundedIconButton.tsx
1624
+ /** 24px 圆形图标按钮;纹理与双边线只作装饰。 */
1625
+ function RoundedIconButton({ type = "button", disabled = false, children, className, ...props }) {
1626
+ return /* @__PURE__ */ jsxs("button", {
1627
+ ...props,
1628
+ type,
1629
+ disabled,
1630
+ className: cn("ef-rounded-icon-button", className),
1631
+ children: [
1632
+ /* @__PURE__ */ jsx("span", {
1633
+ "aria-hidden": "true",
1634
+ className: "ef-rounded-icon-button-texture"
1635
+ }),
1636
+ /* @__PURE__ */ jsx("span", {
1637
+ "aria-hidden": "true",
1638
+ className: "ef-rounded-icon-button-border"
1639
+ }),
1640
+ /* @__PURE__ */ jsx("span", {
1641
+ "aria-hidden": "true",
1642
+ className: "ef-rounded-icon-button-content",
1643
+ children
1644
+ })
1645
+ ]
1646
+ });
1647
+ }
1648
+ //#endregion
1649
+ //#region src/game/rounded-select/RoundedSelect.tsx
1650
+ /** 圆角触发器与半覆盖面板;隐藏项不进入列表,禁用项不能选择。 */
1651
+ function RoundedSelect({ options, value, defaultValue, onValueChange, placeholder, name, form, required: requiredProp, disabled: disabledProp, invalid: invalidProp, open, defaultOpen = false, onOpenChange, id: idProp, "aria-label": ariaLabel, "aria-describedby": describedByProp, className, classNames, ref }) {
1652
+ const { id, disabled, required, invalid, ...field } = useFieldControl({
1653
+ id: idProp,
1654
+ disabled: disabledProp,
1655
+ required: requiredProp,
1656
+ invalid: invalidProp,
1657
+ "aria-describedby": describedByProp
1658
+ });
1659
+ const [expanded, setExpanded] = useControllableState({
1660
+ value: open,
1661
+ defaultValue: defaultOpen
1662
+ });
1663
+ const [currentValue, setCurrentValue] = useControllableState({
1664
+ value,
1665
+ defaultValue: defaultValue ?? null,
1666
+ onChange: (next) => {
1667
+ if (next !== null) onValueChange?.(next);
1668
+ }
1669
+ });
1670
+ if (disabled && expanded && open === void 0) setExpanded(false);
1671
+ const popupOpen = expanded && !disabled;
1672
+ const [hovered, setHovered] = useState(false);
1673
+ const [pressed, setPressed] = useState(false);
1674
+ const boxRef = useRef(null);
1675
+ const { anchorRef, portalRef } = usePortalScope();
1676
+ return /* @__PURE__ */ jsxs(Select.Root, {
1677
+ items: options,
1678
+ value: currentValue,
1679
+ onValueChange: setCurrentValue,
1680
+ name,
1681
+ form,
1682
+ required,
1683
+ disabled,
1684
+ modal: false,
1685
+ open: popupOpen,
1686
+ onOpenChange: (next) => {
1687
+ setExpanded(next);
1688
+ if (next !== expanded) onOpenChange?.(next);
1689
+ },
1690
+ children: [/* @__PURE__ */ jsx("div", {
1691
+ ref: (node) => {
1692
+ boxRef.current = node;
1693
+ anchorRef(node);
1694
+ },
1695
+ className: cn("ef-rounded-select", className, classNames?.container),
1696
+ "data-invalid": invalid ? "" : void 0,
1697
+ children: /* @__PURE__ */ jsx(Select.Trigger, {
1698
+ ref,
1699
+ id,
1700
+ "aria-label": ariaLabel,
1701
+ "aria-invalid": invalid || void 0,
1702
+ "aria-describedby": field["aria-describedby"],
1703
+ className: cn("ef-rounded-select-trigger", classNames?.trigger),
1704
+ onPointerEnter: (event) => setHovered(event.pointerType !== "touch"),
1705
+ onPointerLeave: () => {
1706
+ setHovered(false);
1707
+ setPressed(false);
1708
+ },
1709
+ onPointerDown: (event) => setPressed(event.button === 0),
1710
+ onPointerUp: () => setPressed(false),
1711
+ onPointerCancel: () => setPressed(false),
1712
+ children: /* @__PURE__ */ jsx(RoundedTriggerContent, { placeholder })
1713
+ })
1714
+ }), /* @__PURE__ */ jsx(Select.Portal, {
1715
+ ref: portalRef,
1716
+ children: /* @__PURE__ */ jsxs(Select.Positioner, {
1717
+ anchor: boxRef,
1718
+ alignItemWithTrigger: false,
1719
+ side: "bottom",
1720
+ align: "start",
1721
+ sideOffset: -16,
1722
+ className: "ef-rounded-select-positioner",
1723
+ children: [/* @__PURE__ */ jsx("div", {
1724
+ "aria-hidden": "true",
1725
+ className: cn("ef-rounded-select-trigger ef-rounded-select-mirror", classNames?.trigger),
1726
+ "data-invalid": invalid ? "" : void 0,
1727
+ "data-hovered": hovered ? "" : void 0,
1728
+ "data-pressed": pressed ? "" : void 0,
1729
+ children: /* @__PURE__ */ jsx(RoundedTriggerContent, { placeholder })
1730
+ }), /* @__PURE__ */ jsx(Select.Popup, {
1731
+ className: cn("ef-rounded-select-popup", classNames?.optionContainer),
1732
+ children: /* @__PURE__ */ jsxs(ScrollArea.Root, {
1733
+ className: cn("ef-rounded-select-scroll-area", classNames?.optionScrollArea),
1734
+ children: [/* @__PURE__ */ jsx(ScrollArea.Viewport, {
1735
+ className: "ef-rounded-select-viewport",
1736
+ style: { overflowX: "hidden" },
1737
+ render: /* @__PURE__ */ jsx(Select.List, { role: "listbox" }),
1738
+ children: /* @__PURE__ */ jsx(ScrollArea.Content, { children: options.filter((option) => !option.hidden).map((option) => /* @__PURE__ */ jsx(Select.Item, {
1739
+ value: option.value,
1740
+ label: option.textValue,
1741
+ disabled: disabled || option.disabled,
1742
+ className: cn("ef-rounded-select-option", classNames?.option),
1743
+ render: (itemProps, state) => {
1744
+ const left = typeof option.left === "function" ? option.left(state.selected) : option.left;
1745
+ const right = typeof option.right === "function" ? option.right(state.selected) : option.right;
1746
+ return /* @__PURE__ */ jsxs("div", {
1747
+ ...itemProps,
1748
+ children: [
1749
+ left != null && /* @__PURE__ */ jsx("span", {
1750
+ className: "ef-rounded-select-left",
1751
+ children: left
1752
+ }),
1753
+ /* @__PURE__ */ jsx(Select.ItemText, {
1754
+ className: "ef-rounded-select-option-label",
1755
+ children: option.label
1756
+ }),
1757
+ right != null && /* @__PURE__ */ jsx("span", {
1758
+ className: "ef-rounded-select-right",
1759
+ children: right
1760
+ }),
1761
+ /* @__PURE__ */ jsx(RoundedOptionIndicator, {})
1762
+ ]
1763
+ });
1764
+ }
1765
+ }, option.value)) })
1766
+ }), /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
1767
+ orientation: "vertical",
1768
+ className: "ef-rounded-select-scrollbar",
1769
+ children: /* @__PURE__ */ jsx(ScrollArea.Thumb, { className: "ef-rounded-select-thumb" })
1770
+ })]
1771
+ })
1772
+ })]
1773
+ })
1774
+ })]
1775
+ });
1776
+ }
1777
+ function RoundedTriggerContent({ placeholder }) {
1778
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Select.Value, {
1779
+ placeholder,
1780
+ className: "ef-rounded-select-value"
1781
+ }), /* @__PURE__ */ jsx(Select.Icon, {
1782
+ className: "ef-rounded-select-chevron",
1783
+ children: /* @__PURE__ */ jsx("svg", {
1784
+ "aria-hidden": "true",
1785
+ viewBox: "0 0 1024 1024",
1786
+ width: "0.6em",
1787
+ height: "0.6em",
1788
+ fill: "currentColor",
1789
+ children: /* @__PURE__ */ jsx("path", { d: "M957.056 338.624C951.84 327.296 940.512 320 928 320H96c-12.512 0-23.84 7.296-29.088 18.624-5.216 11.36-3.328 24.704 4.768 34.208l416 485.344c6.08 7.104 14.944 11.2 24.288 11.2s18.208-4.096 24.288-11.2l416-485.344c8.096-9.472 9.952-22.88 4.768-34.208Z" })
1790
+ })
1791
+ })] });
1792
+ }
1793
+ function RoundedOptionIndicator() {
1794
+ return /* @__PURE__ */ jsx("span", {
1795
+ className: "ef-rounded-select-check",
1796
+ children: /* @__PURE__ */ jsx(Select.ItemIndicator, { children: /* @__PURE__ */ jsx("svg", {
1797
+ "aria-hidden": "true",
1798
+ width: "0.85em",
1799
+ height: "0.85em",
1800
+ viewBox: "0 0 24 24",
1801
+ fill: "none",
1802
+ stroke: "currentColor",
1803
+ strokeWidth: "3",
1804
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
1805
+ }) })
1806
+ });
1807
+ }
1808
+ //#endregion
1809
+ //#region src/game/rounded-toggle/keyboard.ts
1810
+ function roundedGroupBoundary(event) {
1811
+ if (event.defaultPrevented || event.key !== "Home" && event.key !== "End") return;
1812
+ const options = event.currentTarget.querySelectorAll("[data-rounded-option]:not([disabled]):not([aria-disabled=\"true\"])");
1813
+ const target = event.key === "Home" ? options[0] : options[options.length - 1];
1814
+ if (!target) return;
1815
+ event.preventDefault();
1816
+ target.focus();
1817
+ target.click();
1818
+ }
1819
+ //#endregion
1820
+ //#region src/game/rounded-toggle/spring-highlight.ts
1821
+ const DURATION = 650;
1822
+ const SAMPLES = 65;
1823
+ /** stiffness 300、damping 30、mass 1 的位移响应。 */
1824
+ function roundedSpringProgress(milliseconds) {
1825
+ const time = Math.max(0, milliseconds) / 1e3;
1826
+ const frequency = Math.sqrt(75);
1827
+ return 1 - Math.exp(-15 * time) * (Math.cos(frequency * time) + 15 / frequency * Math.sin(frequency * time));
1828
+ }
1829
+ const offsets = Array.from({ length: 66 }, (_, index) => ({
1830
+ offset: index / SAMPLES,
1831
+ time: index / SAMPLES * DURATION
1832
+ }));
1833
+ function springPosition(time, start, end, velocity) {
1834
+ const seconds = time / 1e3;
1835
+ const frequency = Math.sqrt(75);
1836
+ return start + (end - start) * roundedSpringProgress(time) + velocity * Math.exp(-15 * seconds) * Math.sin(frequency * seconds) / frequency;
1837
+ }
1838
+ function springVelocity(time, start, end, velocity) {
1839
+ const seconds = time / 1e3;
1840
+ const frequency = Math.sqrt(75);
1841
+ return Math.exp(-15 * seconds) * (velocity * Math.cos(frequency * seconds) + (300 * (end - start) - 15 * velocity) / frequency * Math.sin(frequency * seconds));
1842
+ }
1843
+ function translation(transform) {
1844
+ if (transform === "none") return 0;
1845
+ const values = transform.match(/^matrix\((.+)\)$/)?.[1]?.split(",");
1846
+ return values ? Number.parseFloat(values[4] ?? "0") : 0;
1847
+ }
1848
+ function useSpringHighlight(target) {
1849
+ const ref = useRef(null);
1850
+ const previous = useRef(null);
1851
+ const animation = useRef(null);
1852
+ const movement = useRef({
1853
+ start: 0,
1854
+ end: 0,
1855
+ velocity: 0
1856
+ });
1857
+ useLayoutEffect(() => {
1858
+ const node = ref.current;
1859
+ if (!node) return;
1860
+ const before = previous.current;
1861
+ previous.current = target;
1862
+ if (!target) {
1863
+ animation.current?.cancel();
1864
+ animation.current = null;
1865
+ return;
1866
+ }
1867
+ const from = animation.current ? getComputedStyle(node).transform : (() => {
1868
+ node.style.transform = before ?? target;
1869
+ return getComputedStyle(node).transform;
1870
+ })();
1871
+ const active = animation.current;
1872
+ const velocity = active?.playState === "running" && typeof active.currentTime === "number" ? springVelocity(active.currentTime, movement.current.start, movement.current.end, movement.current.velocity) : 0;
1873
+ animation.current?.cancel();
1874
+ animation.current = null;
1875
+ node.style.transform = target;
1876
+ const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
1877
+ if (!before || media?.matches || typeof node.animate !== "function") return;
1878
+ const start = translation(from);
1879
+ const end = translation(getComputedStyle(node).transform);
1880
+ if (Math.abs(end - start) < .01 && Math.abs(velocity) < .01) return;
1881
+ movement.current = {
1882
+ start,
1883
+ end,
1884
+ velocity
1885
+ };
1886
+ const running = node.animate(offsets.map(({ offset, time }) => ({
1887
+ offset,
1888
+ transform: `translateX(${offset === 1 ? end : springPosition(time, start, end, velocity)}px)`
1889
+ })), {
1890
+ duration: DURATION,
1891
+ easing: "linear"
1892
+ });
1893
+ animation.current = running;
1894
+ running.onfinish = () => {
1895
+ if (animation.current === running) animation.current = null;
1896
+ };
1897
+ }, [target]);
1898
+ useLayoutEffect(() => {
1899
+ const node = ref.current;
1900
+ if (!node) return;
1901
+ const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
1902
+ const settle = () => {
1903
+ animation.current?.cancel();
1904
+ animation.current = null;
1905
+ };
1906
+ const reduced = () => {
1907
+ if (media?.matches) settle();
1908
+ };
1909
+ media?.addEventListener("change", reduced);
1910
+ const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(settle) : null;
1911
+ observer?.observe(node);
1912
+ return () => {
1913
+ observer?.disconnect();
1914
+ media?.removeEventListener("change", reduced);
1915
+ settle();
1916
+ };
1917
+ }, []);
1918
+ return ref;
1919
+ }
1920
+ //#endregion
1921
+ //#region src/game/rounded-switch/RoundedSwitch.tsx
1922
+ function RoundedSwitch({ checked, defaultChecked = false, onCheckedChange, onText = "ON", offText = "OFF", name, form, disabled: disabledProp, required: requiredProp, className, onKeyDown, ...props }) {
1923
+ const field = useFieldContext();
1924
+ const disabled = disabledProp ?? field?.disabled ?? false;
1925
+ const required = requiredProp ?? field?.required ?? false;
1926
+ const [current, setCurrent] = useControllableState({
1927
+ value: checked,
1928
+ defaultValue: defaultChecked,
1929
+ onChange: onCheckedChange
1930
+ });
1931
+ const target = current ? "translateX(0%)" : "translateX(calc(100% - 1em))";
1932
+ const highlightRef = useSpringHighlight(target);
1933
+ return /* @__PURE__ */ jsxs(RadioGroup, {
1934
+ ...props,
1935
+ "aria-labelledby": props["aria-labelledby"] ?? (field && !field.group ? field.labelId : void 0),
1936
+ "aria-describedby": props["aria-describedby"] ?? field?.describedBy,
1937
+ "aria-invalid": field?.invalid || void 0,
1938
+ value: current,
1939
+ name,
1940
+ form,
1941
+ disabled,
1942
+ required,
1943
+ className: cn("ef-rounded-switch", className),
1944
+ onValueChange: (next) => {
1945
+ if (!disabled) setCurrent(next);
1946
+ },
1947
+ onKeyDown: (event) => {
1948
+ onKeyDown?.(event);
1949
+ roundedGroupBoundary(event);
1950
+ },
1951
+ children: [
1952
+ /* @__PURE__ */ jsx("div", {
1953
+ ref: highlightRef,
1954
+ "aria-hidden": "true",
1955
+ className: "ef-rounded-switch__highlight",
1956
+ style: { transform: target }
1957
+ }),
1958
+ /* @__PURE__ */ jsx(Radio.Root, {
1959
+ value: true,
1960
+ nativeButton: true,
1961
+ render: /* @__PURE__ */ jsx("button", { type: "button" }),
1962
+ "data-rounded-option": "",
1963
+ className: "ef-rounded-switch__option",
1964
+ children: /* @__PURE__ */ jsx("span", { children: onText })
1965
+ }),
1966
+ /* @__PURE__ */ jsx(Radio.Root, {
1967
+ value: false,
1968
+ nativeButton: true,
1969
+ render: /* @__PURE__ */ jsx("button", { type: "button" }),
1970
+ "data-rounded-option": "",
1971
+ className: "ef-rounded-switch__option",
1972
+ children: /* @__PURE__ */ jsx("span", { children: offText })
1973
+ })
1974
+ ]
1975
+ });
1976
+ }
1977
+ //#endregion
1978
+ //#region src/game/rounded-toggle/RoundedToggle.tsx
1979
+ function RoundedToggle({ options, value, defaultValue, onValueChange, label, name, form, required: requiredProp, disabled: disabledProp, className, onKeyDown, "aria-label": ariaLabel, ...props }) {
1980
+ const field = useFieldContext();
1981
+ const disabled = disabledProp ?? field?.disabled ?? false;
1982
+ const required = requiredProp ?? field?.required ?? false;
1983
+ const [current, setCurrent] = useControllableState({
1984
+ value,
1985
+ defaultValue,
1986
+ onChange: (next) => {
1987
+ if (next !== void 0) onValueChange?.(next);
1988
+ }
1989
+ });
1990
+ const index = options.findIndex((option) => option.value === current);
1991
+ const target = index >= 0 ? `translateX(${index * 100}%)` : null;
1992
+ const highlightRef = useSpringHighlight(target);
1993
+ return /* @__PURE__ */ jsxs(RadioGroup, {
1994
+ ...props,
1995
+ "aria-label": ariaLabel ?? label,
1996
+ "aria-labelledby": props["aria-labelledby"] ?? (field && !field.group ? field.labelId : void 0),
1997
+ "aria-describedby": props["aria-describedby"] ?? field?.describedBy,
1998
+ "aria-invalid": field?.invalid || void 0,
1999
+ value: index >= 0 ? current : null,
2000
+ name,
2001
+ form,
2002
+ disabled,
2003
+ required,
2004
+ className: cn("ef-rounded-toggle", className),
2005
+ onValueChange: (next) => {
2006
+ if (disabled || next === null || !options.some((option) => option.value === next && !option.disabled)) return;
2007
+ setCurrent(next);
2008
+ },
2009
+ onKeyDown: (event) => {
2010
+ onKeyDown?.(event);
2011
+ roundedGroupBoundary(event);
2012
+ },
2013
+ children: [/* @__PURE__ */ jsx("div", {
2014
+ ref: highlightRef,
2015
+ "aria-hidden": "true",
2016
+ hidden: index < 0,
2017
+ className: "ef-rounded-toggle__highlight",
2018
+ style: options.length > 0 ? {
2019
+ width: `${100 / options.length}%`,
2020
+ transform: target ?? void 0
2021
+ } : void 0
2022
+ }), options.map((option) => /* @__PURE__ */ jsxs(Radio.Root, {
2023
+ value: option.value,
2024
+ disabled: option.disabled,
2025
+ "aria-label": option["aria-label"],
2026
+ nativeButton: true,
2027
+ render: /* @__PURE__ */ jsx("button", { type: "button" }),
2028
+ "data-rounded-option": "",
2029
+ className: "ef-rounded-toggle__option",
2030
+ children: [option.icon && /* @__PURE__ */ jsx("span", {
2031
+ "aria-hidden": "true",
2032
+ className: "ef-rounded-toggle__icon",
2033
+ children: option.icon
2034
+ }), /* @__PURE__ */ jsx("span", {
2035
+ className: "ef-rounded-toggle__label",
2036
+ children: option.label
2037
+ })]
2038
+ }, option.value))]
2039
+ });
2040
+ }
2041
+ //#endregion
2042
+ export { AnimatedDialog, BannerAlert, EMPTY_ITEM_ICON_LAYERS, FullWidthAlert, ITEM_ICON_TRANSFORMS, ItemCard, ItemHoverCard, ItemHoverCardLayer, ItemIcon, ItemTooltipCard, RoundedButton, RoundedIconButton, RoundedSelect, RoundedSwitch, RoundedToggle, coerceItemIconTrans, itemIconTransformStyle, pickItemIconLayers };