@ntmats/hc-ui 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
1
5
  // src/index.tsx
2
- import { forwardRef, useId, useRef } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
- function Surface({ tone = "content", className, ...props }) {
5
- return /* @__PURE__ */ jsx("div", { ...props, className: ["hc-ui-surface", className].filter(Boolean).join(" "), "data-tone": tone });
6
+ import { cloneElement, createContext, forwardRef, Fragment, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
9
+ function Surface({ tone = "content", radius, className, ...props }) {
10
+ return /* @__PURE__ */ jsx("div", { ...props, className: ["hc-ui-surface", className].filter(Boolean).join(" "), "data-tone": tone, "data-radius": radius });
6
11
  }
7
12
  function SegmentedControl({
8
13
  label,
@@ -12,12 +17,24 @@ function SegmentedControl({
12
17
  density = "comfortable",
13
18
  orientation = "horizontal",
14
19
  hideLabels = false,
20
+ indicator = "static",
21
+ equalWidth = false,
22
+ optionWidth,
15
23
  className,
24
+ style,
16
25
  ...props
17
26
  }) {
18
27
  const buttons = useRef([]);
19
28
  const selectedIndex = options.findIndex((option) => option.value === value && !option.disabled);
20
29
  const tabStop = selectedIndex >= 0 ? selectedIndex : options.findIndex((option) => !option.disabled);
30
+ const slide = indicator === "slide";
31
+ const equal = slide || equalWidth || optionWidth !== void 0;
32
+ const knobIndex = options.findIndex((option) => option.value === value);
33
+ const layout = {
34
+ ...equal ? { "--hc-segmented-count": options.length } : {},
35
+ ...slide ? { "--hc-segmented-index": Math.max(knobIndex, 0) } : {},
36
+ ...optionWidth !== void 0 ? { "--hc-segmented-option-width": `${optionWidth}px` } : {}
37
+ };
21
38
  const move = (index, key) => {
22
39
  if (!["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp", "Home", "End"].includes(key)) return false;
23
40
  const direction = key === "ArrowRight" || key === "ArrowDown" ? 1 : -1;
@@ -36,7 +53,7 @@ function SegmentedControl({
36
53
  buttons.current[next]?.focus();
37
54
  return true;
38
55
  };
39
- return /* @__PURE__ */ jsx(
56
+ return /* @__PURE__ */ jsxs(
40
57
  "div",
41
58
  {
42
59
  ...props,
@@ -47,35 +64,41 @@ function SegmentedControl({
47
64
  "data-density": density,
48
65
  "data-orientation": orientation,
49
66
  "data-labels": hideLabels ? "hidden" : void 0,
50
- children: options.map((option, index) => {
51
- const selected = option.value === value;
52
- return /* @__PURE__ */ jsxs(
53
- "button",
54
- {
55
- ref: (element) => {
56
- buttons.current[index] = element;
57
- },
58
- type: "button",
59
- role: "radio",
60
- "aria-checked": selected,
61
- "aria-label": option.label,
62
- title: hideLabels ? option.label : void 0,
63
- disabled: option.disabled,
64
- tabIndex: !option.disabled && index === tabStop ? 0 : -1,
65
- className: "hc-ui-segmented__option",
66
- "data-selected": selected || void 0,
67
- onClick: () => !option.disabled && onChange(option.value),
68
- onKeyDown: (event) => {
69
- if (move(index, event.key)) event.preventDefault();
67
+ "data-indicator": slide ? "slide" : void 0,
68
+ "data-equal-width": equal || void 0,
69
+ style: { ...layout, ...style },
70
+ children: [
71
+ slide && knobIndex >= 0 && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__indicator", "aria-hidden": "true" }),
72
+ options.map((option, index) => {
73
+ const selected = option.value === value;
74
+ return /* @__PURE__ */ jsxs(
75
+ "button",
76
+ {
77
+ ref: (element) => {
78
+ buttons.current[index] = element;
79
+ },
80
+ type: "button",
81
+ role: "radio",
82
+ "aria-checked": selected,
83
+ "aria-label": option.label,
84
+ title: hideLabels ? option.label : void 0,
85
+ disabled: option.disabled,
86
+ tabIndex: !option.disabled && index === tabStop ? 0 : -1,
87
+ className: "hc-ui-segmented__option",
88
+ "data-selected": selected || void 0,
89
+ onClick: () => !option.disabled && onChange(option.value),
90
+ onKeyDown: (event) => {
91
+ if (move(index, event.key)) event.preventDefault();
92
+ },
93
+ children: [
94
+ option.icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__icon", "aria-hidden": "true", children: option.icon }),
95
+ !hideLabels && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__label", children: option.label })
96
+ ]
70
97
  },
71
- children: [
72
- option.icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__icon", "aria-hidden": "true", children: option.icon }),
73
- !hideLabels && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__label", children: option.label })
74
- ]
75
- },
76
- option.value
77
- );
78
- })
98
+ option.value
99
+ );
100
+ })
101
+ ]
79
102
  }
80
103
  );
81
104
  }
@@ -106,7 +129,7 @@ var Button = forwardRef(function Button2({ variant = "secondary", size = "md", i
106
129
  }
107
130
  );
108
131
  });
109
- var IconButton = forwardRef(function IconButton2({ label, icon, size = "md", tone = "primary", selected, type = "button", title, className, ...props }, ref) {
132
+ var IconButton = forwardRef(function IconButton2({ label, icon, size = "md", tone = "primary", variant = "muted", shape = "circle", selected, iconKey, type = "button", title, className, ...props }, ref) {
110
133
  return /* @__PURE__ */ jsx(
111
134
  "button",
112
135
  {
@@ -119,16 +142,63 @@ var IconButton = forwardRef(function IconButton2({ label, icon, size = "md", ton
119
142
  className: classes("hc-ui-icon-button", className),
120
143
  "data-size": size,
121
144
  "data-tone": tone,
122
- children: /* @__PURE__ */ jsx("span", { className: "hc-ui-icon-button__icon", "aria-hidden": "true", children: icon })
145
+ "data-variant": variant === "muted" ? void 0 : variant,
146
+ "data-shape": shape === "circle" ? void 0 : shape,
147
+ children: /* @__PURE__ */ jsx("span", { className: "hc-ui-icon-button__icon", "aria-hidden": "true", "data-swap": iconKey === void 0 ? void 0 : "", children: icon }, iconKey)
123
148
  }
124
149
  );
125
150
  });
126
- var Chip = forwardRef(function Chip2({ selected, icon, type = "button", className, children, ...props }, ref) {
127
- return /* @__PURE__ */ jsxs("button", { ...props, ref, type, "aria-pressed": selected, className: classes("hc-ui-chip", className), children: [
151
+ var glyph = (size, d) => /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", strokeWidth: 1.8, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) });
152
+ var CHEVRON_DOWN = "M6 9L12 15L18 9";
153
+ var XMARK = "M6.75827 17.2426L12.0009 12M17.2435 6.75736L12.0009 12M12.0009 12L6.75827 6.75736M12.0009 12L17.2435 17.2426";
154
+ var visible = (node) => node !== void 0 && node !== null && node !== false && node !== "";
155
+ var Chip = forwardRef(function Chip2({ selected, icon, size = "md", variant = "outline", applied = false, count, badge, chevron, onClear, clearLabel, type = "button", className, children, ...props }, ref) {
156
+ const popup = props["aria-haspopup"];
157
+ const withChevron = chevron ?? (popup !== void 0 && popup !== false && popup !== "false");
158
+ const look = {
159
+ "data-size": size === "md" ? void 0 : size,
160
+ "data-variant": variant === "outline" ? void 0 : variant,
161
+ "data-applied": applied || void 0
162
+ };
163
+ const content = /* @__PURE__ */ jsxs(Fragment2, { children: [
128
164
  icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__icon", "aria-hidden": "true", children: icon }),
129
- children
165
+ children,
166
+ visible(count) && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__count", children: count }),
167
+ visible(badge) && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__badge", children: badge }),
168
+ withChevron && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__chevron", "aria-hidden": "true", children: glyph(size === "xs" ? 12 : 14, CHEVRON_DOWN) })
169
+ ] });
170
+ if (!onClear) {
171
+ return /* @__PURE__ */ jsx("button", { ...props, ref, type, "aria-pressed": selected, className: classes("hc-ui-chip", className), ...look, children: content });
172
+ }
173
+ const { style, id, onClick, disabled, ...rest } = props;
174
+ return /* @__PURE__ */ jsxs("span", { className: classes("hc-ui-chip", className), style, ...look, "data-clearable": "", "data-selected": selected || void 0, "data-disabled": disabled || void 0, children: [
175
+ onClick ? /* @__PURE__ */ jsx("button", { ...rest, id, ref, type, disabled, "aria-pressed": selected, className: "hc-ui-chip__main", onClick, children: content }) : /* @__PURE__ */ jsx("span", { id, className: "hc-ui-chip__main", children: content }),
176
+ /* @__PURE__ */ jsx("button", { type: "button", className: "hc-ui-chip__clear", "aria-label": clearLabel, title: clearLabel, disabled, onClick: onClear, children: glyph(12, XMARK) })
130
177
  ] });
131
178
  });
179
+ var FilterBar = forwardRef(function FilterBar2({ label, start, end, footer, layout = "auto", density = "comfortable", sticky = false, className, children, ...props }, ref) {
180
+ return /* @__PURE__ */ jsxs(
181
+ "div",
182
+ {
183
+ ...props,
184
+ ref,
185
+ role: "group",
186
+ "aria-label": label,
187
+ className: classes("hc-ui-filter-bar", className),
188
+ "data-layout": layout,
189
+ "data-density": density === "comfortable" ? void 0 : density,
190
+ "data-sticky": sticky || void 0,
191
+ children: [
192
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-filter-bar__line", children: [
193
+ start && /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__start", children: start }),
194
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__row", children }),
195
+ end && /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__end", children: end })
196
+ ] }),
197
+ footer && /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__footer", children: footer })
198
+ ]
199
+ }
200
+ );
201
+ });
132
202
  var Field = forwardRef(function Field2({ label, hideLabel = false, density = "comfortable", icon, end, hint, error, id, className, disabled, "aria-describedby": describedBy, ...props }, ref) {
133
203
  const generated = useId();
134
204
  const inputId = id ?? generated;
@@ -169,12 +239,3081 @@ function Skeleton({ width, height, circle = false, className, style, ...props })
169
239
  }
170
240
  );
171
241
  }
242
+ var finite = (number) => Number.isFinite(number) ? number : 0;
243
+ var Meter = forwardRef(function Meter2({ value, max, label, valueText, decorative = false, thickness = 4, track = "muted", cap = "round", overflow = "clamp", tone = "auto", animated = false, className, ...props }, ref) {
244
+ const amount = Math.max(0, finite(value));
245
+ const limit = Math.max(0, finite(max));
246
+ const over = tone === "over" || tone === "auto" && amount > limit;
247
+ const scale = overflow === "mark" ? Math.max(limit, amount, 1) : limit;
248
+ const share = scale > 0 ? Math.min(100, amount / scale * 100) : amount > 0 ? 100 : 0;
249
+ const marker = overflow === "mark" && amount > limit ? limit / scale * 100 : null;
250
+ const semantics = decorative ? { "aria-hidden": true } : {
251
+ role: "meter",
252
+ "aria-label": label,
253
+ "aria-valuemin": 0,
254
+ "aria-valuemax": limit,
255
+ // ARIA keeps aria-valuenow inside the range; the text carries the real amount.
256
+ "aria-valuenow": Math.min(amount, limit),
257
+ "aria-valuetext": valueText ?? `${finite(value)} / ${finite(max)}`
258
+ };
259
+ return /* @__PURE__ */ jsxs(
260
+ "span",
261
+ {
262
+ ...props,
263
+ ...semantics,
264
+ ref,
265
+ className: classes("hc-ui-meter", className),
266
+ "data-thickness": thickness,
267
+ "data-track": track,
268
+ "data-cap": cap,
269
+ "data-tone": over ? "over" : void 0,
270
+ "data-animated": animated || void 0,
271
+ children: [
272
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-meter__fill", style: { width: `${share}%` } }),
273
+ marker !== null && /* @__PURE__ */ jsx("span", { className: "hc-ui-meter__limit", style: { left: `${marker}%` } })
274
+ ]
275
+ }
276
+ );
277
+ });
278
+ var checkboxIcon = (className, d) => /* @__PURE__ */ jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", strokeWidth: 2.4, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) });
279
+ var Checkbox = forwardRef(function Checkbox2({ size = "md", indeterminate = false, className, ...props }, ref) {
280
+ const inputRef = useRef(null);
281
+ useImperativeHandle(ref, () => inputRef.current);
282
+ useEffect(() => {
283
+ if (inputRef.current) inputRef.current.indeterminate = indeterminate;
284
+ }, [indeterminate]);
285
+ return /* @__PURE__ */ jsxs("span", { className: classes("hc-ui-checkbox", className), "data-size": size, children: [
286
+ /* @__PURE__ */ jsx("input", { ...props, ref: inputRef, type: "checkbox", className: "hc-ui-checkbox__input" }),
287
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-checkbox__box", "aria-hidden": "true", children: [
288
+ checkboxIcon("hc-ui-checkbox__mark", "M5 13L9 17L19 7"),
289
+ checkboxIcon("hc-ui-checkbox__mixed", "M6 12H18")
290
+ ] })
291
+ ] });
292
+ });
293
+ var useIsomorphicLayoutEffect = typeof document === "undefined" ? useEffect : useLayoutEffect;
294
+ var scrollLocks = 0;
295
+ var restoreScroll = () => {
296
+ };
297
+ function lockScroll() {
298
+ if (scrollLocks++ === 0) {
299
+ const root = document.documentElement;
300
+ const { overflow, scrollbarGutter } = root.style;
301
+ const hasScrollbar = window.innerWidth > root.clientWidth;
302
+ root.style.overflow = "hidden";
303
+ if (hasScrollbar) root.style.scrollbarGutter = "stable";
304
+ restoreScroll = () => {
305
+ root.style.overflow = overflow;
306
+ root.style.scrollbarGutter = scrollbarGutter;
307
+ };
308
+ }
309
+ let released = false;
310
+ return () => {
311
+ if (released) return;
312
+ released = true;
313
+ if (--scrollLocks === 0) restoreScroll();
314
+ };
315
+ }
316
+ function DialogMount({ onMount }) {
317
+ useIsomorphicLayoutEffect(onMount, []);
318
+ return null;
319
+ }
320
+ var closeGlyph = /* @__PURE__ */ jsx("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", strokeWidth: 1.8, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M6.75827 17.2426L12.0009 12M17.2435 6.75736L12.0009 12M12.0009 12L6.75827 6.75736M12.0009 12L17.2435 17.2426", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) });
321
+ var outside = (element, x, y) => {
322
+ const box = element.getBoundingClientRect();
323
+ return x < box.left || x > box.right || y < box.top || y > box.bottom;
324
+ };
325
+ var Dialog = forwardRef(function Dialog2({
326
+ open,
327
+ onClose,
328
+ title,
329
+ description,
330
+ children,
331
+ actions,
332
+ closeLabel,
333
+ closeIcon,
334
+ size = "md",
335
+ dismissible = true,
336
+ className,
337
+ onClick,
338
+ onPointerDown,
339
+ "aria-describedby": describedBy,
340
+ ...props
341
+ }, ref) {
342
+ const dialogRef = useRef(null);
343
+ const setRef = useCallback((node) => {
344
+ dialogRef.current = node;
345
+ if (typeof ref === "function") ref(node);
346
+ else if (ref) ref.current = node;
347
+ }, [ref]);
348
+ const id = useId();
349
+ const titleId = `${id}-title`;
350
+ const descriptionId = description ? `${id}-description` : void 0;
351
+ const [mounted, setMounted] = useState(open);
352
+ const [closing, setClosing] = useState(false);
353
+ if (open && (!mounted || closing)) {
354
+ setMounted(true);
355
+ setClosing(false);
356
+ }
357
+ if (!open && mounted && !closing) setClosing(true);
358
+ const latest = useRef({ open, onClose, dismissible, closing });
359
+ useIsomorphicLayoutEffect(() => {
360
+ latest.current = { open, onClose, dismissible, closing };
361
+ });
362
+ const keepFocus = useRef(null);
363
+ const handleMount = useCallback(() => {
364
+ const dialog = document.getElementById(titleId)?.closest("dialog");
365
+ if (!dialog) return () => {
366
+ };
367
+ const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
368
+ if (!dialog.open) dialog.showModal();
369
+ const kept = keepFocus.current;
370
+ keepFocus.current = null;
371
+ if (kept instanceof HTMLElement && dialog.contains(kept)) kept.focus();
372
+ const unlock = lockScroll();
373
+ return () => {
374
+ unlock();
375
+ keepFocus.current = dialog.contains(document.activeElement) ? document.activeElement : null;
376
+ if (dialog.open) dialog.close();
377
+ queueMicrotask(() => {
378
+ const active = document.activeElement;
379
+ const lost = !active || active === document.body || dialog.contains(active);
380
+ if (lost && !dialog.open && trigger?.isConnected) trigger.focus({ preventScroll: true });
381
+ });
382
+ };
383
+ }, [titleId]);
384
+ useEffect(() => {
385
+ const dialog = dialogRef.current;
386
+ if (!closing || !dialog) return;
387
+ let done = false;
388
+ const finish = () => {
389
+ if (done) return;
390
+ done = true;
391
+ setMounted(false);
392
+ setClosing(false);
393
+ };
394
+ const animations = typeof dialog.getAnimations === "function" ? dialog.getAnimations({ subtree: true }).filter((animation) => {
395
+ const effect = animation.effect;
396
+ return effect?.target === dialog && effect.getComputedTiming().endTime !== Infinity;
397
+ }) : [];
398
+ if (animations.length) Promise.all(animations.map((animation) => animation.finished)).then(finish, finish);
399
+ else finish();
400
+ return () => {
401
+ done = true;
402
+ };
403
+ }, [closing]);
404
+ const ownEvent = (event) => event.target === event.currentTarget;
405
+ const handleCancel = (event) => {
406
+ if (!ownEvent(event)) return;
407
+ event.preventDefault();
408
+ if (latest.current.dismissible && !latest.current.closing) latest.current.onClose();
409
+ };
410
+ const handleNativeClose = (event) => {
411
+ const dialog = event.currentTarget;
412
+ if (!ownEvent(event) || dialog.open || !latest.current.open || latest.current.closing) return;
413
+ if (latest.current.dismissible) latest.current.onClose();
414
+ window.setTimeout(() => {
415
+ if (latest.current.open && !latest.current.closing && dialog.isConnected && !dialog.open) dialog.showModal();
416
+ }, 0);
417
+ };
418
+ const pressedBackdrop = useRef(false);
419
+ const handlePointerDown = (event) => {
420
+ onPointerDown?.(event);
421
+ pressedBackdrop.current = ownEvent(event) && outside(event.currentTarget, event.clientX, event.clientY);
422
+ };
423
+ const handleClick = (event) => {
424
+ onClick?.(event);
425
+ const fromBackdrop = pressedBackdrop.current && ownEvent(event) && outside(event.currentTarget, event.clientX, event.clientY);
426
+ pressedBackdrop.current = false;
427
+ if (fromBackdrop && latest.current.dismissible && !latest.current.closing) latest.current.onClose();
428
+ };
429
+ if (!mounted) return null;
430
+ const hasBody = Boolean(description) || children !== void 0 && children !== null && children !== false;
431
+ return /* @__PURE__ */ jsxs(
432
+ "dialog",
433
+ {
434
+ ...props,
435
+ ref: setRef,
436
+ className: classes("hc-ui-dialog", className),
437
+ "data-size": size,
438
+ "data-closing": closing || void 0,
439
+ "aria-labelledby": titleId,
440
+ "aria-describedby": classes(descriptionId, describedBy) || void 0,
441
+ onCancel: handleCancel,
442
+ onClose: handleNativeClose,
443
+ onPointerDown: handlePointerDown,
444
+ onClick: handleClick,
445
+ children: [
446
+ /* @__PURE__ */ jsx(DialogMount, { onMount: handleMount }),
447
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-dialog__header", children: [
448
+ /* @__PURE__ */ jsx("h2", { className: "hc-ui-dialog__title", id: titleId, children: title }),
449
+ closeLabel && /* @__PURE__ */ jsx(IconButton, { className: "hc-ui-dialog__close", label: closeLabel, icon: closeIcon ?? closeGlyph, size: "sm", tone: "secondary", onClick: () => latest.current.onClose() })
450
+ ] }),
451
+ hasBody && /* @__PURE__ */ jsxs("div", { className: "hc-ui-dialog__body", children: [
452
+ description && /* @__PURE__ */ jsx("p", { className: "hc-ui-dialog__description", id: descriptionId, children: description }),
453
+ children
454
+ ] }),
455
+ actions && /* @__PURE__ */ jsx("div", { className: "hc-ui-dialog__actions", children: actions })
456
+ ]
457
+ }
458
+ );
459
+ });
460
+ var SHEET_EASE = "cubic-bezier(0.32, 0.72, 0, 1)";
461
+ var NO_DRAG = 'input, textarea, select, [contenteditable="true"], [data-hc-sheet-nodrag]';
462
+ function useSheetDrag(sheet, latest) {
463
+ useEffect(() => {
464
+ const el = sheet;
465
+ if (!el) return;
466
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)");
467
+ let startX = 0;
468
+ let startY = 0;
469
+ let lastY = 0;
470
+ let lastT = 0;
471
+ let velocity = 0;
472
+ let tracking = false;
473
+ let dragging = false;
474
+ let inHandle = false;
475
+ let scroller = null;
476
+ const timers = /* @__PURE__ */ new Set();
477
+ const later = (run, ms) => {
478
+ const id = window.setTimeout(() => {
479
+ timers.delete(id);
480
+ run();
481
+ }, ms);
482
+ timers.add(id);
483
+ };
484
+ const expanded = () => el.hasAttribute("data-expanded");
485
+ const findScroller = (node) => {
486
+ while (node && node !== el.parentElement) {
487
+ const overflowY = getComputedStyle(node).overflowY;
488
+ if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight + 1) return node;
489
+ if (node === el) break;
490
+ node = node.parentElement;
491
+ }
492
+ return null;
493
+ };
494
+ const settle = () => {
495
+ el.style.transition = reduce.matches ? "none" : `translate 320ms ${SHEET_EASE}`;
496
+ el.style.translate = "";
497
+ later(() => {
498
+ el.style.transition = "";
499
+ }, 340);
500
+ };
501
+ const onStart = (event) => {
502
+ if (event.touches.length !== 1 || getComputedStyle(el).getPropertyValue("--hc-sheet-drag").trim() !== "on") return;
503
+ const touch = event.touches[0];
504
+ const target = event.target;
505
+ if (target === el && outside(el, touch.clientX, touch.clientY)) return;
506
+ if (target.closest?.("dialog") !== el || target.closest(NO_DRAG)) return;
507
+ startX = touch.clientX;
508
+ startY = lastY = touch.clientY;
509
+ lastT = performance.now();
510
+ velocity = 0;
511
+ dragging = false;
512
+ tracking = true;
513
+ inHandle = Boolean(target.closest?.("[data-hc-sheet-handle]")) || touch.clientY - el.getBoundingClientRect().top < 64;
514
+ scroller = inHandle ? null : findScroller(target);
515
+ };
516
+ const onMove = (event) => {
517
+ if (!tracking) return;
518
+ const touch = event.touches[0];
519
+ const dy = touch.clientY - startY;
520
+ const dx = touch.clientX - startX;
521
+ if (!dragging) {
522
+ if (Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) {
523
+ tracking = false;
524
+ return;
525
+ }
526
+ if (dy < -4 && !(inHandle && !expanded())) {
527
+ tracking = false;
528
+ return;
529
+ }
530
+ if (Math.abs(dy) < 8) return;
531
+ if (scroller && scroller.scrollTop > 0) {
532
+ tracking = false;
533
+ return;
534
+ }
535
+ dragging = true;
536
+ startY = touch.clientY;
537
+ el.style.transition = "none";
538
+ }
539
+ if (event.cancelable) event.preventDefault();
540
+ const now = performance.now();
541
+ velocity = (touch.clientY - lastY) / Math.max(1, now - lastT);
542
+ lastY = touch.clientY;
543
+ lastT = now;
544
+ const offset = touch.clientY - startY;
545
+ el.style.translate = `0 ${offset > 0 ? offset : offset / 6}px`;
546
+ };
547
+ const onEnd = () => {
548
+ if (!tracking) return;
549
+ tracking = false;
550
+ if (!dragging) return;
551
+ dragging = false;
552
+ const dy = lastY - startY;
553
+ const height = el.getBoundingClientRect().height || 1;
554
+ if (dy < -40 || velocity < -0.5) {
555
+ el.setAttribute("data-expanded", "");
556
+ settle();
557
+ return;
558
+ }
559
+ const goingDown = dy > height * 0.2 || velocity > 0.5 && dy > 20;
560
+ if (goingDown && expanded()) {
561
+ el.removeAttribute("data-expanded");
562
+ settle();
563
+ return;
564
+ }
565
+ if (!goingDown || !latest.current.dismissible) {
566
+ settle();
567
+ return;
568
+ }
569
+ el.style.transition = reduce.matches ? "none" : `translate 220ms ${SHEET_EASE}`;
570
+ el.style.translate = "0 105%";
571
+ el.setAttribute("data-dismissing", "");
572
+ later(() => latest.current.onClose(), reduce.matches ? 0 : 200);
573
+ later(() => {
574
+ if (!el.isConnected || el.hasAttribute("data-closing")) return;
575
+ el.removeAttribute("data-dismissing");
576
+ el.style.transition = reduce.matches ? "none" : `translate 320ms ${SHEET_EASE}`;
577
+ el.style.translate = "";
578
+ }, 900);
579
+ };
580
+ el.addEventListener("touchstart", onStart, { passive: true });
581
+ el.addEventListener("touchmove", onMove, { passive: false });
582
+ el.addEventListener("touchend", onEnd);
583
+ el.addEventListener("touchcancel", onEnd);
584
+ return () => {
585
+ el.removeEventListener("touchstart", onStart);
586
+ el.removeEventListener("touchmove", onMove);
587
+ el.removeEventListener("touchend", onEnd);
588
+ el.removeEventListener("touchcancel", onEnd);
589
+ timers.forEach((id) => window.clearTimeout(id));
590
+ };
591
+ }, [sheet, latest]);
592
+ }
593
+ var Sheet = forwardRef(function Sheet2({ presentation = "auto", width = 440, footer, dismissible = true, onClose, className, style, ...props }, ref) {
594
+ const [node, setNode] = useState(null);
595
+ const setRef = useCallback((element) => {
596
+ setNode(element);
597
+ setRefs(element, ref);
598
+ }, [ref]);
599
+ const latest = useRef({ onClose, dismissible });
600
+ useIsomorphicLayoutEffect(() => {
601
+ latest.current = { onClose, dismissible };
602
+ });
603
+ useSheetDrag(node, latest);
604
+ const sized = width === 440 ? style : { ...style, "--hc-sheet-width": `${width}px` };
605
+ return /* @__PURE__ */ jsx(
606
+ Dialog,
607
+ {
608
+ ...props,
609
+ ref: setRef,
610
+ size: "md",
611
+ actions: footer,
612
+ dismissible,
613
+ onClose,
614
+ className: classes("hc-ui-sheet", className),
615
+ style: sized,
616
+ "data-presentation": presentation
617
+ }
618
+ );
619
+ });
620
+ function useRovingRadios(options, value) {
621
+ const buttons = useRef([]);
622
+ const [focusIndex, setFocusIndex] = useState(null);
623
+ const chosen = options.findIndex((option) => option.value === value && !option.disabled);
624
+ const fallback = chosen >= 0 ? chosen : options.findIndex((option) => !option.disabled);
625
+ const tabStop = focusIndex !== null && options[focusIndex] && !options[focusIndex].disabled ? focusIndex : fallback;
626
+ const onKeyDown = (index) => (event) => {
627
+ const enabled = options.map((option, at) => option.disabled ? -1 : at).filter((at) => at >= 0);
628
+ if (!enabled.length) return;
629
+ const position = enabled.indexOf(index);
630
+ let next;
631
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") next = enabled[(position + 1) % enabled.length];
632
+ else if (event.key === "ArrowUp" || event.key === "ArrowLeft") next = enabled[(position - 1 + enabled.length) % enabled.length];
633
+ else if (event.key === "Home") next = enabled[0];
634
+ else if (event.key === "End") next = enabled[enabled.length - 1];
635
+ else return;
636
+ event.preventDefault();
637
+ setFocusIndex(next);
638
+ buttons.current[next]?.focus();
639
+ };
640
+ const item = (index) => ({
641
+ ref: (element) => {
642
+ buttons.current[index] = element;
643
+ },
644
+ tabIndex: index === tabStop ? 0 : -1,
645
+ onKeyDown: onKeyDown(index),
646
+ onFocus: () => setFocusIndex(index)
647
+ });
648
+ return item;
649
+ }
650
+ function SheetOptions({ label, options, value, onChange, variant = "card", className, ...props }) {
651
+ const item = useRovingRadios(options, value);
652
+ const list = variant === "list";
653
+ return /* @__PURE__ */ jsx("div", { ...props, className: classes("hc-ui-sheet-options", className), role: "radiogroup", "aria-label": label, "data-variant": list ? "list" : void 0, children: options.map((option, index) => {
654
+ const on = option.value === value;
655
+ return /* @__PURE__ */ jsxs(
656
+ "button",
657
+ {
658
+ ...item(index),
659
+ type: "button",
660
+ role: "radio",
661
+ "aria-checked": on,
662
+ disabled: option.disabled,
663
+ className: "hc-ui-sheet-options__row",
664
+ "data-selected": on || void 0,
665
+ onClick: () => onChange(option.value),
666
+ children: [
667
+ option.icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__icon", "aria-hidden": "true", children: option.icon }),
668
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-sheet-options__text", children: [
669
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__label", children: option.label }),
670
+ option.hint && /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__hint", children: option.hint })
671
+ ] }),
672
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__check", "aria-hidden": "true", children: on && /* @__PURE__ */ jsx("svg", { width: list ? 20 : 18, height: list ? 20 : 18, viewBox: "0 0 24 24", fill: "none", strokeWidth: list ? 1.8 : 2.4, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M5 13L9 17L19 7", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) }) })
673
+ ]
674
+ },
675
+ option.value
676
+ );
677
+ }) });
678
+ }
679
+ function SheetChips({ label, options, value, onChange, className, ...props }) {
680
+ const item = useRovingRadios(options, value);
681
+ return /* @__PURE__ */ jsx("div", { ...props, className: classes("hc-ui-sheet-chips", className), role: "radiogroup", "aria-label": label, children: options.map((option, index) => /* @__PURE__ */ jsx(
682
+ "button",
683
+ {
684
+ ...item(index),
685
+ type: "button",
686
+ role: "radio",
687
+ "aria-checked": option.value === value,
688
+ disabled: option.disabled,
689
+ className: "hc-ui-chip",
690
+ onClick: () => onChange(option.value),
691
+ children: option.label
692
+ },
693
+ option.value
694
+ )) });
695
+ }
696
+ var GAP = 4;
697
+ function boundaryOf(anchor, clip) {
698
+ const box = { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight };
699
+ if (!clip) return box;
700
+ for (let node = anchor.parentElement; node && node !== document.documentElement; node = node.parentElement) {
701
+ const style = getComputedStyle(node);
702
+ if (style.overflowX === "visible" && style.overflowY === "visible") continue;
703
+ const rect = node.getBoundingClientRect();
704
+ box.top = Math.max(box.top, rect.top);
705
+ box.left = Math.max(box.left, rect.left);
706
+ box.right = Math.min(box.right, rect.right);
707
+ box.bottom = Math.min(box.bottom, rect.bottom);
708
+ }
709
+ return box;
710
+ }
711
+ function placePopup(trigger, size, boundary, side, align) {
712
+ const room = { bottom: boundary.bottom - trigger.bottom - GAP, top: trigger.top - boundary.top - GAP };
713
+ const other = side === "bottom" ? "top" : "bottom";
714
+ const finalSide = size.height > room[side] && room[other] > room[side] ? other : side;
715
+ const fits = (edge) => edge === "end" ? trigger.right - size.width >= boundary.left : trigger.left + size.width <= boundary.right;
716
+ const finalAlign = !fits(align) && fits(align === "end" ? "start" : "end") ? align === "end" ? "start" : "end" : align;
717
+ return { side: finalSide, align: finalAlign };
718
+ }
719
+ var layers = [];
720
+ function themeOf(element) {
721
+ const scoped = element?.closest("[data-theme]")?.getAttribute("data-theme");
722
+ if (scoped === "light" || scoped === "dark") return scoped;
723
+ const classed = element?.closest(".dark, .light");
724
+ return classed ? classed.classList.contains("dark") ? "dark" : "light" : void 0;
725
+ }
726
+ var setRefs = (node, ...refs) => {
727
+ for (const ref of refs) {
728
+ if (typeof ref === "function") ref(node);
729
+ else if (ref) ref.current = node;
730
+ }
731
+ };
732
+ function usePopup({ kind, trigger, open: openProp, defaultOpen = false, onOpenChange, side = "bottom", align = "end", portal = false, theme, contentId, onShown }) {
733
+ const [uncontrolled, setUncontrolled] = useState(defaultOpen);
734
+ const open = openProp ?? uncontrolled;
735
+ const generated = useId();
736
+ const id = contentId ?? `${generated}-popup`;
737
+ const triggerRef = useRef(null);
738
+ const anchorRef = useRef(null);
739
+ const contentRef = useRef(null);
740
+ const focusAfter = useRef("none");
741
+ const reason = useRef("first");
742
+ const [placement, setPlacement] = useState({ side, align });
743
+ const [container, setContainer] = useState(null);
744
+ const latest = useRef({ open, onOpenChange, onShown });
745
+ useIsomorphicLayoutEffect(() => {
746
+ latest.current = { open, onOpenChange, onShown };
747
+ });
748
+ const setOpen = useCallback((next, after = "none") => {
749
+ if (next === latest.current.open) return;
750
+ if (!next) focusAfter.current = after;
751
+ if (openProp === void 0) setUncontrolled(next);
752
+ latest.current.onOpenChange?.(next);
753
+ }, [openProp]);
754
+ const close = useCallback((after = "trigger") => setOpen(false, after), [setOpen]);
755
+ const show = (why) => {
756
+ reason.current = why;
757
+ setOpen(true);
758
+ };
759
+ useIsomorphicLayoutEffect(() => {
760
+ if (!open || !portal) {
761
+ setContainer(null);
762
+ return;
763
+ }
764
+ const element = portal instanceof HTMLElement ? portal : triggerRef.current?.closest("dialog[open]") ?? document.body;
765
+ setContainer({ element, theme: theme ?? themeOf(triggerRef.current) });
766
+ }, [open, portal, theme]);
767
+ const place = useCallback(() => {
768
+ const content = contentRef.current;
769
+ const anchor = anchorRef.current;
770
+ if (!content || !anchor) return;
771
+ const triggerBox = (triggerRef.current ?? anchor).getBoundingClientRect();
772
+ const size = { width: content.offsetWidth, height: content.offsetHeight };
773
+ if (!portal) {
774
+ setPlacement(placePopup(anchor.getBoundingClientRect(), size, boundaryOf(anchor, true), side, align));
775
+ return;
776
+ }
777
+ const next = placePopup(triggerBox, size, boundaryOf(anchor, false), side, align);
778
+ const style = content.style;
779
+ style.top = "0px";
780
+ style.left = "0px";
781
+ const origin = content.getBoundingClientRect();
782
+ const top = next.side === "bottom" ? triggerBox.bottom + GAP : triggerBox.top - GAP - size.height;
783
+ const left = next.align === "end" ? triggerBox.right - size.width : triggerBox.left;
784
+ style.top = `${top - origin.top}px`;
785
+ style.left = `${left - origin.left}px`;
786
+ setPlacement({ ...next, top: top - origin.top, left: left - origin.left });
787
+ }, [portal, side, align]);
788
+ const ready = open && (!portal || container !== null);
789
+ useIsomorphicLayoutEffect(() => {
790
+ if (!ready) return;
791
+ place();
792
+ const content = contentRef.current;
793
+ if (content) latest.current.onShown?.(content, reason.current);
794
+ }, [ready]);
795
+ useIsomorphicLayoutEffect(() => {
796
+ if (!ready) setPlacement({ side, align });
797
+ }, [ready, side, align]);
798
+ const handlers = useRef({ close, place });
799
+ useIsomorphicLayoutEffect(() => {
800
+ handlers.current = { close, place };
801
+ });
802
+ useEffect(() => {
803
+ if (!ready) return;
804
+ const layer = { content: () => contentRef.current };
805
+ layers.push(layer);
806
+ const isTop = () => layers[layers.length - 1] === layer;
807
+ const onKey = (event) => {
808
+ if (event.key !== "Escape" || !isTop() || event.defaultPrevented) return;
809
+ event.preventDefault();
810
+ event.stopPropagation();
811
+ handlers.current.close("trigger");
812
+ };
813
+ const onPointer = (event) => {
814
+ const target = event.target;
815
+ if (anchorRef.current?.contains(target) || contentRef.current?.contains(target)) return;
816
+ const index = layers.indexOf(layer);
817
+ if (layers.slice(index + 1).some((other) => other.content()?.contains(target))) return;
818
+ handlers.current.close("none");
819
+ };
820
+ const onMove = () => handlers.current.place();
821
+ document.addEventListener("keydown", onKey, true);
822
+ document.addEventListener("pointerdown", onPointer, true);
823
+ window.addEventListener("resize", onMove);
824
+ if (portal) window.addEventListener("scroll", onMove, true);
825
+ return () => {
826
+ layers.splice(layers.indexOf(layer), 1);
827
+ document.removeEventListener("keydown", onKey, true);
828
+ document.removeEventListener("pointerdown", onPointer, true);
829
+ window.removeEventListener("resize", onMove);
830
+ window.removeEventListener("scroll", onMove, true);
831
+ };
832
+ }, [ready, portal]);
833
+ useIsomorphicLayoutEffect(() => {
834
+ if (open) return;
835
+ const after = focusAfter.current;
836
+ focusAfter.current = "none";
837
+ const active = document.activeElement;
838
+ const lost = !active || active === document.body;
839
+ if (after === "trigger" || after === "if-lost" && lost) triggerRef.current?.focus({ preventScroll: true });
840
+ }, [open]);
841
+ const triggerProps = {
842
+ ref: (node) => {
843
+ triggerRef.current = node;
844
+ },
845
+ "aria-haspopup": kind,
846
+ "aria-expanded": open,
847
+ "aria-controls": open ? id : void 0,
848
+ onClick: (event) => {
849
+ if (event.defaultPrevented) return;
850
+ if (open) close("none");
851
+ else show(event.detail === 0 ? "first" : "pointer");
852
+ },
853
+ onKeyDown: (event) => {
854
+ if (event.defaultPrevented || kind !== "menu") return;
855
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
856
+ event.preventDefault();
857
+ show(event.key === "ArrowUp" ? "last" : "first");
858
+ }
859
+ }
860
+ };
861
+ let triggerNode;
862
+ if (typeof trigger === "function") triggerNode = trigger(triggerProps, { open });
863
+ else {
864
+ const own = trigger.props;
865
+ const elementRef = trigger.ref;
866
+ triggerNode = cloneElement(trigger, {
867
+ ...triggerProps,
868
+ ref: (node) => setRefs(node, triggerProps.ref, elementRef),
869
+ onClick: (event) => {
870
+ own.onClick?.(event);
871
+ triggerProps.onClick(event);
872
+ },
873
+ onKeyDown: (event) => {
874
+ own.onKeyDown?.(event);
875
+ triggerProps.onKeyDown(event);
876
+ }
877
+ });
878
+ }
879
+ const contentAttributes = {
880
+ id,
881
+ "data-side": placement.side,
882
+ "data-align": placement.align,
883
+ "data-portal": portal ? "" : void 0,
884
+ style: portal ? { top: placement.top ?? 0, left: placement.left ?? 0, right: "auto", bottom: "auto" } : void 0
885
+ };
886
+ const wrap = (content) => {
887
+ if (!open) return null;
888
+ if (!portal) return content;
889
+ if (!container) return null;
890
+ return createPortal(/* @__PURE__ */ jsx("div", { className: "hc-ui-portal", "data-hc-ds": "", "data-theme": container.theme, children: content }), container.element);
891
+ };
892
+ return { open, close, triggerNode, anchorRef, contentRef, contentAttributes, wrap, triggerRef };
893
+ }
894
+ var mergeStyle = (base, own) => base || own ? { ...own, ...base } : void 0;
895
+ var MenuContext = createContext(null);
896
+ var RadioContext = createContext(null);
897
+ var ITEM_SELECTOR = '[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"]';
898
+ var itemsOf = (menu) => [...menu.querySelectorAll(ITEM_SELECTOR)].filter((item) => !item.disabled && item.closest('[role="menu"]') === menu);
899
+ var itemText = (item) => (item.querySelector(".hc-ui-menu__text")?.firstChild?.textContent ?? item.textContent ?? "").trim().toLocaleLowerCase();
900
+ var Menu = forwardRef(function Menu2({ label, trigger, children, open, defaultOpen, onOpenChange, side, align, portal, theme, anchorClassName, className, style, id, onKeyDown, ...props }, ref) {
901
+ const typed = useRef({ text: "", at: 0 });
902
+ const popup = usePopup({
903
+ kind: "menu",
904
+ trigger,
905
+ open,
906
+ defaultOpen,
907
+ onOpenChange,
908
+ side,
909
+ align,
910
+ portal,
911
+ theme,
912
+ contentId: id,
913
+ onShown: (menu, why) => {
914
+ typed.current = { text: "", at: 0 };
915
+ const items = itemsOf(menu);
916
+ const selected = items.find((item) => item.getAttribute("role") === "menuitemradio" && item.getAttribute("aria-checked") === "true");
917
+ (why === "last" ? items[items.length - 1] : selected ?? items[0])?.focus({ preventScroll: true });
918
+ }
919
+ });
920
+ const { close } = popup;
921
+ const context = useMemo(() => ({ close }), [close]);
922
+ useIsomorphicLayoutEffect(() => {
923
+ const menu = popup.contentRef.current;
924
+ const active = document.activeElement;
925
+ const lost = !active || active === document.body || menu?.contains(active) && active.disabled;
926
+ if (popup.open && menu && lost) itemsOf(menu)[0]?.focus({ preventScroll: true });
927
+ });
928
+ const handleKeyDown = (event) => {
929
+ onKeyDown?.(event);
930
+ if (event.defaultPrevented) return;
931
+ const menu = event.currentTarget;
932
+ const items = itemsOf(menu);
933
+ const current = items.indexOf(document.activeElement);
934
+ const focus = (index) => items[(index + items.length) % items.length]?.focus();
935
+ switch (event.key) {
936
+ case "ArrowDown":
937
+ event.preventDefault();
938
+ focus(current + 1);
939
+ return;
940
+ case "ArrowUp":
941
+ event.preventDefault();
942
+ focus(current < 0 ? -1 : current - 1);
943
+ return;
944
+ case "Home":
945
+ event.preventDefault();
946
+ focus(0);
947
+ return;
948
+ case "End":
949
+ event.preventDefault();
950
+ focus(-1);
951
+ return;
952
+ case "Tab":
953
+ popup.triggerRef.current?.focus({ preventScroll: true });
954
+ close("none");
955
+ return;
956
+ }
957
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey || !items.length) return;
958
+ const now = Date.now();
959
+ const typing = typed.current.text !== "" && now - typed.current.at <= 500;
960
+ if (event.key === " " && !typing) return;
961
+ event.preventDefault();
962
+ const key = event.key.toLocaleLowerCase();
963
+ typed.current.text = typing ? typed.current.text + key : key;
964
+ typed.current.at = now;
965
+ const query = [...typed.current.text].every((letter) => letter === key) ? key : typed.current.text;
966
+ const start = query.length === 1 ? current + 1 : Math.max(current, 0);
967
+ for (let step = 0; step < items.length; step += 1) {
968
+ const item = items[(start + step) % items.length];
969
+ if (itemText(item).startsWith(query)) {
970
+ item.focus();
971
+ break;
972
+ }
973
+ }
974
+ };
975
+ return /* @__PURE__ */ jsxs("div", { className: classes("hc-ui-popup-anchor", anchorClassName), ref: popup.anchorRef, children: [
976
+ popup.triggerNode,
977
+ popup.wrap(
978
+ /* @__PURE__ */ jsx(MenuContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
979
+ "div",
980
+ {
981
+ ...props,
982
+ ...popup.contentAttributes,
983
+ style: mergeStyle(popup.contentAttributes.style, style),
984
+ ref: (node) => setRefs(node, popup.contentRef, ref),
985
+ role: "menu",
986
+ "aria-label": label,
987
+ tabIndex: -1,
988
+ className: classes("hc-ui-menu", className),
989
+ onKeyDown: handleKeyDown,
990
+ children
991
+ }
992
+ ) })
993
+ )
994
+ ] });
995
+ });
996
+ function useMenu(name) {
997
+ const menu = useContext(MenuContext);
998
+ if (!menu) throw new Error(`${name} must be inside a Menu`);
999
+ return menu;
1000
+ }
1001
+ var checkGlyph = /* @__PURE__ */ jsx("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", strokeWidth: 1.8, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M5 13L9 17L19 7", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) });
1002
+ var MenuRow = forwardRef(function MenuRow2({ icon, hint, danger, end, role, onChoose, className, children, type = "button", onClick, ...props }, ref) {
1003
+ return /* @__PURE__ */ jsxs(
1004
+ "button",
1005
+ {
1006
+ ...props,
1007
+ ref,
1008
+ type,
1009
+ role,
1010
+ tabIndex: -1,
1011
+ className: classes("hc-ui-menu__item", className),
1012
+ "data-danger": danger || void 0,
1013
+ onClick: (event) => {
1014
+ onClick?.(event);
1015
+ if (!event.defaultPrevented) onChoose(event);
1016
+ },
1017
+ children: [
1018
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-menu__text", children: [
1019
+ children,
1020
+ hint && /* @__PURE__ */ jsx("small", { className: "hc-ui-menu__hint", children: hint })
1021
+ ] }),
1022
+ end,
1023
+ icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-menu__icon", "aria-hidden": "true", children: icon })
1024
+ ]
1025
+ }
1026
+ );
1027
+ });
1028
+ var MenuItem = forwardRef(function MenuItem2({ onSelect, ...props }, ref) {
1029
+ const menu = useMenu("MenuItem");
1030
+ return /* @__PURE__ */ jsx(MenuRow, { ...props, ref, role: "menuitem", onChoose: (event) => {
1031
+ onSelect?.(event);
1032
+ if (!event.defaultPrevented) menu.close("if-lost");
1033
+ } });
1034
+ });
1035
+ function MenuRadioGroup({ value, onValueChange, label, className, children, ...props }) {
1036
+ const context = useMemo(() => ({ value, onValueChange }), [value, onValueChange]);
1037
+ return /* @__PURE__ */ jsx(RadioContext.Provider, { value: context, children: /* @__PURE__ */ jsx("div", { ...props, role: "group", "aria-label": label, className: classes("hc-ui-menu__group", className), children }) });
1038
+ }
1039
+ var MenuItemRadio = forwardRef(function MenuItemRadio2({ value, ...props }, ref) {
1040
+ const menu = useMenu("MenuItemRadio");
1041
+ const group = useContext(RadioContext);
1042
+ if (!group) throw new Error("MenuItemRadio must be inside a MenuRadioGroup");
1043
+ const checked = group.value === value;
1044
+ return /* @__PURE__ */ jsx(
1045
+ MenuRow,
1046
+ {
1047
+ ...props,
1048
+ ref,
1049
+ role: "menuitemradio",
1050
+ "aria-checked": checked,
1051
+ end: checked ? /* @__PURE__ */ jsx("span", { className: "hc-ui-menu__check", children: checkGlyph }) : void 0,
1052
+ onChoose: () => {
1053
+ group.onValueChange(value);
1054
+ menu.close("trigger");
1055
+ }
1056
+ }
1057
+ );
1058
+ });
1059
+ var MenuItemCheckbox = forwardRef(function MenuItemCheckbox2({ checked, onCheckedChange, closeOnSelect = false, ...props }, ref) {
1060
+ const menu = useMenu("MenuItemCheckbox");
1061
+ return /* @__PURE__ */ jsx(
1062
+ MenuRow,
1063
+ {
1064
+ ...props,
1065
+ ref,
1066
+ role: "menuitemcheckbox",
1067
+ "aria-checked": checked,
1068
+ end: checked ? /* @__PURE__ */ jsx("span", { className: "hc-ui-menu__check", children: checkGlyph }) : void 0,
1069
+ onChoose: () => {
1070
+ onCheckedChange(!checked);
1071
+ if (closeOnSelect) menu.close("trigger");
1072
+ }
1073
+ }
1074
+ );
1075
+ });
1076
+ function MenuSeparator({ className, ...props }) {
1077
+ return /* @__PURE__ */ jsx("div", { ...props, role: "separator", className: classes("hc-ui-menu__separator", className) });
1078
+ }
1079
+ function MenuLabel({ className, ...props }) {
1080
+ return /* @__PURE__ */ jsx("div", { ...props, className: classes("hc-ui-menu__label", className) });
1081
+ }
1082
+ function SortMenu({ label, value, options, onChange, applied = false, size = "sm", triggerClassName, ...props }) {
1083
+ const current = options.find((option) => option.value === value)?.label;
1084
+ return /* @__PURE__ */ jsx(
1085
+ Menu,
1086
+ {
1087
+ ...props,
1088
+ label,
1089
+ trigger: /* @__PURE__ */ jsx(Chip, { size, applied, className: triggerClassName, "aria-label": current === void 0 ? label : `${label}: ${current}`, children: current }),
1090
+ children: /* @__PURE__ */ jsx(MenuRadioGroup, { value, onValueChange: onChange, children: options.map((option) => /* @__PURE__ */ jsx(MenuItemRadio, { value: option.value, hint: option.hint, disabled: option.disabled, children: option.label }, option.value)) })
1091
+ }
1092
+ );
1093
+ }
1094
+ var Popover = forwardRef(function Popover2({ label, trigger, children, autoFocus = false, open, defaultOpen, onOpenChange, side, align, portal, theme, anchorClassName, className, style, id, ...props }, ref) {
1095
+ const popup = usePopup({
1096
+ kind: "dialog",
1097
+ trigger,
1098
+ open,
1099
+ defaultOpen,
1100
+ onOpenChange,
1101
+ side,
1102
+ align,
1103
+ portal,
1104
+ theme,
1105
+ contentId: id,
1106
+ onShown: (content) => {
1107
+ if (!autoFocus) return;
1108
+ content.querySelector('button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])')?.focus({ preventScroll: true });
1109
+ }
1110
+ });
1111
+ const close = useCallback(() => popup.close("trigger"), [popup.close]);
1112
+ return /* @__PURE__ */ jsxs("div", { className: classes("hc-ui-popup-anchor", anchorClassName), ref: popup.anchorRef, children: [
1113
+ popup.triggerNode,
1114
+ popup.wrap(
1115
+ /* @__PURE__ */ jsx(
1116
+ "div",
1117
+ {
1118
+ ...props,
1119
+ ...popup.contentAttributes,
1120
+ style: mergeStyle(popup.contentAttributes.style, style),
1121
+ ref: (node) => setRefs(node, popup.contentRef, ref),
1122
+ role: "dialog",
1123
+ "aria-label": label,
1124
+ tabIndex: -1,
1125
+ className: classes("hc-ui-popover", className),
1126
+ children: typeof children === "function" ? children({ close }) : children
1127
+ }
1128
+ )
1129
+ )
1130
+ ] });
1131
+ });
1132
+ var arrowGlyph = (up) => glyph(12, up ? "M12 21L12 3M12 3L20.5 11.5M12 3L3.5 11.5" : "M12 3L12 21M12 21L20.5 12.5M12 21L3.5 12.5");
1133
+ function TableInner({ columns, rows, rowKey, sort, onSort, stickyHeader = false, onRowClick, selectedKey, className, ...props }, ref) {
1134
+ const alignOf = (column) => column.align ?? (column.numeric ? "end" : "start");
1135
+ const cellLook = (column) => ({
1136
+ className: column.className,
1137
+ "data-align": alignOf(column),
1138
+ "data-numeric": column.numeric || void 0,
1139
+ "data-tone": column.tone
1140
+ });
1141
+ const rowHeaderKey = columns.find((column) => column.rowHeader)?.key;
1142
+ return /* @__PURE__ */ jsxs("table", { ...props, ref, className: classes("hc-ui-table", className), "data-sticky-header": stickyHeader || void 0, "data-clickable": onRowClick ? "" : void 0, children: [
1143
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => {
1144
+ const direction = column.sortable ? sort && sort.key === column.key ? sort.direction : "none" : void 0;
1145
+ return /* @__PURE__ */ jsx(
1146
+ "th",
1147
+ {
1148
+ scope: "col",
1149
+ ...cellLook(column),
1150
+ "aria-sort": direction,
1151
+ style: column.width === void 0 ? void 0 : { width: column.width },
1152
+ children: column.sortable ? /* @__PURE__ */ jsxs("button", { type: "button", className: "hc-ui-table__sort", onClick: () => onSort?.(column.key), children: [
1153
+ column.header,
1154
+ direction !== "none" && arrowGlyph(direction === "ascending")
1155
+ ] }) : column.header
1156
+ },
1157
+ column.key
1158
+ );
1159
+ }) }) }),
1160
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, index) => {
1161
+ const key = rowKey(row, index);
1162
+ const selected = selectedKey !== void 0 && selectedKey !== null && key === selectedKey;
1163
+ const press = onRowClick && (() => onRowClick(row, index));
1164
+ return /* @__PURE__ */ jsx("tr", { onClick: press, "data-selected": selected || void 0, children: columns.map((column) => {
1165
+ const content = column.cell(row, index);
1166
+ if (!column.rowHeader) return /* @__PURE__ */ jsx("td", { ...cellLook(column), children: content }, column.key);
1167
+ return /* @__PURE__ */ jsx("th", { scope: "row", ...cellLook(column), children: press && column.key === rowHeaderKey ? /* @__PURE__ */ jsx(
1168
+ "button",
1169
+ {
1170
+ type: "button",
1171
+ className: "hc-ui-table__row-button",
1172
+ "aria-current": selected || void 0,
1173
+ onClick: (event) => {
1174
+ event.stopPropagation();
1175
+ press();
1176
+ },
1177
+ children: content
1178
+ }
1179
+ ) : content }, column.key);
1180
+ }) }, key);
1181
+ }) })
1182
+ ] });
1183
+ }
1184
+ var Table = forwardRef(TableInner);
1185
+ var Heading = ({ level, ...props }) => {
1186
+ const Tag = `h${level}`;
1187
+ return /* @__PURE__ */ jsx(Tag, { ...props });
1188
+ };
1189
+ var PageHeader = forwardRef(function PageHeader2({ title, level = 1, count, actions, visuallyHidden = false, className, ...props }, ref) {
1190
+ return /* @__PURE__ */ jsxs("header", { ...props, ref, className: classes("hc-ui-page-header", className), "data-visually-hidden": visuallyHidden || void 0, children: [
1191
+ /* @__PURE__ */ jsxs(Heading, { level, className: "hc-ui-page-header__title", children: [
1192
+ title,
1193
+ visible(count) && count !== 0 && /* @__PURE__ */ jsxs(Fragment2, { children: [
1194
+ " ",
1195
+ /* @__PURE__ */ jsx("small", { className: "hc-ui-page-header__count", children: count })
1196
+ ] })
1197
+ ] }),
1198
+ actions && /* @__PURE__ */ jsx("div", { className: "hc-ui-page-header__actions", children: actions })
1199
+ ] });
1200
+ });
1201
+ var navArrow = glyph(14, "M9 6L15 12L9 18");
1202
+ var PanelHeader = forwardRef(function PanelHeader2({ title, level = 2, variant = "bar", eyebrow, subtitle, subtitleInline = false, icon, trail, trailLabel, actions, onClose, closeLabel, closeTitle, closeIcon, className, ...props }, ref) {
1203
+ const steps = trail && trail.length > 0 ? trail : null;
1204
+ return /* @__PURE__ */ jsxs(
1205
+ "header",
1206
+ {
1207
+ ...props,
1208
+ ref,
1209
+ className: classes("hc-ui-panel-header", className),
1210
+ "data-variant": variant,
1211
+ "data-subtitle": subtitle && subtitleInline ? "inline" : void 0,
1212
+ children: [
1213
+ icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-panel-header__icon", "aria-hidden": "true", children: icon }),
1214
+ steps ? /* @__PURE__ */ jsx("nav", { className: "hc-ui-panel-header__trail", "aria-label": trailLabel, children: steps.map((step, index) => {
1215
+ const last = index === steps.length - 1;
1216
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1217
+ index > 0 && navArrow,
1218
+ last ? /* @__PURE__ */ jsx(Heading, { level, className: "hc-ui-panel-header__title", "aria-current": "location", children: step.label }) : step.onSelect ? /* @__PURE__ */ jsx("button", { type: "button", className: "hc-ui-panel-header__step", onClick: step.onSelect, children: step.label }) : /* @__PURE__ */ jsx("strong", { className: "hc-ui-panel-header__title", children: step.label })
1219
+ ] }, `${index}:${step.label}`);
1220
+ }) }) : /* @__PURE__ */ jsxs("div", { className: "hc-ui-panel-header__text", children: [
1221
+ eyebrow && /* @__PURE__ */ jsx("p", { className: "hc-ui-panel-header__eyebrow", children: eyebrow }),
1222
+ /* @__PURE__ */ jsx(Heading, { level, className: "hc-ui-panel-header__title", children: title }),
1223
+ subtitle && /* @__PURE__ */ jsx("p", { className: "hc-ui-panel-header__subtitle", children: subtitle })
1224
+ ] }),
1225
+ actions && /* @__PURE__ */ jsx("div", { className: "hc-ui-panel-header__actions", children: actions }),
1226
+ onClose && /* @__PURE__ */ jsx("button", { type: "button", className: "hc-ui-panel-header__close", onClick: onClose, "aria-label": closeLabel, title: closeTitle ?? closeLabel, children: closeIcon ?? glyph(18, XMARK) })
1227
+ ]
1228
+ }
1229
+ );
1230
+ });
1231
+ var ListRow = forwardRef(function ListRow2({ type = "button", className, ...props }, ref) {
1232
+ return /* @__PURE__ */ jsx("button", { ...props, ref, type, className: classes("hc-ui-list-row", className) });
1233
+ });
1234
+ var searchGlyph = /* @__PURE__ */ jsxs("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", strokeWidth: 1.8, "aria-hidden": "true", focusable: "false", children: [
1235
+ /* @__PURE__ */ jsx("path", { d: "M17 17L21 21", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
1236
+ /* @__PURE__ */ jsx("path", { d: "M3 11C3 15.4183 6.58172 19 11 19C13.213 19 15.2161 18.1015 16.6644 16.6493C18.1077 15.2022 19 13.2053 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11Z", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" })
1237
+ ] });
1238
+ var CommandPalette = forwardRef(function CommandPalette2({
1239
+ open,
1240
+ onClose,
1241
+ label,
1242
+ inputLabel,
1243
+ placeholder,
1244
+ query,
1245
+ onQueryChange,
1246
+ groups,
1247
+ searchIcon,
1248
+ loading,
1249
+ empty,
1250
+ selectHint,
1251
+ altHint,
1252
+ hints,
1253
+ listClassName,
1254
+ className,
1255
+ onClick,
1256
+ onPointerDown,
1257
+ ...props
1258
+ }, ref) {
1259
+ const dialogRef = useRef(null);
1260
+ const inputRef = useRef(null);
1261
+ const listRef = useRef(null);
1262
+ const setRef = useCallback((node) => setRefs(node, dialogRef, ref), [ref]);
1263
+ const id = useId();
1264
+ const listId = `${id}-list`;
1265
+ const optionId = (index2) => `${id}-option-${index2}`;
1266
+ const items = useMemo(() => groups.flatMap((group) => group.items), [groups]);
1267
+ const [active, setActive] = useState(0);
1268
+ const [seen, setSeen] = useState({ query, open });
1269
+ if (seen.query !== query || seen.open !== open) {
1270
+ setSeen({ query, open });
1271
+ if (active !== 0) setActive(0);
1272
+ }
1273
+ const current = items.length ? Math.min(active, items.length - 1) : -1;
1274
+ const latest = useRef({ open, onClose });
1275
+ useIsomorphicLayoutEffect(() => {
1276
+ latest.current = { open, onClose };
1277
+ });
1278
+ useIsomorphicLayoutEffect(() => {
1279
+ const dialog = dialogRef.current;
1280
+ if (!open || !dialog) return;
1281
+ const trigger = document.activeElement instanceof HTMLElement && !dialog.contains(document.activeElement) ? document.activeElement : null;
1282
+ if (!dialog.open) dialog.showModal();
1283
+ inputRef.current?.focus();
1284
+ const unlock = lockScroll();
1285
+ return () => {
1286
+ unlock();
1287
+ if (dialog.open) dialog.close();
1288
+ queueMicrotask(() => {
1289
+ const focused = document.activeElement;
1290
+ const lost = !focused || focused === document.body || dialog.contains(focused);
1291
+ if (lost && !dialog.open && trigger?.isConnected) trigger.focus({ preventScroll: true });
1292
+ });
1293
+ };
1294
+ }, [open]);
1295
+ useEffect(() => {
1296
+ if (open && current >= 0) listRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
1297
+ }, [open, current]);
1298
+ const ownEvent = (event) => event.target === event.currentTarget;
1299
+ const handleCancel = (event) => {
1300
+ if (!ownEvent(event)) return;
1301
+ event.preventDefault();
1302
+ latest.current.onClose();
1303
+ };
1304
+ const handleNativeClose = (event) => {
1305
+ const dialog = event.currentTarget;
1306
+ if (!ownEvent(event) || dialog.open || !latest.current.open) return;
1307
+ latest.current.onClose();
1308
+ window.setTimeout(() => {
1309
+ if (latest.current.open && dialog.isConnected && !dialog.open) {
1310
+ dialog.showModal();
1311
+ inputRef.current?.focus();
1312
+ }
1313
+ }, 0);
1314
+ };
1315
+ const pressedBackdrop = useRef(false);
1316
+ const handlePointerDown = (event) => {
1317
+ onPointerDown?.(event);
1318
+ pressedBackdrop.current = ownEvent(event) && outside(event.currentTarget, event.clientX, event.clientY);
1319
+ };
1320
+ const handleClick = (event) => {
1321
+ onClick?.(event);
1322
+ const fromBackdrop = pressedBackdrop.current && ownEvent(event) && outside(event.currentTarget, event.clientX, event.clientY);
1323
+ pressedBackdrop.current = false;
1324
+ if (fromBackdrop) latest.current.onClose();
1325
+ };
1326
+ const handleKeyDown = (event) => {
1327
+ const count = items.length;
1328
+ if (!count || event.nativeEvent.isComposing || event.altKey) return;
1329
+ let next = null;
1330
+ if (event.key === "ArrowDown") next = (current + 1) % count;
1331
+ else if (event.key === "ArrowUp") next = (current - 1 + count) % count;
1332
+ else if (event.key === "Home" && !event.shiftKey) next = 0;
1333
+ else if (event.key === "End" && !event.shiftKey) next = count - 1;
1334
+ else if (event.key === "Enter") {
1335
+ const item = items[current];
1336
+ event.preventDefault();
1337
+ if ((event.metaKey || event.ctrlKey) && item.onAltSelect) item.onAltSelect();
1338
+ else item.onSelect();
1339
+ return;
1340
+ }
1341
+ if (next === null) return;
1342
+ event.preventDefault();
1343
+ setActive(next);
1344
+ };
1345
+ if (!open) return null;
1346
+ let index = 0;
1347
+ return /* @__PURE__ */ jsx(
1348
+ "dialog",
1349
+ {
1350
+ ...props,
1351
+ ref: setRef,
1352
+ className: classes("hc-ui-command-palette", className),
1353
+ "aria-label": label,
1354
+ onCancel: handleCancel,
1355
+ onClose: handleNativeClose,
1356
+ onPointerDown: handlePointerDown,
1357
+ onClick: handleClick,
1358
+ children: /* @__PURE__ */ jsxs("div", { className: "hc-ui-command-palette__box", onKeyDown: handleKeyDown, children: [
1359
+ /* @__PURE__ */ jsxs("label", { className: "hc-ui-command-palette__field", children: [
1360
+ searchIcon ?? searchGlyph,
1361
+ /* @__PURE__ */ jsx(
1362
+ "input",
1363
+ {
1364
+ ref: inputRef,
1365
+ className: "hc-ui-command-palette__input",
1366
+ value: query,
1367
+ placeholder,
1368
+ onChange: (event) => onQueryChange(event.target.value),
1369
+ autoComplete: "off",
1370
+ "aria-label": inputLabel ?? label,
1371
+ role: "combobox",
1372
+ "aria-expanded": "true",
1373
+ "aria-controls": listId,
1374
+ "aria-autocomplete": "list",
1375
+ "aria-activedescendant": current >= 0 ? optionId(current) : void 0
1376
+ }
1377
+ ),
1378
+ visible(loading) && /* @__PURE__ */ jsx("span", { className: "hc-ui-command-palette__status", children: loading })
1379
+ ] }),
1380
+ /* @__PURE__ */ jsxs(
1381
+ "div",
1382
+ {
1383
+ ref: listRef,
1384
+ className: classes("hc-ui-command-palette__list", listClassName),
1385
+ id: listId,
1386
+ role: "listbox",
1387
+ "aria-label": label,
1388
+ "aria-busy": visible(loading) || void 0,
1389
+ children: [
1390
+ visible(empty) && /* @__PURE__ */ jsx("p", { className: "hc-ui-command-palette__empty", children: empty }),
1391
+ groups.map((group, groupIndex) => {
1392
+ if (!group.items.length) return null;
1393
+ const headingId = visible(group.heading) ? `${id}-group-${groupIndex}` : void 0;
1394
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-command-palette__group", role: headingId ? "group" : "presentation", "aria-labelledby": headingId, children: [
1395
+ headingId && /* @__PURE__ */ jsx("p", { className: "hc-ui-command-palette__heading", id: headingId, "aria-hidden": "true", children: group.heading }),
1396
+ group.items.map((item) => {
1397
+ const itemIndex = index++;
1398
+ const isActive = itemIndex === current;
1399
+ const itemAltHint = item.onAltSelect ? item.altHint ?? altHint : void 0;
1400
+ return /* @__PURE__ */ jsxs(
1401
+ "button",
1402
+ {
1403
+ type: "button",
1404
+ tabIndex: -1,
1405
+ id: optionId(itemIndex),
1406
+ role: "option",
1407
+ "aria-selected": isActive,
1408
+ className: "hc-ui-command-palette__item",
1409
+ onMouseDown: (event) => event.preventDefault(),
1410
+ onMouseMove: () => {
1411
+ if (!isActive) setActive(itemIndex);
1412
+ },
1413
+ onClick: () => item.onSelect(),
1414
+ children: [
1415
+ item.icon !== void 0 && /* @__PURE__ */ jsx("span", { className: "hc-ui-command-palette__icon", "aria-hidden": "true", children: item.icon }),
1416
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-command-palette__text", children: [
1417
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-command-palette__label", children: item.label }),
1418
+ visible(item.detail) && /* @__PURE__ */ jsx("small", { className: "hc-ui-command-palette__detail", children: item.detail })
1419
+ ] }),
1420
+ isActive && visible(itemAltHint) && /* @__PURE__ */ jsx("kbd", { className: "hc-ui-command-palette__kbd", "aria-hidden": "true", children: itemAltHint }),
1421
+ isActive && visible(selectHint) && /* @__PURE__ */ jsx("kbd", { className: "hc-ui-command-palette__kbd", "aria-hidden": "true", children: selectHint })
1422
+ ]
1423
+ },
1424
+ item.key
1425
+ );
1426
+ })
1427
+ ] }, group.key);
1428
+ })
1429
+ ]
1430
+ }
1431
+ ),
1432
+ hints && hints.length > 0 && /* @__PURE__ */ jsx("footer", { className: "hc-ui-command-palette__footer", children: hints.map((hint, hintIndex) => /* @__PURE__ */ jsxs("span", { children: [
1433
+ hint.keys.map((key, keyIndex) => /* @__PURE__ */ jsx("kbd", { className: "hc-ui-command-palette__kbd", children: key }, keyIndex)),
1434
+ " ",
1435
+ hint.label
1436
+ ] }, hintIndex)) })
1437
+ ] })
1438
+ }
1439
+ );
1440
+ });
1441
+ var cx = (...names) => names.filter(Boolean).join(" ");
1442
+ var flag = (on) => on ? "" : void 0;
1443
+ function useImageChain(sources) {
1444
+ const key = sources.join("\n");
1445
+ const [state, setState] = useState({ key, index: 0 });
1446
+ const index = state.key === key ? state.index : 0;
1447
+ if (state.key !== key) setState({ key, index: 0 });
1448
+ return {
1449
+ src: sources[index],
1450
+ index,
1451
+ /** Call from `onError`: moves to the next address. */
1452
+ next: () => setState({ key, index: index + 1 })
1453
+ };
1454
+ }
1455
+ var MINI_DIAL_STATS = ["speed", "attack", "defense", "damage"];
1456
+ var MiniDialView = forwardRef(function MiniDialView2({ columns, currentClick, label, className }, ref) {
1457
+ if (columns.length === 0) return null;
1458
+ return /* @__PURE__ */ jsx(
1459
+ "span",
1460
+ {
1461
+ ref,
1462
+ className: cx("hc-ui-mini-dial", className),
1463
+ role: "img",
1464
+ "aria-label": label,
1465
+ style: { gridTemplateColumns: `repeat(${columns.length}, var(--hc-mini-dial-cell, 7px))` },
1466
+ children: MINI_DIAL_STATS.map(
1467
+ (stat) => columns.map(({ click, cells }) => {
1468
+ const { color, round, special } = cells[stat];
1469
+ return /* @__PURE__ */ jsx(
1470
+ "i",
1471
+ {
1472
+ className: "hc-ui-mini-dial__cell",
1473
+ "data-round": flag(round),
1474
+ "data-past": flag(currentClick !== void 0 && click < currentClick),
1475
+ "data-now": flag(click === currentClick),
1476
+ "data-special": flag(special),
1477
+ "data-empty": flag(!color),
1478
+ style: color ? { backgroundColor: color } : void 0
1479
+ },
1480
+ `${stat}-${click}`
1481
+ );
1482
+ })
1483
+ )
1484
+ }
1485
+ );
1486
+ });
1487
+ var PowerIconView = forwardRef(function PowerIconView2({ color, glyphSrc, invertGlyph = false, size = 24 }, ref) {
1488
+ return /* @__PURE__ */ jsx("span", { ref, className: "hc-ui-power-icon", style: { width: size, height: size, backgroundColor: color }, children: /* @__PURE__ */ jsx("img", { className: "hc-ui-power-icon__glyph", "data-invert": flag(invertGlyph), src: glyphSrc, alt: "" }) });
1489
+ });
1490
+ var StatGlyphView = forwardRef(function StatGlyphView2({ src, alt, size = 22 }, ref) {
1491
+ return /* @__PURE__ */ jsx("img", { ref, className: "hc-ui-stat-glyph", src, alt, style: { width: size, height: size } });
1492
+ });
1493
+ var SpecialIconView = forwardRef(function SpecialIconView2({ backgroundSrc, glyphSrc, size = 24 }, ref) {
1494
+ return /* @__PURE__ */ jsxs("span", { ref, className: "hc-ui-special-icon", style: { width: size, height: size }, children: [
1495
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-special-icon__bg", src: backgroundSrc, alt: "" }),
1496
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-special-icon__glyph", src: glyphSrc, alt: "" })
1497
+ ] });
1498
+ });
1499
+ function EquipmentIcon({ size = 24 }) {
1500
+ return /* @__PURE__ */ jsx("span", { className: "hc-ui-equipment-icon", style: { width: size, height: size }, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "58%", height: "58%", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M12 2.5 L21.5 12 L12 21.5 L2.5 12 Z" }) }) });
1501
+ }
1502
+ var STAT_ICON_PATHS = {
1503
+ speed: "M12 4 L17 9 L13 9 L13 17 L11 17 L11 9 L7 9 Z",
1504
+ attack: "M4 12 L9 7 L9 11 L17 11 L17 13 L9 13 L9 17 Z",
1505
+ defense: "M12 3 L20 6 V12 C20 16 16.5 19.5 12 21 C7.5 19.5 4 16 4 12 V6 Z",
1506
+ damage: "M12 4 L20 12 L12 20 L4 12 Z"
1507
+ };
1508
+ function StatIcon({ type, size = 24, className }) {
1509
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", className, "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: STAT_ICON_PATHS[type] }) });
1510
+ }
1511
+ var preventDefault = (event) => event.preventDefault();
1512
+ var AvatarView = forwardRef(function AvatarView2({ sources, fallbackSrc, alt, shape = "round", className, onFallback, size }, ref) {
1513
+ const { src, index, next } = useImageChain(sources);
1514
+ const isFallback = src === void 0;
1515
+ return /* @__PURE__ */ jsx(
1516
+ "img",
1517
+ {
1518
+ ref,
1519
+ src: src ?? fallbackSrc,
1520
+ alt,
1521
+ className: cx("hc-ui-avatar", className),
1522
+ "data-shape": shape,
1523
+ "data-fallback": flag(isFallback),
1524
+ "data-sized": flag(size !== void 0),
1525
+ style: size !== void 0 ? { "--hc-avatar-size": `${size}px` } : void 0,
1526
+ loading: "lazy",
1527
+ decoding: "async",
1528
+ referrerPolicy: "no-referrer",
1529
+ draggable: false,
1530
+ onDragStart: preventDefault,
1531
+ onContextMenu: preventDefault,
1532
+ onMouseDown: preventDefault,
1533
+ onTouchStart: preventDefault,
1534
+ onError: () => {
1535
+ if (isFallback) return;
1536
+ next();
1537
+ if (index + 1 >= sources.length) onFallback?.();
1538
+ }
1539
+ }
1540
+ );
1541
+ });
1542
+ var SetIconView = forwardRef(function SetIconView2({ sources, className, title }, ref) {
1543
+ const { src, next } = useImageChain(sources);
1544
+ if (!src) return null;
1545
+ return /* @__PURE__ */ jsx("img", { ref, className, src, alt: "", "aria-hidden": true, title, loading: "lazy", decoding: "async", onError: next }, src);
1546
+ });
1547
+ var COMBAT_DIAL_STATS = ["speed", "attack", "defense", "damage"];
1548
+ var PRINTED_ROW = 12;
1549
+ var CombatDialView = forwardRef(function CombatDialView2({
1550
+ columns,
1551
+ legend,
1552
+ points,
1553
+ selectedPointIndex,
1554
+ focusClick,
1555
+ currentClick,
1556
+ labels,
1557
+ presentation = "default",
1558
+ onClickChange,
1559
+ onSelectPoint,
1560
+ tip,
1561
+ onTipChange: setTip,
1562
+ tipId,
1563
+ tipSlot
1564
+ }, ref) {
1565
+ const tracksClick = !!onClickChange;
1566
+ const hasMultiplePoints = points.length > 1;
1567
+ const selectedPointColumnRef = useRef(null);
1568
+ const gridWrapRef = useRef(null);
1569
+ const hideTip = () => setTip(null);
1570
+ useEffect(() => {
1571
+ const wrap = gridWrapRef.current;
1572
+ const column = selectedPointColumnRef.current;
1573
+ if (!wrap || !column) return;
1574
+ const wrapRect = wrap.getBoundingClientRect();
1575
+ const columnRect = column.getBoundingClientRect();
1576
+ const delta = columnRect.left - wrapRect.left - (wrap.clientWidth - column.clientWidth) / 2;
1577
+ wrap.scrollTo({ left: wrap.scrollLeft + delta, behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
1578
+ }, [selectedPointIndex, focusClick, points.length]);
1579
+ if (columns.length === 0) return null;
1580
+ const renderValueCell = (stat, cell, clickNumber) => {
1581
+ const { tip: content } = cell;
1582
+ const hasTip = !!content;
1583
+ const key = `${clickNumber}:${stat}`;
1584
+ const openTip = (trigger, x, y, mode) => {
1585
+ if (content) setTip({ key, click: clickNumber, stat, content, anchor: { x, y, preferAbove: mode === "dialog" }, mode, trigger });
1586
+ };
1587
+ const cellProps = {
1588
+ className: "hc-ui-combat-dial__cell",
1589
+ "data-kind": "value",
1590
+ "data-round": flag(cell.round),
1591
+ "data-outlined": flag(cell.outlined),
1592
+ style: { backgroundColor: cell.background, color: cell.color }
1593
+ };
1594
+ if (!hasTip && !tracksClick) return /* @__PURE__ */ jsx("span", { role: "img", "aria-label": cell.label, ...cellProps, children: cell.value });
1595
+ return /* @__PURE__ */ jsx(
1596
+ "button",
1597
+ {
1598
+ type: "button",
1599
+ "aria-label": cell.label,
1600
+ "aria-haspopup": hasTip ? "dialog" : void 0,
1601
+ "aria-expanded": hasTip ? tip?.key === key && tip.mode === "dialog" : void 0,
1602
+ "aria-describedby": tip?.key === key && tip.mode === "hover" ? tipId : void 0,
1603
+ ...cellProps,
1604
+ "data-tip": flag(hasTip),
1605
+ onPointerEnter: (event) => {
1606
+ if (event.pointerType === "mouse" && hasTip && tip?.mode !== "dialog") openTip(event.currentTarget, event.clientX, event.clientY, "hover");
1607
+ },
1608
+ onPointerMove: (event) => {
1609
+ if (event.pointerType === "mouse") setTip((previous) => previous?.key === key && previous.mode === "hover" ? { ...previous, anchor: { x: event.clientX, y: event.clientY } } : previous);
1610
+ },
1611
+ onPointerLeave: () => setTip((previous) => previous?.mode === "hover" ? null : previous),
1612
+ onClick: (event) => {
1613
+ event.stopPropagation();
1614
+ onClickChange?.(clickNumber);
1615
+ if (hasTip) {
1616
+ const rect = event.currentTarget.getBoundingClientRect();
1617
+ openTip(event.currentTarget, event.detail ? event.clientX : rect.left, event.detail ? event.clientY : rect.bottom, "dialog");
1618
+ } else hideTip();
1619
+ },
1620
+ children: cell.value
1621
+ }
1622
+ );
1623
+ };
1624
+ const legendIcon = (icon) => !icon.glyph ? /* @__PURE__ */ jsx("img", { className: "hc-ui-combat-dial__special-icon hc-ui-stat-glyph", src: icon.src, alt: icon.alt, title: icon.title }) : presentation === "printed" ? /* @__PURE__ */ jsx("img", { className: "hc-ui-stat-glyph", src: icon.src, alt: icon.alt }) : /* @__PURE__ */ jsx(StatGlyphView, { src: icon.src, alt: icon.alt });
1625
+ const rows = presentation === "printed" && columns.length > PRINTED_ROW ? [columns.slice(0, PRINTED_ROW), columns.slice(PRINTED_ROW)] : [columns];
1626
+ const renderGridRow = (rowColumns, withKo, rowIndex) => /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__grid", children: [
1627
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__col", "data-kind": "legend", children: [
1628
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__cell hc-ui-combat-dial__rangebox", "data-kind": "legend", children: [
1629
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__range", children: legend.range }),
1630
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__bolts", children: Array.from({ length: Math.max(legend.targets, 0) }).map((_, i) => /* @__PURE__ */ jsx("img", { className: "hc-ui-combat-dial__bolt", src: legend.boltSrc, alt: "" }, i)) })
1631
+ ] }),
1632
+ COMBAT_DIAL_STATS.map((stat) => /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "legend", children: legendIcon(legend.icons[stat]) }, stat))
1633
+ ] }),
1634
+ rowColumns.map((column) => {
1635
+ const isCurrent = tracksClick && column.click === currentClick;
1636
+ return /* @__PURE__ */ jsxs(
1637
+ "div",
1638
+ {
1639
+ role: "group",
1640
+ ref: focusClick === column.click ? selectedPointColumnRef : null,
1641
+ className: "hc-ui-combat-dial__col",
1642
+ "data-kind": "click",
1643
+ "data-current": flag(isCurrent),
1644
+ "data-dimmed": flag(column.dimmed),
1645
+ "data-start": flag(!!column.startColor),
1646
+ style: column.startColor ? { "--hc-dial-start-color": column.startColor } : void 0,
1647
+ "aria-label": column.label,
1648
+ children: [
1649
+ onClickChange ? /* @__PURE__ */ jsx("button", { type: "button", className: "hc-ui-combat-dial__cell", "data-kind": "number", onClick: () => onClickChange(column.click), "aria-pressed": isCurrent, "aria-label": column.selectLabel, children: column.click }) : /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "number", children: column.click }),
1650
+ COMBAT_DIAL_STATS.map((stat) => /* @__PURE__ */ jsx(Fragment, { children: renderValueCell(stat, column.cells[stat], column.click) }, stat))
1651
+ ]
1652
+ },
1653
+ column.click
1654
+ );
1655
+ }),
1656
+ withKo && /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__col", "data-kind": "ko", children: [
1657
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "number", children: columns.length + 1 }),
1658
+ COMBAT_DIAL_STATS.map((stat) => /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "ko", children: labels.koCell }, stat))
1659
+ ] })
1660
+ ] }, rowIndex);
1661
+ return /* @__PURE__ */ jsxs("section", { ref, className: "hc-ui-combat-dial", "data-presentation": presentation, "data-static": flag(!tracksClick), children: [
1662
+ presentation !== "printed" && /* @__PURE__ */ jsxs("header", { className: "hc-ui-combat-dial__header", children: [
1663
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__header-top", children: [
1664
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__label", children: labels.heading }),
1665
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-combat-dial__status", children: [
1666
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__status-ko", children: labels.koMark }),
1667
+ /* @__PURE__ */ jsx("span", { children: labels.koAt })
1668
+ ] })
1669
+ ] }),
1670
+ points.some((point) => point.valueLabel) && /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__points", role: hasMultiplePoints ? "group" : void 0, children: points.map((point) => {
1671
+ const isActive = point.index === selectedPointIndex;
1672
+ return /* @__PURE__ */ jsxs(
1673
+ "button",
1674
+ {
1675
+ type: "button",
1676
+ className: "hc-ui-combat-dial__chip",
1677
+ style: { "--hc-dial-chip-color": point.color },
1678
+ onClick: () => onSelectPoint(point),
1679
+ disabled: !hasMultiplePoints,
1680
+ "aria-pressed": isActive,
1681
+ title: hasMultiplePoints ? point.title : void 0,
1682
+ children: [
1683
+ point.valueLabel && /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__chip-value", children: point.valueLabel }),
1684
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__chip-bar" }),
1685
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__chip-clicks", children: point.clicksLabel })
1686
+ ]
1687
+ },
1688
+ point.index
1689
+ );
1690
+ }) })
1691
+ ] }),
1692
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__grid-wrap", ref: gridWrapRef, children: rows.map((rowColumns, rowIndex) => renderGridRow(rowColumns, rowIndex === rows.length - 1, rowIndex)) }),
1693
+ tip && tipSlot
1694
+ ] });
1695
+ });
1696
+ var CARD_WIDTH = 375;
1697
+ var CARD_HEIGHT = 525;
1698
+ var BACK_TABLE_HEIGHT = 440;
1699
+ var BYSTANDER_DIAL_STEP = { x: 7, y: 24 };
1700
+ function nextDistribution(pages, overflowing) {
1701
+ const last = pages.length ? Math.max(...pages) : 0;
1702
+ for (let table = 0; table <= last; table += 1) {
1703
+ if (!overflowing(table)) continue;
1704
+ const items = pages.flatMap((page, index) => page === table ? [index] : []);
1705
+ if (!items.length || table > 0 && items.length < 2) continue;
1706
+ const next = pages.slice();
1707
+ next[items.at(-1)] = table + 1;
1708
+ return next;
1709
+ }
1710
+ return null;
1711
+ }
1712
+ function backCount(pages) {
1713
+ const last = pages.length ? Math.max(...pages) : 0;
1714
+ return Math.max(1, Math.ceil(last / 2));
1715
+ }
1716
+ var MAX_TILT_DEG = 3;
1717
+ var DIAL_DAMPING = 0.15;
1718
+ var CARD_DIAL_SELECTOR = ".hc-ui-physical-card__dial, .hc-ui-physical-card__bystander-dial";
1719
+ var CAN_TILT = "(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)";
1720
+ var CAN_DEVICE_TILT = "(hover: none) and (pointer: coarse) and (prefers-reduced-motion: no-preference)";
1721
+ var DEVICE_RANGE_DEG = 18;
1722
+ var BASELINE_DRIFT = 0.01;
1723
+ var DEVICE_SMOOTHING = 0.2;
1724
+ function tiltAt(x, y, damping = 1) {
1725
+ const offsetX = (x - 0.5) * 2 * damping;
1726
+ const offsetY = (y - 0.5) * 2 * damping;
1727
+ return {
1728
+ rotateX: -offsetY * MAX_TILT_DEG,
1729
+ rotateY: offsetX * MAX_TILT_DEG,
1730
+ parallaxX: offsetX,
1731
+ parallaxY: offsetY,
1732
+ shineX: x * 100,
1733
+ shineY: y * 100
1734
+ };
1735
+ }
1736
+ var PARALLAX_ART_PX = 4;
1737
+ var PARALLAX_HEADER_PX = 1.5;
1738
+ var CARD_ART_SELECTOR = ".hc-ui-physical-card__token, .hc-ui-physical-card__terrain-art, .hc-ui-physical-card__bystander-token";
1739
+ function tiltStyles(tilt) {
1740
+ return {
1741
+ card: `perspective(1100px) rotateX(${tilt.rotateX}deg) rotateY(${tilt.rotateY}deg)`,
1742
+ art: `${tilt.parallaxX * PARALLAX_ART_PX}px ${tilt.parallaxY * PARALLAX_ART_PX}px`,
1743
+ header: `${tilt.parallaxX * PARALLAX_HEADER_PX}px ${tilt.parallaxY * PARALLAX_HEADER_PX}px`,
1744
+ shine: `${tilt.shineX}% ${tilt.shineY}%`
1745
+ };
1746
+ }
1747
+ var eachIn = (root, selector, write) => {
1748
+ root.querySelectorAll(selector).forEach(write);
1749
+ };
1750
+ function applyTilt(root, tilt) {
1751
+ const styles = tiltStyles(tilt);
1752
+ root.dataset.tilt = "";
1753
+ eachIn(root, ".hc-ui-physical-card-tilt", (element) => {
1754
+ element.style.transform = styles.card;
1755
+ });
1756
+ eachIn(root, CARD_ART_SELECTOR, (element) => {
1757
+ element.style.translate = styles.art;
1758
+ });
1759
+ eachIn(root, ".hc-ui-physical-card__header", (element) => {
1760
+ element.style.translate = styles.header;
1761
+ });
1762
+ eachIn(root, ".hc-ui-physical-card__shine", (element) => {
1763
+ element.style.setProperty("--hc-card-shine-at", styles.shine);
1764
+ });
1765
+ }
1766
+ function clearTilt(root) {
1767
+ delete root.dataset.tilt;
1768
+ eachIn(root, ".hc-ui-physical-card-tilt", (element) => {
1769
+ element.style.transform = "";
1770
+ });
1771
+ eachIn(root, `${CARD_ART_SELECTOR}, .hc-ui-physical-card__header`, (element) => {
1772
+ element.style.translate = "";
1773
+ });
1774
+ }
1775
+ function devicePosition(beta, gamma, baseline, screenAngle = 0) {
1776
+ const clampAxis = (value) => Math.max(-1, Math.min(1, value / DEVICE_RANGE_DEG));
1777
+ const front = clampAxis(beta - baseline.beta);
1778
+ const side = clampAxis(gamma - baseline.gamma);
1779
+ const [horizontal, vertical] = screenAngle === 90 ? [front, -side] : screenAngle === 270 ? [-front, side] : screenAngle === 180 ? [-side, -front] : [side, front];
1780
+ return { x: (horizontal + 1) / 2, y: (vertical + 1) / 2 };
1781
+ }
1782
+ var motionAsked = false;
1783
+ function requestMotionAccess() {
1784
+ const request = typeof DeviceOrientationEvent === "undefined" ? void 0 : DeviceOrientationEvent.requestPermission;
1785
+ if (motionAsked || !request) return;
1786
+ motionAsked = true;
1787
+ request.call(DeviceOrientationEvent).catch(() => {
1788
+ motionAsked = false;
1789
+ });
1790
+ }
1791
+ function useDeviceTilt(ref, enabled) {
1792
+ useEffect(() => {
1793
+ const element = ref.current;
1794
+ if (!enabled || !element || typeof DeviceOrientationEvent === "undefined" || !window.matchMedia(CAN_DEVICE_TILT).matches) return;
1795
+ let touching = false;
1796
+ let baseline = null;
1797
+ let position = { x: 0.5, y: 0.5 };
1798
+ let frame = 0;
1799
+ const onOrientation = (event) => {
1800
+ if (touching || event.beta == null || event.gamma == null) return;
1801
+ baseline ?? (baseline = { beta: event.beta, gamma: event.gamma });
1802
+ baseline.beta += (event.beta - baseline.beta) * BASELINE_DRIFT;
1803
+ baseline.gamma += (event.gamma - baseline.gamma) * BASELINE_DRIFT;
1804
+ const target = devicePosition(event.beta, event.gamma, baseline, screen.orientation?.angle ?? 0);
1805
+ position = { x: position.x + (target.x - position.x) * DEVICE_SMOOTHING, y: position.y + (target.y - position.y) * DEVICE_SMOOTHING };
1806
+ cancelAnimationFrame(frame);
1807
+ frame = requestAnimationFrame(() => applyTilt(element, tiltAt(position.x, position.y)));
1808
+ };
1809
+ const onTouchStart = () => {
1810
+ touching = true;
1811
+ };
1812
+ const onTouchEnd = (event) => {
1813
+ touching = event.touches.length > 0;
1814
+ };
1815
+ const passive = { passive: true, capture: true };
1816
+ window.addEventListener("deviceorientation", onOrientation);
1817
+ window.addEventListener("touchstart", onTouchStart, passive);
1818
+ window.addEventListener("touchend", onTouchEnd, passive);
1819
+ window.addEventListener("touchcancel", onTouchEnd, passive);
1820
+ return () => {
1821
+ window.removeEventListener("deviceorientation", onOrientation);
1822
+ window.removeEventListener("touchstart", onTouchStart, passive);
1823
+ window.removeEventListener("touchend", onTouchEnd, passive);
1824
+ window.removeEventListener("touchcancel", onTouchEnd, passive);
1825
+ cancelAnimationFrame(frame);
1826
+ clearTilt(element);
1827
+ };
1828
+ }, [ref, enabled]);
1829
+ }
1830
+ function useCardTilt() {
1831
+ const frame = useRef(0);
1832
+ useEffect(() => () => cancelAnimationFrame(frame.current), []);
1833
+ const onPointerMove = (event) => {
1834
+ if (event.pointerType !== "mouse" || !window.matchMedia(CAN_TILT).matches) return;
1835
+ const slot = event.currentTarget;
1836
+ const rect = slot.getBoundingClientRect();
1837
+ const overDial = event.target.closest(CARD_DIAL_SELECTOR);
1838
+ const tilt = tiltAt((event.clientX - rect.left) / rect.width, (event.clientY - rect.top) / rect.height, overDial ? DIAL_DAMPING : 1);
1839
+ cancelAnimationFrame(frame.current);
1840
+ frame.current = requestAnimationFrame(() => applyTilt(slot, tilt));
1841
+ };
1842
+ const onPointerLeave = (event) => {
1843
+ cancelAnimationFrame(frame.current);
1844
+ clearTilt(event.currentTarget);
1845
+ };
1846
+ return { onPointerMove, onPointerLeave };
1847
+ }
1848
+ var PHYSICAL_CARD_STATS = ["speed", "attack", "defense", "damage"];
1849
+ var CARD_GAP = 16;
1850
+ var MAX_CARD_SCALE = 1.2;
1851
+ var SIDE_BY_SIDE_MIN_SCALE = 0.86;
1852
+ var LONG_PRESS_MS = 450;
1853
+ var LONG_PRESS_SLOP = 10;
1854
+ var FOCUS_CHROME_HEIGHT = 112;
1855
+ function useCardScale(fitHeight, focus, scrollerSelector) {
1856
+ const ref = useRef(null);
1857
+ const [width, setWidth] = useState(0);
1858
+ const [height, setHeight] = useState(0);
1859
+ useLayoutEffect(() => {
1860
+ const element = ref.current;
1861
+ if (!element) return;
1862
+ const scroller = fitHeight && scrollerSelector ? element.closest(scrollerSelector) : null;
1863
+ const measure = () => {
1864
+ setWidth(element.clientWidth);
1865
+ if (focus) setHeight(window.innerHeight - FOCUS_CHROME_HEIGHT);
1866
+ if (!scroller) return;
1867
+ const top = element.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
1868
+ setHeight(scroller.clientHeight - top - 16);
1869
+ };
1870
+ measure();
1871
+ if (typeof ResizeObserver === "undefined") return;
1872
+ const observer = new ResizeObserver(measure);
1873
+ observer.observe(element);
1874
+ if (scroller) observer.observe(scroller);
1875
+ return () => observer.disconnect();
1876
+ }, [fitHeight, focus, scrollerSelector]);
1877
+ const columns = !focus && width >= CARD_WIDTH * SIDE_BY_SIDE_MIN_SCALE * 2 + CARD_GAP ? 2 : 1;
1878
+ const byWidth = width ? Math.min(MAX_CARD_SCALE, (width - CARD_GAP * (columns - 1)) / columns / CARD_WIDTH) : 1;
1879
+ const scale = (fitHeight || focus) && height > 0 ? Math.max(0.6, Math.min(byWidth, height / CARD_HEIGHT)) : byWidth;
1880
+ return { ref, scale };
1881
+ }
1882
+ function useFitText(text, max, min) {
1883
+ const ref = useRef(null);
1884
+ const [fit, setFit] = useState({ text, size: max });
1885
+ const size = fit.text === text ? fit.size : max;
1886
+ useLayoutEffect(() => {
1887
+ const element = ref.current;
1888
+ if (!element || size <= min || element.scrollWidth <= element.clientWidth + 1) return;
1889
+ setFit({ text, size: size - 1 });
1890
+ });
1891
+ return { ref, size };
1892
+ }
1893
+ function CardRule({ name, text, icon, onCopy }) {
1894
+ return /* @__PURE__ */ jsxs("section", { className: "hc-ui-physical-card__rule", children: [
1895
+ name && /* @__PURE__ */ jsxs("h3", { children: [
1896
+ icon,
1897
+ /* @__PURE__ */ jsx("span", { children: name })
1898
+ ] }),
1899
+ /* @__PURE__ */ jsx("p", { className: "hc-ui-physical-card__copy", onClick: onCopy, children: text })
1900
+ ] });
1901
+ }
1902
+ function CardFrame({ side, label, children, legacySrc, backgroundSrc, away = false }) {
1903
+ const tilt = useCardTilt();
1904
+ return /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card-slot", "data-side": side, "data-away": flag(away), ...tilt, children: /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card-tilt", children: /* @__PURE__ */ jsxs("article", { className: "hc-ui-physical-card", "data-side": side, "aria-label": label, children: [
1905
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__bg", "data-side": side, src: backgroundSrc, alt: "", "aria-hidden": true, draggable: false }),
1906
+ legacySrc && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__legacy", src: legacySrc, alt: "", "aria-hidden": true, draggable: false }),
1907
+ children,
1908
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-physical-card__shine", "aria-hidden": true })
1909
+ ] }) }) });
1910
+ }
1911
+ function useLongPress(onLongPress, enabled) {
1912
+ const press = useRef(null);
1913
+ const cancel = () => {
1914
+ if (press.current) window.clearTimeout(press.current.timer);
1915
+ press.current = null;
1916
+ };
1917
+ useEffect(() => cancel, []);
1918
+ if (!enabled) return {};
1919
+ return {
1920
+ onPointerDown: (event) => {
1921
+ if (event.pointerType !== "touch" || !event.isPrimary) return;
1922
+ cancel();
1923
+ const timer = window.setTimeout(() => {
1924
+ press.current = null;
1925
+ navigator.vibrate?.(10);
1926
+ onLongPress();
1927
+ }, LONG_PRESS_MS);
1928
+ press.current = { timer, x: event.clientX, y: event.clientY };
1929
+ },
1930
+ onPointerMove: (event) => {
1931
+ const start = press.current;
1932
+ if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) > LONG_PRESS_SLOP) cancel();
1933
+ },
1934
+ onPointerUp: cancel,
1935
+ onPointerCancel: cancel,
1936
+ // On Android, holding would open the image's menu over the focus view.
1937
+ onContextMenu: (event) => {
1938
+ if (press.current) event.preventDefault();
1939
+ }
1940
+ };
1941
+ }
1942
+ function PhysicalCardFocusView({ label, onClose, children }) {
1943
+ const ref = useRef(null);
1944
+ const released = useRef(false);
1945
+ useLayoutEffect(() => {
1946
+ const dialog = ref.current;
1947
+ if (!dialog) return;
1948
+ dialog.showModal();
1949
+ const release = () => window.setTimeout(() => {
1950
+ released.current = true;
1951
+ }, 0);
1952
+ window.addEventListener("pointerup", release, { once: true });
1953
+ window.addEventListener("pointercancel", release, { once: true });
1954
+ window.addEventListener("touchend", requestMotionAccess);
1955
+ return () => {
1956
+ window.removeEventListener("touchend", requestMotionAccess);
1957
+ window.removeEventListener("pointerup", release);
1958
+ window.removeEventListener("pointercancel", release);
1959
+ dialog.close();
1960
+ };
1961
+ }, []);
1962
+ const closeOutside = (event) => {
1963
+ const target = event.target;
1964
+ if (released.current && (target === event.currentTarget || target.classList.contains("hc-ui-physical-cards") && target.hasAttribute("data-focus"))) onClose();
1965
+ };
1966
+ return /* @__PURE__ */ jsx("dialog", { ref, className: "hc-ui-card-focus", "aria-label": label, onClick: closeOutside, onCancel: (event) => {
1967
+ event.preventDefault();
1968
+ onClose();
1969
+ }, children });
1970
+ }
1971
+ var PhysicalCardView = forwardRef(function PhysicalCardView2({
1972
+ images,
1973
+ layout,
1974
+ title,
1975
+ gearSrc,
1976
+ headerLines,
1977
+ teamAbilities,
1978
+ teamAbilityIcon,
1979
+ collection,
1980
+ dimensions,
1981
+ token,
1982
+ bystander,
1983
+ keyphrases,
1984
+ relic,
1985
+ rules,
1986
+ contentKey,
1987
+ improved,
1988
+ dial,
1989
+ terrainDial,
1990
+ points,
1991
+ onSelectPoint,
1992
+ backBar,
1993
+ labels,
1994
+ onCopy,
1995
+ fitHeight = false,
1996
+ fitHeightWithin,
1997
+ focus = false,
1998
+ onRequestFocus,
1999
+ focusActions,
2000
+ tip,
2001
+ onTipChange: setTip,
2002
+ tipId,
2003
+ tipSlot
2004
+ }, forwardedRef) {
2005
+ const { ref: wrapRef, scale } = useCardScale(fitHeight, focus, fitHeightWithin);
2006
+ useDeviceTilt(wrapRef, focus);
2007
+ const longPress = useLongPress(() => onRequestFocus?.(), !focus && !!onRequestFocus);
2008
+ const [side, setSide] = useState(0);
2009
+ const name = useFitText(title, 28, 12);
2010
+ const setWrapRef = useCallback((element) => {
2011
+ wrapRef.current = element;
2012
+ if (typeof forwardedRef === "function") forwardedRef(element);
2013
+ else if (forwardedRef) forwardedRef.current = element;
2014
+ }, [wrapRef, forwardedRef]);
2015
+ const tipProps = ({ key, tip: content }) => ({
2016
+ "aria-haspopup": "dialog",
2017
+ "aria-expanded": tip?.key === key && tip.mode === "dialog",
2018
+ "aria-describedby": tip?.key === key && tip.mode === "hover" ? tipId : void 0,
2019
+ onPointerEnter: (event) => {
2020
+ if (event.pointerType === "mouse" && tip?.mode !== "dialog") setTip({ ...content, key, anchor: { x: event.clientX, y: event.clientY }, mode: "hover", trigger: event.currentTarget });
2021
+ },
2022
+ onPointerMove: (event) => {
2023
+ if (event.pointerType === "mouse") setTip((value) => value?.key === key && value.mode === "hover" ? { ...value, anchor: { x: event.clientX, y: event.clientY } } : value);
2024
+ },
2025
+ onPointerLeave: () => setTip((value) => value?.mode === "hover" ? null : value),
2026
+ onBlur: () => setTip((value) => value?.mode === "hover" ? null : value),
2027
+ onClick: (event) => {
2028
+ event.stopPropagation();
2029
+ const rect = event.currentTarget.getBoundingClientRect();
2030
+ setTip({ ...content, key, anchor: { x: event.detail ? event.clientX : rect.left, y: event.detail ? event.clientY : rect.bottom, preferAbove: true }, mode: "dialog", trigger: event.currentTarget });
2031
+ }
2032
+ });
2033
+ const zeros = () => rules.map(() => 0);
2034
+ const [distribution, setDistribution] = useState(() => ({ key: contentKey, pages: zeros() }));
2035
+ const pages = distribution.key === contentKey && distribution.pages.length === rules.length ? distribution.pages : zeros();
2036
+ const tables = useRef([]);
2037
+ useLayoutEffect(() => {
2038
+ const next = nextDistribution(pages, (table) => {
2039
+ const element = tables.current[table];
2040
+ return !!element && element.scrollHeight > element.clientHeight + 1;
2041
+ });
2042
+ if (next) setDistribution({ key: contentKey, pages: next });
2043
+ });
2044
+ const tableRef = (index) => (element) => {
2045
+ tables.current[index] = element;
2046
+ };
2047
+ const renderRules = (table) => rules.map((rule, index) => pages[index] !== table ? null : /* @__PURE__ */ jsx(
2048
+ CardRule,
2049
+ {
2050
+ name: rule.name,
2051
+ text: rule.text,
2052
+ icon: rule.icon,
2053
+ onCopy: () => onCopy(rule.copyText)
2054
+ },
2055
+ index
2056
+ ));
2057
+ const tokenClass = "hc-ui-physical-card__token";
2058
+ const tokenNode = !token ? null : token.kind === "portrait" ? token.render(tokenClass) : token.kind === "terrain" ? /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__terrain-art", src: token.src, alt: token.alt, loading: "lazy" }) : /* @__PURE__ */ jsx("span", { className: tokenClass, "data-object": "", style: token.color ? { "--hc-card-object-color": token.color } : void 0, children: token.src && /* @__PURE__ */ jsx("img", { src: token.src, alt: token.alt, loading: "lazy", onError: (event) => {
2059
+ event.currentTarget.hidden = true;
2060
+ } }) });
2061
+ const objectLines = keyphrases ? /* @__PURE__ */ jsxs(Fragment2, { children: [
2062
+ keyphrases.map((line) => /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__keyphrase", children: line.content }, line.key)),
2063
+ relic && /* @__PURE__ */ jsxs("p", { className: "hc-ui-physical-card__relic", children: [
2064
+ /* @__PURE__ */ jsxs("b", { children: [
2065
+ relic.label,
2066
+ ":"
2067
+ ] }),
2068
+ " ",
2069
+ relic.value
2070
+ ] })
2071
+ ] }) : null;
2072
+ const improvedGroups = improved.map((group) => /* @__PURE__ */ jsxs("span", { className: "hc-ui-physical-card__improved-group", children: [
2073
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__improved-kind", src: group.kindIconSrc, alt: group.kindLabel }),
2074
+ group.abilities.map((ability) => /* @__PURE__ */ jsx(
2075
+ "button",
2076
+ {
2077
+ type: "button",
2078
+ className: "hc-ui-physical-card__improved",
2079
+ "aria-label": ability.label,
2080
+ ...tipProps(ability),
2081
+ children: /* @__PURE__ */ jsx("img", { src: ability.iconSrc, alt: "" })
2082
+ },
2083
+ ability.key
2084
+ ))
2085
+ ] }, group.kind));
2086
+ const pointValues = points.kind === "none" ? null : points.kind === "object" ? /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__object-points", children: [
2087
+ points.values,
2088
+ " ",
2089
+ points.suffix
2090
+ ] }) : /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__points", "data-table": points.kind, role: points.items.length > 1 ? "group" : void 0, "aria-label": points.label, children: points.items.map((point) => /* @__PURE__ */ jsx(
2091
+ "button",
2092
+ {
2093
+ type: "button",
2094
+ className: "hc-ui-physical-card__point",
2095
+ "data-active": flag(point.index === points.selectedIndex),
2096
+ style: { color: point.color },
2097
+ disabled: points.items.length < 2,
2098
+ "aria-pressed": points.items.length > 1 ? point.index === points.selectedIndex : void 0,
2099
+ "aria-label": point.label,
2100
+ onClick: () => onSelectPoint(point),
2101
+ children: point.value
2102
+ },
2103
+ point.index
2104
+ )) });
2105
+ const terrainDialNode = terrainDial && /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__terrain-dial", children: [
2106
+ /* @__PURE__ */ jsxs("span", { children: [
2107
+ /* @__PURE__ */ jsx("img", { src: terrainDial.icons.range.src, alt: terrainDial.icons.range.alt }),
2108
+ " / ",
2109
+ /* @__PURE__ */ jsx("img", { src: terrainDial.icons.giantReach.src, alt: terrainDial.icons.giantReach.alt })
2110
+ ] }),
2111
+ /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx("img", { src: terrainDial.icons.damage.src, alt: terrainDial.icons.damage.alt }) }),
2112
+ /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx("img", { src: terrainDial.icons.destroy.src, alt: terrainDial.icons.destroy.alt }) }),
2113
+ /* @__PURE__ */ jsx("b", { children: terrainDial.values[0] }),
2114
+ /* @__PURE__ */ jsx("b", { children: terrainDial.values[1] }),
2115
+ /* @__PURE__ */ jsx("b", { children: terrainDial.values[2] })
2116
+ ] });
2117
+ const teamAbilityButtons = teamAbilities.map((ability) => /* @__PURE__ */ jsxs(
2118
+ "button",
2119
+ {
2120
+ type: "button",
2121
+ className: "hc-ui-physical-card__ability",
2122
+ "aria-label": ability.label,
2123
+ ...tipProps(ability),
2124
+ children: [
2125
+ teamAbilityIcon,
2126
+ /* @__PURE__ */ jsx("img", { src: ability.iconSrc, alt: "", onError: (event) => {
2127
+ event.currentTarget.hidden = true;
2128
+ } })
2129
+ ]
2130
+ },
2131
+ ability.key
2132
+ ));
2133
+ const bystanderToken = bystander && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander", children: /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-token", children: [
2134
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-clip", children: [
2135
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-art", children: /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-window", children: bystander.art && /* @__PURE__ */ jsx("img", { src: bystander.art.src, alt: bystander.art.alt, loading: "lazy", onError: (event) => {
2136
+ event.currentTarget.hidden = true;
2137
+ } }) }) }),
2138
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-stripe", "aria-hidden": true }),
2139
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-symbols", children: [
2140
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-tab", "data-stat": "range", "aria-label": bystander.rangeLabel, role: "img", children: [
2141
+ bystander.range != null && /* @__PURE__ */ jsx("span", { children: bystander.range }),
2142
+ Array.from({ length: bystander.targets }, (_, index) => /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__bystander-bolt", src: bystander.boltSrc, alt: "", style: { left: 18 + index * 4 } }, index))
2143
+ ] }),
2144
+ PHYSICAL_CARD_STATS.map((type) => bystander.symbols[type] && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-tab", "data-stat": type, children: /* @__PURE__ */ jsx("img", { src: bystander.symbols[type], alt: "" }) }, type))
2145
+ ] })
2146
+ ] }),
2147
+ bystander.improved.length > 0 && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-improved", children: bystander.improved.map(({ kind, iconSrc, label, ability }, index) => {
2148
+ const angle = 2 * Math.PI / 22 * index;
2149
+ const position = { left: Math.cos(angle) * 90 - 9, top: Math.sin(angle) * 90 - 9 };
2150
+ if (!ability) return /* @__PURE__ */ jsx("img", { src: iconSrc, alt: label, style: position }, `${kind}-kind`);
2151
+ return /* @__PURE__ */ jsx(
2152
+ "button",
2153
+ {
2154
+ type: "button",
2155
+ className: "hc-ui-physical-card__bystander-improved-ability",
2156
+ style: position,
2157
+ "aria-label": ability.label,
2158
+ ...tipProps(ability),
2159
+ children: /* @__PURE__ */ jsx("img", { src: ability.iconSrc, alt: "" })
2160
+ },
2161
+ `${kind}-${ability.key}`
2162
+ );
2163
+ }) }),
2164
+ (bystander.trait || teamAbilityButtons.length > 0) && /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-abilities", children: [
2165
+ bystander.trait && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__bystander-trait", src: bystander.trait.src, alt: bystander.trait.alt }),
2166
+ teamAbilityButtons
2167
+ ] }),
2168
+ bystander.extra && /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-extra", "data-kind": bystander.extra.kind, children: [
2169
+ /* @__PURE__ */ jsx("img", { src: bystander.extra.src, alt: bystander.extra.alt }),
2170
+ bystander.extra.kind !== "construct" && /* @__PURE__ */ jsx("span", { children: bystander.extra.text })
2171
+ ] }),
2172
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-name", children: bystander.name }),
2173
+ bystander.dial && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-dial", role: "group", "aria-label": bystander.dial.label, children: bystander.dial.cells.map((cell, row) => {
2174
+ const position = { left: row * BYSTANDER_DIAL_STEP.x, top: row * BYSTANDER_DIAL_STEP.y };
2175
+ const cellStyle = { ...position, backgroundColor: cell.background, color: cell.color };
2176
+ const cellProps = { className: "hc-ui-physical-card__bystander-value", "data-special": flag(cell.special), style: cellStyle, "aria-label": cell.label };
2177
+ if (!cell.tip) return /* @__PURE__ */ jsx("span", { ...cellProps, role: "img", children: cell.value }, cell.stat);
2178
+ return /* @__PURE__ */ jsx("button", { type: "button", ...cellProps, "data-tip": "", ...tipProps(cell.tip), children: cell.value }, cell.stat);
2179
+ }) })
2180
+ ] }) });
2181
+ const backs = backCount(pages);
2182
+ const shownSide = Math.min(side, backs);
2183
+ const nextSide = (shownSide + 1) % (backs + 1);
2184
+ const style = { "--hc-card-scale": scale, "--hc-card-front-table-height": `${layout.frontTableHeight}px`, "--hc-card-back-table-height": `${BACK_TABLE_HEIGHT}px`, "--hc-card-width": `${CARD_WIDTH}px`, "--hc-card-height": `${CARD_HEIGHT}px` };
2185
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-cards", "data-focus": flag(focus), ref: setWrapRef, style, ...longPress, children: [
2186
+ /* @__PURE__ */ jsxs(CardFrame, { side: "front", backgroundSrc: images.front, legacySrc: images.legacy, label: labels.front, away: focus && shownSide !== 0, children: [
2187
+ layout.teamUp && /* @__PURE__ */ jsx("span", { className: "hc-ui-physical-card__team-up", "aria-hidden": true }),
2188
+ layout.rarityImage && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__rarity", src: layout.rarityImage, alt: "", "aria-hidden": true, draggable: false }),
2189
+ layout.dialImage && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__dial-bg", "data-table": layout.frontTable, src: layout.dialImage, alt: "", "aria-hidden": true, draggable: false }),
2190
+ /* @__PURE__ */ jsxs("header", { className: "hc-ui-physical-card__header", "data-team-up": flag(layout.teamUp), children: [
2191
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__title", ref: name.ref, children: [
2192
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: name.size }, children: title }),
2193
+ gearSrc && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__gear", src: gearSrc, alt: "" }),
2194
+ !bystander && teamAbilityButtons
2195
+ ] }),
2196
+ headerLines.map((line, index) => /* @__PURE__ */ jsx("p", { className: "hc-ui-physical-card__header-line", "data-kind": line.kind, children: line.text }, index))
2197
+ ] }),
2198
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__collection", children: [
2199
+ /* @__PURE__ */ jsx("strong", { children: collection.number }),
2200
+ /* @__PURE__ */ jsx(SetIconView, { sources: collection.setSources, title: collection.setTitle, className: "hc-ui-physical-card__set" })
2201
+ ] }),
2202
+ dimensions && /* @__PURE__ */ jsx("span", { className: "hc-ui-physical-card__dimensions", "data-oval": flag(dimensions.oval), children: dimensions.text }),
2203
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__table", "data-column": "front", "data-table": layout.frontTable, "data-small-token": flag(layout.smallToken), ref: tableRef(0), children: [
2204
+ tokenNode,
2205
+ bystanderToken,
2206
+ objectLines,
2207
+ renderRules(0)
2208
+ ] }),
2209
+ improved.length > 0 && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__improved-row", style: { top: layout.improvedTop }, children: improvedGroups }),
2210
+ dial && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__dial", "data-tables": dial.tables, children: dial.node }),
2211
+ terrainDialNode,
2212
+ pointValues
2213
+ ] }),
2214
+ Array.from({ length: backs }, (_, back) => /* @__PURE__ */ jsxs(CardFrame, { side: "back", backgroundSrc: images.back, legacySrc: images.legacy, label: labels.back(back, backs), away: focus && shownSide !== back + 1, children: [
2215
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__table", "data-column": "left", ref: tableRef(back * 2 + 1), children: renderRules(back * 2 + 1) }),
2216
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__table", "data-column": "right", ref: tableRef(back * 2 + 2), children: renderRules(back * 2 + 2) }),
2217
+ back === backs - 1 && /* @__PURE__ */ jsxs("footer", { className: "hc-ui-physical-card__back-bar", children: [
2218
+ backBar.realName && /* @__PURE__ */ jsxs("p", { children: [
2219
+ /* @__PURE__ */ jsxs("b", { children: [
2220
+ backBar.realName.label,
2221
+ ":"
2222
+ ] }),
2223
+ " ",
2224
+ backBar.realName.value
2225
+ ] }),
2226
+ backBar.source && /* @__PURE__ */ jsxs("p", { children: [
2227
+ /* @__PURE__ */ jsxs("b", { children: [
2228
+ backBar.source.label,
2229
+ ":"
2230
+ ] }),
2231
+ " ",
2232
+ backBar.source.value
2233
+ ] })
2234
+ ] }),
2235
+ back !== backs - 1 && /* @__PURE__ */ jsx("footer", { className: "hc-ui-physical-card__back-bar", "aria-hidden": true })
2236
+ ] }, back)),
2237
+ focus && /* @__PURE__ */ jsx("div", { className: "hc-ui-card-focus__actions", children: focusActions?.({ nextSide, backs, flip: () => setSide(nextSide) }) }),
2238
+ tip && tipSlot
2239
+ ] });
2240
+ });
2241
+ var TILE_SIZE = 64;
2242
+ var EDGE_SIZE = TILE_SIZE / 16;
2243
+ var WALL_SIZE = EDGE_SIZE * 2;
2244
+ var MAX_EDGES = 4;
2245
+ var HINDERING_TILE_COLOR = "#B4ECB4";
2246
+ var BLOCKING_TILE_COLOR = "#c9b8aa";
2247
+ var WATER_TILE_COLOR = "#dee8eb";
2248
+ var OBSCURING_TILE_COLOR = "#E8E8E8";
2249
+ var WINDOW_TILE_COLOR = "#dee8eb";
2250
+ var SPECIAL_TILE_COLOR = "#FFD394";
2251
+ var STARTING_ZONE_TILE_COLOR = "#fff8fa";
2252
+ var ELEVATION_TILE_COLORS = ["#FFFFFF", "#FB7A7F", "#D0424C", "#B02020", "#902020", "#702020"];
2253
+ var TERRAIN_TILE_COLORS = {
2254
+ clear: ELEVATION_TILE_COLORS[0],
2255
+ hindering: HINDERING_TILE_COLOR,
2256
+ blocking: BLOCKING_TILE_COLOR,
2257
+ water: WATER_TILE_COLOR,
2258
+ obscuring: OBSCURING_TILE_COLOR,
2259
+ window: WINDOW_TILE_COLOR,
2260
+ special: SPECIAL_TILE_COLOR,
2261
+ special2: SPECIAL_TILE_COLOR
2262
+ };
2263
+ var TERRAIN_EDGE_COLORS = {
2264
+ clear: "#FFFFFF",
2265
+ hindering: "#3ACE3A",
2266
+ blocking: "#76491E",
2267
+ water: "#1FA9E1",
2268
+ obscuring: "#BBBBBB",
2269
+ window: "#1FA9E1",
2270
+ special: "#FF7600",
2271
+ special2: "#FF7600"
2272
+ };
2273
+ var STARTING_ZONE_EDGE_COLOR = "#D560A9";
2274
+ var ELEVATION_EDGE_COLOR = "#FF0000";
2275
+ var INDOOR_EDGE_COLOR = "#FFFF00";
2276
+ var WALL_COLORS = {
2277
+ normal: "#000000",
2278
+ glass: "#E59400",
2279
+ glass2: "#33BBFF",
2280
+ door: "#000000",
2281
+ door2: "#DDDDDD",
2282
+ forceField: "#E59400",
2283
+ chainLinkFence: "#555555",
2284
+ levelSeparator: "#E5E500"
2285
+ };
2286
+ var DASHED_WALLS = /* @__PURE__ */ new Set(["glass", "glass2", "door", "door2", "levelSeparator"]);
2287
+ var Tile = class {
2288
+ constructor(x, y, t, hideLabel) {
2289
+ __publicField(this, "x");
2290
+ __publicField(this, "y");
2291
+ __publicField(this, "terrain");
2292
+ __publicField(this, "type");
2293
+ __publicField(this, "elevation");
2294
+ __publicField(this, "isStartingZone");
2295
+ __publicField(this, "isStartingZone4p");
2296
+ __publicField(this, "group");
2297
+ __publicField(this, "label");
2298
+ __publicField(this, "hideLabel");
2299
+ __publicField(this, "edges", new Array(4 * MAX_EDGES).fill(null));
2300
+ __publicField(this, "ramps", [false, false, false, false]);
2301
+ __publicField(this, "fillStyle");
2302
+ this.x = x;
2303
+ this.y = y;
2304
+ this.terrain = t.terrain ?? "clear";
2305
+ this.type = t.type;
2306
+ this.elevation = t.elevation ?? 1;
2307
+ this.isStartingZone = t.isStartingZone;
2308
+ this.isStartingZone4p = t.isStartingZone4p;
2309
+ this.group = t.group ?? 0;
2310
+ this.label = t.label;
2311
+ this.hideLabel = hideLabel;
2312
+ if (this.terrain === "hindering") this.fillStyle = HINDERING_TILE_COLOR;
2313
+ else if (this.terrain === "blocking") this.fillStyle = BLOCKING_TILE_COLOR;
2314
+ else if (this.terrain === "water") this.fillStyle = WATER_TILE_COLOR;
2315
+ else if (this.terrain === "obscuring") this.fillStyle = OBSCURING_TILE_COLOR;
2316
+ else if (this.terrain === "window") this.fillStyle = WINDOW_TILE_COLOR;
2317
+ else if (this.terrain === "special" || this.terrain === "special2") this.fillStyle = SPECIAL_TILE_COLOR;
2318
+ else if (this.isStartingZone || this.isStartingZone4p) this.fillStyle = STARTING_ZONE_TILE_COLOR;
2319
+ else if (typeof this.elevation === "string") this.fillStyle = ELEVATION_TILE_COLORS[1];
2320
+ else this.fillStyle = ELEVATION_TILE_COLORS[this.elevation - 1] ?? ELEVATION_TILE_COLORS[0];
2321
+ }
2322
+ addEdge(edgeType, strokeStyle, dashed) {
2323
+ while (this.edges[edgeType]) edgeType += 4;
2324
+ if (edgeType < this.edges.length) this.edges[edgeType] = { strokeStyle, dashed };
2325
+ }
2326
+ hasEdge(edgeType) {
2327
+ while (edgeType >= 0) {
2328
+ if (this.edges[edgeType]) return true;
2329
+ edgeType -= 4;
2330
+ }
2331
+ return false;
2332
+ }
2333
+ draw(ctx) {
2334
+ ctx.fillStyle = this.fillStyle;
2335
+ ctx.fillRect(this.x * TILE_SIZE, this.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
2336
+ }
2337
+ drawEdges(ctx) {
2338
+ const edgeLevels = [-1, -1, -1, -1];
2339
+ for (let i = 0; i < MAX_EDGES; i++) {
2340
+ for (let j = 0; j < 4; j++) {
2341
+ if (this.edges[4 * i + j]) edgeLevels[j] = i;
2342
+ }
2343
+ }
2344
+ for (let i = 0; i < 4; i++) {
2345
+ this.drawEdge(ctx, 0 /* LEFT */, edgeLevels[0 /* LEFT */]--);
2346
+ this.drawEdge(ctx, 1 /* RIGHT */, edgeLevels[1 /* RIGHT */]--);
2347
+ this.drawEdge(ctx, 2 /* TOP */, edgeLevels[2 /* TOP */]--);
2348
+ this.drawEdge(ctx, 3 /* BOTTOM */, edgeLevels[3 /* BOTTOM */]--);
2349
+ }
2350
+ this.drawRamps(ctx);
2351
+ this.drawLabel(ctx);
2352
+ }
2353
+ drawEdge(ctx, edgeType, level) {
2354
+ if (edgeType < 0 || level < 0) return;
2355
+ const edge = this.edges[edgeType + level * 4];
2356
+ if (!edge) return;
2357
+ let x0 = 0;
2358
+ let y0 = 0;
2359
+ let x1 = 0;
2360
+ let y1 = 0;
2361
+ let lineDashOffset = 0;
2362
+ const overhangSize = EDGE_SIZE * (level + 1);
2363
+ const oLeft = this.hasEdge(0 /* LEFT */ + level * 4) ? 0 : overhangSize;
2364
+ const oRight = this.hasEdge(1 /* RIGHT */ + level * 4) ? 0 : overhangSize;
2365
+ const oTop = this.hasEdge(2 /* TOP */ + level * 4) ? 0 : overhangSize;
2366
+ const oBottom = this.hasEdge(3 /* BOTTOM */ + level * 4) ? 0 : overhangSize;
2367
+ switch (edgeType) {
2368
+ case 0 /* LEFT */:
2369
+ x0 = x1 = this.x * TILE_SIZE + EDGE_SIZE / 2 + EDGE_SIZE * level;
2370
+ y0 = this.y * TILE_SIZE - oTop;
2371
+ y1 = (this.y + 1) * TILE_SIZE + oBottom;
2372
+ lineDashOffset = oTop;
2373
+ break;
2374
+ case 1 /* RIGHT */:
2375
+ x0 = x1 = (this.x + 1) * TILE_SIZE - EDGE_SIZE / 2 - EDGE_SIZE * level;
2376
+ y0 = this.y * TILE_SIZE - oTop;
2377
+ y1 = (this.y + 1) * TILE_SIZE + oBottom;
2378
+ lineDashOffset = oTop;
2379
+ break;
2380
+ case 2 /* TOP */:
2381
+ x0 = this.x * TILE_SIZE - oLeft;
2382
+ y0 = y1 = this.y * TILE_SIZE + EDGE_SIZE / 2 + EDGE_SIZE * level;
2383
+ x1 = (this.x + 1) * TILE_SIZE + oRight;
2384
+ lineDashOffset = oLeft;
2385
+ break;
2386
+ case 3 /* BOTTOM */:
2387
+ x0 = this.x * TILE_SIZE - oLeft;
2388
+ y0 = y1 = (this.y + 1) * TILE_SIZE - EDGE_SIZE / 2 - EDGE_SIZE * level;
2389
+ x1 = (this.x + 1) * TILE_SIZE + oRight;
2390
+ lineDashOffset = oLeft;
2391
+ break;
2392
+ }
2393
+ ctx.strokeStyle = edge.strokeStyle;
2394
+ ctx.lineWidth = EDGE_SIZE;
2395
+ if (edge.dashed) {
2396
+ ctx.setLineDash([EDGE_SIZE, EDGE_SIZE]);
2397
+ ctx.lineDashOffset = lineDashOffset;
2398
+ }
2399
+ ctx.beginPath();
2400
+ ctx.moveTo(x0, y0);
2401
+ ctx.lineTo(x1, y1);
2402
+ ctx.stroke();
2403
+ ctx.setLineDash([]);
2404
+ }
2405
+ drawRamps(ctx) {
2406
+ if (this.ramps[1 /* RIGHT */]) this.drawRamp(ctx, Math.PI * 3 / 2);
2407
+ if (this.ramps[0 /* LEFT */]) this.drawRamp(ctx, Math.PI / 2);
2408
+ if (this.ramps[2 /* TOP */]) this.drawRamp(ctx, Math.PI);
2409
+ if (this.ramps[3 /* BOTTOM */]) this.drawRamp(ctx, 0);
2410
+ }
2411
+ drawRamp(ctx, angle) {
2412
+ ctx.save();
2413
+ ctx.translate(this.x * TILE_SIZE + TILE_SIZE / 2, this.y * TILE_SIZE + TILE_SIZE / 2);
2414
+ ctx.rotate(angle);
2415
+ ctx.beginPath();
2416
+ ctx.moveTo(0, EDGE_SIZE);
2417
+ ctx.lineTo(-(TILE_SIZE / 2 - EDGE_SIZE * 2), TILE_SIZE / 2 - EDGE_SIZE);
2418
+ ctx.lineTo(TILE_SIZE / 2 - EDGE_SIZE * 2, TILE_SIZE / 2 - EDGE_SIZE);
2419
+ ctx.closePath();
2420
+ ctx.lineWidth = EDGE_SIZE * 2;
2421
+ ctx.strokeStyle = ELEVATION_EDGE_COLOR;
2422
+ ctx.stroke();
2423
+ ctx.fillStyle = ELEVATION_TILE_COLORS[1];
2424
+ ctx.fill();
2425
+ ctx.font = "18px Arial";
2426
+ ctx.fillStyle = "#000000";
2427
+ ctx.textAlign = "center";
2428
+ ctx.textBaseline = "middle";
2429
+ ctx.fillText(String(this.elevation), 0, TILE_SIZE / 4 + 4);
2430
+ ctx.restore();
2431
+ }
2432
+ drawLabel(ctx) {
2433
+ if (this.hideLabel || this.x !== 0 && this.y !== 0 && !this.label) return;
2434
+ let text = "";
2435
+ ctx.save();
2436
+ ctx.translate(this.x * TILE_SIZE + TILE_SIZE / 2, this.y * TILE_SIZE + TILE_SIZE / 2);
2437
+ if (this.label) {
2438
+ text = this.label.text;
2439
+ ctx.fillStyle = this.label.color ?? ELEVATION_EDGE_COLOR;
2440
+ ctx.rotate((this.label.rotate ?? 0) * Math.PI / 180);
2441
+ } else {
2442
+ if (this.y === 0) text = String.fromCharCode("A".charCodeAt(0) + this.x);
2443
+ if (this.x === 0) text += this.y + 1;
2444
+ ctx.fillStyle = "#000000";
2445
+ }
2446
+ ctx.font = "30px Arial";
2447
+ ctx.textAlign = "center";
2448
+ ctx.textBaseline = "middle";
2449
+ ctx.fillText(text, 0, 0);
2450
+ ctx.restore();
2451
+ }
2452
+ };
2453
+ var HCMap = class {
2454
+ constructor(json) {
2455
+ __publicField(this, "width");
2456
+ __publicField(this, "height");
2457
+ __publicField(this, "type");
2458
+ __publicField(this, "tiles", []);
2459
+ __publicField(this, "walls");
2460
+ __publicField(this, "ziplines");
2461
+ this.width = json.width;
2462
+ this.height = json.height;
2463
+ this.type = json.type ?? "outdoor";
2464
+ const hideLabels = json.hideLabels ?? false;
2465
+ for (let y = 0; y < this.height; y++) {
2466
+ for (let x = 0; x < this.width; x++) {
2467
+ const jt = { ...json.rows[y]?.tiles[x] ?? {} };
2468
+ if (!jt.terrain && json.defaultTerrain) jt.terrain = json.defaultTerrain;
2469
+ if (!jt.elevation && json.defaultElevation) jt.elevation = json.defaultElevation;
2470
+ if (!jt.type) jt.type = json.defaultType ?? this.type;
2471
+ this.tiles.push(new Tile(x, y, jt, hideLabels));
2472
+ }
2473
+ }
2474
+ for (let y = 0; y < this.height; y++) {
2475
+ for (let x = 0; x < this.width; x++) {
2476
+ const tile = this.getTile(x, y);
2477
+ this.tryAddEdge(tile, this.getTile(x - 1, y), 0 /* LEFT */);
2478
+ this.tryAddEdge(tile, this.getTile(x + 1, y), 1 /* RIGHT */);
2479
+ this.tryAddEdge(tile, this.getTile(x, y - 1), 2 /* TOP */);
2480
+ this.tryAddEdge(tile, this.getTile(x, y + 1), 3 /* BOTTOM */);
2481
+ }
2482
+ }
2483
+ for (const ramp of json.ramps ?? []) {
2484
+ let { x0, y0, x1, y1 } = ramp;
2485
+ if (x0 > x1 || y0 > y1) {
2486
+ [x0, x1] = [x1, x0];
2487
+ [y0, y1] = [y1, y0];
2488
+ }
2489
+ if (x0 < x1) {
2490
+ if (x0 >= 0 && x0 < this.width) this.tiles[y0 * this.width + x0].ramps[1 /* RIGHT */] = true;
2491
+ if (x1 >= 0 && x1 < this.width) this.tiles[y1 * this.width + x1].ramps[0 /* LEFT */] = true;
2492
+ } else {
2493
+ if (y0 >= 0 && y0 < this.height) this.tiles[y0 * this.width + x0].ramps[3 /* BOTTOM */] = true;
2494
+ if (y1 >= 0 && y1 < this.height) this.tiles[y1 * this.width + x1].ramps[2 /* TOP */] = true;
2495
+ }
2496
+ }
2497
+ this.walls = (json.walls ?? []).map((w) => ({ ...w, type: w.type ?? json.defaultWallType ?? "normal" }));
2498
+ this.ziplines = json.ziplines ?? [];
2499
+ }
2500
+ getTile(x, y) {
2501
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return null;
2502
+ return this.tiles[y * this.width + x];
2503
+ }
2504
+ tryAddEdge(t0, t1, edgeType) {
2505
+ if (this.type === "indoorOutdoor" && t0.type === "indoor" && (t1 == null || t1.type === "outdoor")) {
2506
+ t0.addEdge(edgeType, INDOOR_EDGE_COLOR, false);
2507
+ }
2508
+ if (typeof t0.elevation === "number" && t0.elevation > 1 && (t1 == null || t0.elevation !== t1.elevation)) {
2509
+ t0.addEdge(edgeType, ELEVATION_EDGE_COLOR, false);
2510
+ }
2511
+ if (t0.isStartingZone && (t1 == null || !t1.isStartingZone)) {
2512
+ t0.addEdge(edgeType, STARTING_ZONE_EDGE_COLOR, false);
2513
+ }
2514
+ if (t0.isStartingZone4p && (t1 == null || !t1.isStartingZone4p)) {
2515
+ t0.addEdge(edgeType, STARTING_ZONE_EDGE_COLOR, true);
2516
+ }
2517
+ if (t0.terrain !== "clear" && (t1 == null || t0.terrain !== t1.terrain || t0.type !== t1.type || t0.elevation !== t1.elevation || t0.isStartingZone !== t1.isStartingZone || t0.isStartingZone4p !== t1.isStartingZone4p || t0.group !== t1.group)) {
2518
+ const dashed = t0.terrain === "window" || t0.terrain === "special2";
2519
+ t0.addEdge(edgeType, TERRAIN_EDGE_COLORS[t0.terrain] ?? "#FFFFFF", dashed);
2520
+ }
2521
+ }
2522
+ draw(ctx) {
2523
+ ctx.save();
2524
+ for (const tile of this.tiles) tile.draw(ctx);
2525
+ ctx.restore();
2526
+ ctx.save();
2527
+ for (const tile of this.tiles) tile.drawEdges(ctx);
2528
+ ctx.restore();
2529
+ ctx.save();
2530
+ ctx.strokeStyle = "#222222";
2531
+ ctx.lineWidth = 1;
2532
+ for (let y = 0; y < this.height; y++) {
2533
+ for (let x = 0; x < this.width; x++) {
2534
+ ctx.strokeRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
2535
+ }
2536
+ }
2537
+ ctx.restore();
2538
+ for (const wall of this.walls) {
2539
+ ctx.save();
2540
+ this.drawWall(ctx, wall);
2541
+ ctx.restore();
2542
+ }
2543
+ for (const z of this.ziplines) {
2544
+ ctx.save();
2545
+ ctx.strokeStyle = WALL_COLORS.normal;
2546
+ ctx.lineCap = "round";
2547
+ ctx.lineWidth = 4;
2548
+ ctx.beginPath();
2549
+ ctx.moveTo(z.x0 * TILE_SIZE + TILE_SIZE / 2, z.y0 * TILE_SIZE + TILE_SIZE / 2);
2550
+ ctx.lineTo(z.x1 * TILE_SIZE + TILE_SIZE / 2, z.y1 * TILE_SIZE + TILE_SIZE / 2);
2551
+ ctx.stroke();
2552
+ ctx.restore();
2553
+ }
2554
+ }
2555
+ drawWall(ctx, wall) {
2556
+ const type = wall.type ?? "normal";
2557
+ ctx.lineWidth = WALL_SIZE;
2558
+ if (type === "levelSeparator") {
2559
+ ctx.strokeStyle = WALL_COLORS.normal;
2560
+ ctx.lineWidth = WALL_SIZE * 2;
2561
+ ctx.beginPath();
2562
+ ctx.moveTo(wall.x0 * TILE_SIZE, wall.y0 * TILE_SIZE);
2563
+ ctx.lineTo(wall.x1 * TILE_SIZE, wall.y1 * TILE_SIZE);
2564
+ ctx.stroke();
2565
+ ctx.lineWidth = WALL_SIZE;
2566
+ }
2567
+ ctx.strokeStyle = WALL_COLORS[type] ?? WALL_COLORS.normal;
2568
+ if (DASHED_WALLS.has(type)) {
2569
+ ctx.setLineDash([WALL_SIZE, WALL_SIZE]);
2570
+ ctx.lineDashOffset = WALL_SIZE / 2;
2571
+ } else {
2572
+ ctx.lineCap = "round";
2573
+ }
2574
+ ctx.beginPath();
2575
+ ctx.moveTo(wall.x0 * TILE_SIZE, wall.y0 * TILE_SIZE);
2576
+ ctx.lineTo(wall.x1 * TILE_SIZE, wall.y1 * TILE_SIZE);
2577
+ ctx.stroke();
2578
+ }
2579
+ };
2580
+ function mapSquare(map, x, y) {
2581
+ const tile = map.rows[y]?.tiles[x] || {};
2582
+ const environment = tile.type || map.defaultType || map.type || "outdoor";
2583
+ return {
2584
+ x,
2585
+ y,
2586
+ terrain: tile.terrain || map.defaultTerrain || "clear",
2587
+ elevation: tile.elevation ?? map.defaultElevation ?? 1,
2588
+ environment: environment === "indoor" || environment === "outdoor" ? environment : "unknown",
2589
+ starting: !!(tile.isStartingZone || tile.isStartingZone4p),
2590
+ label: tile.label?.text
2591
+ };
2592
+ }
2593
+ function elevationLevel(elevation) {
2594
+ const value = typeof elevation === "number" ? elevation : Number.parseInt(elevation, 10);
2595
+ return Number.isFinite(value) ? Math.max(1, value) : 2;
2596
+ }
2597
+ function mapMaxLevel(map) {
2598
+ let level = 1;
2599
+ for (let y = 0; y < map.height; y++) {
2600
+ for (let x = 0; x < map.width; x++) level = Math.max(level, elevationLevel(mapSquare(map, x, y).elevation));
2601
+ }
2602
+ return level;
2603
+ }
2604
+ function distanceToLine(x, y, line) {
2605
+ const dx = line.x1 - line.x0, dy = line.y1 - line.y0;
2606
+ const length2 = dx * dx + dy * dy;
2607
+ const t = length2 ? Math.max(0, Math.min(1, ((x - line.x0) * dx + (y - line.y0) * dy) / length2)) : 0;
2608
+ return Math.hypot(x - line.x0 - t * dx, y - line.y0 - t * dy);
2609
+ }
2610
+ function inspectMap(map, x, y, tolerance = 0.16) {
2611
+ if (x < 0 || y < 0 || x > map.width || y > map.height) return null;
2612
+ const cx2 = Math.min(map.width - 1, Math.floor(x)), cy = Math.min(map.height - 1, Math.floor(y));
2613
+ const square = mapSquare(map, cx2, cy);
2614
+ const wall = (map.walls || []).map((line) => ({ line, distance: distanceToLine(x, y, line) })).filter((item) => item.distance <= tolerance).sort((a, b) => a.distance - b.distance)[0]?.line;
2615
+ if (wall) return { square, wall: wall.type || map.defaultWallType || "normal", line: wall };
2616
+ const vertical = Math.abs(x - Math.round(x)), horizontal = Math.abs(y - Math.round(y));
2617
+ const alongX = vertical <= horizontal;
2618
+ const edge = Math.round(alongX ? x : y);
2619
+ if (Math.min(vertical, horizontal) <= tolerance) {
2620
+ const a = alongX ? { x: edge - 1, y: cy } : { x: cx2, y: edge - 1 };
2621
+ const b = alongX ? { x: edge, y: cy } : { x: cx2, y: edge };
2622
+ const valid = (p) => p.x >= 0 && p.y >= 0 && p.x < map.width && p.y < map.height;
2623
+ if (valid(a) && valid(b)) return {
2624
+ square: mapSquare(map, a.x, a.y),
2625
+ adjacent: mapSquare(map, b.x, b.y),
2626
+ line: alongX ? { x0: edge, y0: cy, x1: edge, y1: cy + 1 } : { x0: cx2, y0: edge, x1: cx2 + 1, y1: edge }
2627
+ };
2628
+ }
2629
+ return { square };
2630
+ }
2631
+ function squareCoordinate(square) {
2632
+ let index = square.x + 1, column = "";
2633
+ while (index > 0) {
2634
+ index--;
2635
+ column = String.fromCharCode(65 + index % 26) + column;
2636
+ index = Math.floor(index / 26);
2637
+ }
2638
+ return `${column}${square.y + 1}`;
2639
+ }
2640
+ var LEVEL_HEIGHT = 0.62;
2641
+ var BASE_Z = -0.34;
2642
+ var WATER_DROP = 0.1;
2643
+ var TERRAIN_VOLUME = { obscuring: 0.42, window: 0.42 };
2644
+ var WALL_HEIGHT = { levelSeparator: 0.12, door: 0.62, door2: 0.62 };
2645
+ var DEFAULT_WALL_HEIGHT = 0.62;
2646
+ function sceneCell(scene, x, y) {
2647
+ if (x < 0 || y < 0 || x >= scene.width || y >= scene.height) return null;
2648
+ return scene.cells[y * scene.width + x];
2649
+ }
2650
+ function buildMapScene(map) {
2651
+ const { width, height } = map;
2652
+ const maxLevel = mapMaxLevel(map);
2653
+ const cells = [];
2654
+ for (let y = 0; y < height; y++) {
2655
+ for (let x = 0; x < width; x++) {
2656
+ const square = mapSquare(map, x, y);
2657
+ const level = elevationLevel(square.elevation);
2658
+ const floor = (level - 1) * LEVEL_HEIGHT - (square.terrain === "water" ? WATER_DROP : 0);
2659
+ const top = square.terrain !== "blocking" ? floor + (TERRAIN_VOLUME[square.terrain] ?? 0) : square.environment === "indoor" ? maxLevel * LEVEL_HEIGHT : floor + LEVEL_HEIGHT;
2660
+ cells.push({ ...square, level, floor, top });
2661
+ }
2662
+ }
2663
+ const scene = { width, height, cells };
2664
+ const walls = [];
2665
+ for (const wall of map.walls ?? []) {
2666
+ const type = wall.type || map.defaultWallType || "normal";
2667
+ const length = Math.hypot(wall.x1 - wall.x0, wall.y1 - wall.y0);
2668
+ const pieces = Math.max(1, Math.round(length));
2669
+ for (let i = 0; i < pieces; i++) {
2670
+ const x0 = wall.x0 + (wall.x1 - wall.x0) * i / pieces, y0 = wall.y0 + (wall.y1 - wall.y0) * i / pieces;
2671
+ const x1 = wall.x0 + (wall.x1 - wall.x0) * (i + 1) / pieces, y1 = wall.y0 + (wall.y1 - wall.y0) * (i + 1) / pieces;
2672
+ const mx = (x0 + x1) / 2, my = (y0 + y1) / 2;
2673
+ const nx = length ? (y1 - y0) / (length / pieces) : 0, ny = length ? (x0 - x1) / (length / pieces) : 0;
2674
+ const sides = [[mx + nx * 0.25, my + ny * 0.25], [mx - nx * 0.25, my - ny * 0.25]].map(([px, py]) => sceneCell(scene, Math.floor(px), Math.floor(py))).filter((cell) => !!cell);
2675
+ const floors = sides.length ? sides.map((cell) => cell.floor) : [0];
2676
+ const z1 = Math.max(...floors) + (WALL_HEIGHT[type] ?? DEFAULT_WALL_HEIGHT);
2677
+ walls.push({ x0, y0, x1, y1, type, z0: Math.min(...floors), z1 });
2678
+ }
2679
+ }
2680
+ const ramps = [];
2681
+ for (const ramp of map.ramps ?? []) {
2682
+ const a = sceneCell(scene, ramp.x0, ramp.y0), b = sceneCell(scene, ramp.x1, ramp.y1);
2683
+ if (!a || !b || Math.abs(a.x - b.x) + Math.abs(a.y - b.y) !== 1 || a.floor === b.floor) continue;
2684
+ const [low, high] = a.floor < b.floor ? [a, b] : [b, a];
2685
+ ramps.push({ x: low.x, y: low.y, dx: high.x - low.x, dy: high.y - low.y, from: low.floor, to: high.floor });
2686
+ }
2687
+ const ziplines = (map.ziplines ?? []).map((line) => ({
2688
+ x0: line.x0 + 0.5,
2689
+ y0: line.y0 + 0.5,
2690
+ x1: line.x1 + 0.5,
2691
+ y1: line.y1 + 0.5,
2692
+ z: Math.max(sceneCell(scene, line.x0, line.y0)?.top ?? 0, sceneCell(scene, line.x1, line.y1)?.top ?? 0) + 0.7
2693
+ }));
2694
+ const maxTop = Math.max(...cells.map((cell) => cell.top), ...walls.map((wall) => wall.z1), ...ziplines.map((line) => line.z));
2695
+ return { width, height, cells, walls, ramps, ziplines, maxTop };
2696
+ }
2697
+ var DEFAULT_CAMERA = { azimuth: -32, tilt: 52, zoom: 1, panX: 0, panY: 0 };
2698
+ var TILT_RANGE = [6, 90];
2699
+ var ZOOM_RANGE = [1, 6];
2700
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2701
+ function makeProjection(scene, camera, viewport) {
2702
+ const rad = camera.azimuth * Math.PI / 180, tilt = camera.tilt * Math.PI / 180;
2703
+ const sin = Math.sin(rad), cos = Math.cos(rad), sinT = Math.sin(tilt), cosT = Math.cos(tilt);
2704
+ const cx2 = scene.width / 2, cy = scene.height / 2, midZ = (scene.maxTop + BASE_Z) / 2;
2705
+ const halfWidth = Math.hypot(cx2, cy);
2706
+ const halfHeight = Math.hypot(halfWidth, (scene.maxTop - BASE_Z) / 2);
2707
+ const fit = Math.min(viewport.width / (halfWidth * 2.06), viewport.height / (halfHeight * 2.06));
2708
+ const scale = fit * camera.zoom;
2709
+ const ox = viewport.width / 2 + camera.panX, oy = viewport.height / 2 + camera.panY;
2710
+ return {
2711
+ scale,
2712
+ sin,
2713
+ cos,
2714
+ point(x, y, z) {
2715
+ const dx = x - cx2, dy = y - cy;
2716
+ return [ox + scale * (dx * cos - dy * sin), oy + scale * ((dx * sin + dy * cos) * sinT - (z - midZ) * cosT)];
2717
+ },
2718
+ depth(x, y) {
2719
+ return (x - cx2) * sin + (y - cy) * cos;
2720
+ },
2721
+ ground(px, py, z) {
2722
+ const rx = (px - ox) / scale, ry = ((py - oy) / scale + (z - midZ) * cosT) / sinT;
2723
+ return [cx2 + rx * cos + ry * sin, cy - rx * sin + ry * cos];
2724
+ }
2725
+ };
2726
+ }
2727
+ function clampPan(scene, camera, viewport) {
2728
+ const limit = Math.hypot(scene.width, scene.height) / 2 * makeProjection(scene, camera, viewport).scale;
2729
+ return { ...camera, panX: clamp(camera.panX, -limit, limit), panY: clamp(camera.panY, -limit, limit) };
2730
+ }
2731
+ function zoomCamera(scene, camera, viewport, factor, anchor = { x: 0, y: 0 }) {
2732
+ const zoom = clamp(camera.zoom * factor, ZOOM_RANGE[0], ZOOM_RANGE[1]);
2733
+ const applied = zoom / camera.zoom;
2734
+ return clampPan(scene, {
2735
+ ...camera,
2736
+ zoom,
2737
+ panX: anchor.x + (camera.panX - anchor.x) * applied,
2738
+ panY: anchor.y + (camera.panY - anchor.y) * applied
2739
+ }, viewport);
2740
+ }
2741
+ var CELL_SIDES = [
2742
+ { ax: 0, ay: 1, bx: 0, by: 0, dx: -1, dy: 0 },
2743
+ { ax: 1, ay: 0, bx: 1, by: 1, dx: 1, dy: 0 },
2744
+ { ax: 0, ay: 0, bx: 1, by: 0, dx: 0, dy: -1 },
2745
+ { ax: 1, ay: 1, bx: 0, by: 1, dx: 0, dy: 1 }
2746
+ ];
2747
+ var inside = (px, py, polygon2) => {
2748
+ let hit = false;
2749
+ for (let i = 0, j = polygon2.length - 1; i < polygon2.length; j = i++) {
2750
+ const [xi, yi] = polygon2[i], [xj, yj] = polygon2[j];
2751
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) hit = !hit;
2752
+ }
2753
+ return hit;
2754
+ };
2755
+ function pickScene(scene, view, px, py) {
2756
+ const items = [];
2757
+ for (const wall of scene.walls) {
2758
+ items.push({
2759
+ depth: view.depth((wall.x0 + wall.x1) / 2, (wall.y0 + wall.y1) / 2),
2760
+ test() {
2761
+ const a = view.point(wall.x0, wall.y0, wall.z0), b = view.point(wall.x1, wall.y1, wall.z0);
2762
+ const c = view.point(wall.x1, wall.y1, wall.z1), d = view.point(wall.x0, wall.y0, wall.z1);
2763
+ if (!inside(px, py, [a, b, c, d])) return null;
2764
+ const length2 = (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2;
2765
+ const t = length2 ? clamp(((px - a[0]) * (b[0] - a[0]) + (py - a[1]) * (b[1] - a[1])) / length2, 0, 1) : 0.5;
2766
+ return { x: wall.x0 + (wall.x1 - wall.x0) * t, y: wall.y0 + (wall.y1 - wall.y0) * t };
2767
+ }
2768
+ });
2769
+ }
2770
+ for (const cell of scene.cells) {
2771
+ items.push({
2772
+ depth: view.depth(cell.x + 0.5, cell.y + 0.5),
2773
+ test() {
2774
+ const p00 = view.point(cell.x, cell.y, cell.top), p10 = view.point(cell.x + 1, cell.y, cell.top);
2775
+ const p11 = view.point(cell.x + 1, cell.y + 1, cell.top), p01 = view.point(cell.x, cell.y + 1, cell.top);
2776
+ if (inside(px, py, [p00, p10, p11, p01])) {
2777
+ const ax = p10[0] - p00[0], ay = p10[1] - p00[1], bx = p01[0] - p00[0], by = p01[1] - p00[1];
2778
+ const det = ax * by - ay * bx;
2779
+ if (!det) return { x: cell.x + 0.5, y: cell.y + 0.5 };
2780
+ const u = ((px - p00[0]) * by - (py - p00[1]) * bx) / det, v = ((py - p00[1]) * ax - (px - p00[0]) * ay) / det;
2781
+ return { x: cell.x + clamp(u, 0, 1), y: cell.y + clamp(v, 0, 1) };
2782
+ }
2783
+ for (const side of CELL_SIDES) {
2784
+ if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
2785
+ const neighbor = sceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2786
+ const low = neighbor ? neighbor.top : BASE_Z;
2787
+ if (low >= cell.top) continue;
2788
+ const a = view.point(cell.x + side.ax, cell.y + side.ay, low), b = view.point(cell.x + side.bx, cell.y + side.by, low);
2789
+ const c = view.point(cell.x + side.bx, cell.y + side.by, cell.top), d = view.point(cell.x + side.ax, cell.y + side.ay, cell.top);
2790
+ if (inside(px, py, [a, b, c, d])) {
2791
+ return { x: cell.x + 0.5 + side.dx * 0.4, y: cell.y + 0.5 + side.dy * 0.4 };
2792
+ }
2793
+ }
2794
+ return null;
2795
+ }
2796
+ });
2797
+ }
2798
+ items.sort((a, b) => b.depth - a.depth);
2799
+ for (const item of items) {
2800
+ const hit = item.test();
2801
+ if (hit) return hit;
2802
+ }
2803
+ return null;
2804
+ }
2805
+ function footprintOutline({ x, y, width, height, cells }, boardWidth, boardHeight) {
2806
+ const inside2 = (cx2, cy) => cx2 >= x && cy >= y && cx2 < x + width && cy < y + height && cx2 >= 0 && cy >= 0 && cx2 < boardWidth && cy < boardHeight && (!cells || !!cells[cy - y]?.[cx2 - x]);
2807
+ const squares = [];
2808
+ const edges = [];
2809
+ for (let cy = Math.max(0, y); cy < Math.min(boardHeight, y + height); cy++) {
2810
+ for (let cx2 = Math.max(0, x); cx2 < Math.min(boardWidth, x + width); cx2++) {
2811
+ if (!inside2(cx2, cy)) continue;
2812
+ squares.push([cx2, cy]);
2813
+ if (!inside2(cx2, cy - 1)) edges.push([[cx2, cy], [cx2 + 1, cy]]);
2814
+ if (!inside2(cx2 + 1, cy)) edges.push([[cx2 + 1, cy], [cx2 + 1, cy + 1]]);
2815
+ if (!inside2(cx2, cy + 1)) edges.push([[cx2, cy + 1], [cx2 + 1, cy + 1]]);
2816
+ if (!inside2(cx2 - 1, cy)) edges.push([[cx2, cy], [cx2, cy + 1]]);
2817
+ }
2818
+ }
2819
+ const directions = /* @__PURE__ */ new Map();
2820
+ for (const [a, b] of edges) {
2821
+ const direction = a[1] === b[1] ? "h" : "v";
2822
+ for (const point of [a, b]) {
2823
+ const key = point.join(",");
2824
+ directions.set(key, (directions.get(key) ?? /* @__PURE__ */ new Set()).add(direction));
2825
+ }
2826
+ }
2827
+ const corners = [...directions].filter(([, set]) => set.size === 2).map(([key]) => key.split(",").map(Number));
2828
+ return { squares, edges, corners };
2829
+ }
2830
+ var SIDE_COLORS = { indoor: "#7f8797", outdoor: "#a3906f", unknown: "#9a948a" };
2831
+ var MOVE_VALID_COLOR = "#1f7ae0";
2832
+ var MOVE_INVALID_COLOR = "#d0242c";
2833
+ var WALL_THICKNESS = 0.07;
2834
+ var HINDERING_BLOCK = "#5acb5a";
2835
+ var TRANSLUCENT_WALLS = { glass: 0.42, glass2: 0.42, forceField: 0.42, chainLinkFence: 0.55 };
2836
+ var GLASS_COLORS = { glass: "#e59400", glass2: "#33bbff", forceField: "#e59400" };
2837
+ function shade(hex, factor) {
2838
+ const n = Number.parseInt(hex.slice(1), 16);
2839
+ const c = [n >> 16, n >> 8 & 255, n & 255].map((v) => Math.round(Math.min(255, v * factor)));
2840
+ return `rgb(${c[0]},${c[1]},${c[2]})`;
2841
+ }
2842
+ function faceLight(view, nx, ny) {
2843
+ const sx = nx * view.cos - ny * view.sin;
2844
+ return 0.74 - sx * 0.2;
2845
+ }
2846
+ function polygon(ctx, points) {
2847
+ ctx.beginPath();
2848
+ points.forEach(([x, y], index) => index ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
2849
+ ctx.closePath();
2850
+ }
2851
+ function face(ctx, points, color) {
2852
+ polygon(ctx, points);
2853
+ ctx.fillStyle = color;
2854
+ ctx.fill();
2855
+ ctx.strokeStyle = color;
2856
+ ctx.lineWidth = 0.7;
2857
+ ctx.stroke();
2858
+ }
2859
+ function sideColor(cell) {
2860
+ if (cell.terrain === "blocking") return TERRAIN_EDGE_COLORS.blocking;
2861
+ if (cell.level > 1) return ELEVATION_TILE_COLORS[Math.min(cell.level, ELEVATION_TILE_COLORS.length) - 1];
2862
+ if (cell.terrain === "water") return TERRAIN_EDGE_COLORS.water;
2863
+ if (cell.terrain === "hindering") return TERRAIN_EDGE_COLORS.hindering;
2864
+ if (cell.terrain === "obscuring") return TERRAIN_EDGE_COLORS.obscuring;
2865
+ if (cell.terrain === "window") return TERRAIN_EDGE_COLORS.window;
2866
+ return SIDE_COLORS[cell.environment] ?? SIDE_COLORS.unknown;
2867
+ }
2868
+ function drawCellSides(ctx, scene, view, cell) {
2869
+ const { point } = view;
2870
+ for (const side of CELL_SIDES) {
2871
+ if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
2872
+ const neighbor = sceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2873
+ const low = neighbor ? neighbor.top : BASE_Z;
2874
+ if (low >= cell.top) continue;
2875
+ const ax = cell.x + side.ax, ay = cell.y + side.ay, bx = cell.x + side.bx, by = cell.y + side.by;
2876
+ const light = faceLight(view, side.dx, side.dy);
2877
+ face(ctx, [point(ax, ay, low), point(bx, by, low), point(bx, by, cell.top), point(ax, ay, cell.top)], shade(sideColor(cell), light));
2878
+ ctx.strokeStyle = "rgba(0,0,0,.26)";
2879
+ ctx.lineWidth = 1;
2880
+ for (let z = 0; z < cell.top - 0.01; z += LEVEL_HEIGHT) {
2881
+ if (z <= low + 0.01) continue;
2882
+ const [x0, y0] = point(ax, ay, z), [x1, y1] = point(bx, by, z);
2883
+ ctx.beginPath();
2884
+ ctx.moveTo(x0, y0);
2885
+ ctx.lineTo(x1, y1);
2886
+ ctx.stroke();
2887
+ }
2888
+ }
2889
+ }
2890
+ function drawCellTop(ctx, scene, view, cell, texture, shadows) {
2891
+ const p00 = view.point(cell.x, cell.y, cell.top), p10 = view.point(cell.x + 1, cell.y, cell.top), p01 = view.point(cell.x, cell.y + 1, cell.top);
2892
+ ctx.save();
2893
+ ctx.transform(p10[0] - p00[0], p10[1] - p00[1], p01[0] - p00[0], p01[1] - p00[1], p00[0], p00[1]);
2894
+ ctx.drawImage(texture, cell.x * TILE_SIZE, cell.y * TILE_SIZE, TILE_SIZE, TILE_SIZE, -0.012, -0.012, 1.024, 1.024);
2895
+ if (cell.environment === "indoor") {
2896
+ ctx.fillStyle = (cell.x + cell.y) % 2 ? "rgba(52,62,92,.13)" : "rgba(52,62,92,.07)";
2897
+ ctx.fillRect(0, 0, 1, 1);
2898
+ }
2899
+ CELL_SIDES.forEach((side, index) => {
2900
+ const neighbor = sceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2901
+ if (!neighbor || neighbor.top <= cell.top + 0.01) return;
2902
+ ctx.fillStyle = shadows[index];
2903
+ ctx.fillRect(0, 0, 1, 1);
2904
+ });
2905
+ ctx.restore();
2906
+ }
2907
+ function hash(x, y, salt) {
2908
+ const value = Math.sin(x * 127.1 + y * 311.7 + salt * 74.7) * 43758.5453;
2909
+ return value - Math.floor(value);
2910
+ }
2911
+ function drawBox(ctx, view, x0, y0, x1, y1, z0, z1, color, topFactor = 1.08) {
2912
+ const { point } = view;
2913
+ const sides = [
2914
+ { ax: x0, ay: y1, bx: x0, by: y0, dx: -1, dy: 0 },
2915
+ { ax: x1, ay: y0, bx: x1, by: y1, dx: 1, dy: 0 },
2916
+ { ax: x0, ay: y0, bx: x1, by: y0, dx: 0, dy: -1 },
2917
+ { ax: x1, ay: y1, bx: x0, by: y1, dx: 0, dy: 1 }
2918
+ ];
2919
+ for (const side of sides) {
2920
+ if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
2921
+ face(ctx, [point(side.ax, side.ay, z0), point(side.bx, side.by, z0), point(side.bx, side.by, z1), point(side.ax, side.ay, z1)], shade(color, faceLight(view, side.dx, side.dy)));
2922
+ }
2923
+ face(ctx, [point(x0, y0, z1), point(x1, y0, z1), point(x1, y1, z1), point(x0, y1, z1)], shade(color, topFactor));
2924
+ }
2925
+ function drawHindering(ctx, view, cell) {
2926
+ const spots = [[0.28, 0.3], [0.7, 0.26], [0.3, 0.72], [0.72, 0.7]].map(([u, v], index) => {
2927
+ const size = 0.13 + hash(cell.x, cell.y, index) * 0.07;
2928
+ const x = cell.x + u + (hash(cell.x, cell.y, index + 4) - 0.5) * 0.1, y = cell.y + v + (hash(cell.x, cell.y, index + 8) - 0.5) * 0.1;
2929
+ return { x, y, size, height: 0.12 + hash(cell.x, cell.y, index + 12) * 0.16 };
2930
+ }).sort((a, b) => view.depth(a.x, a.y) - view.depth(b.x, b.y));
2931
+ for (const spot of spots) {
2932
+ drawBox(ctx, view, spot.x - spot.size, spot.y - spot.size, spot.x + spot.size, spot.y + spot.size, cell.top, cell.top + spot.height, HINDERING_BLOCK, 1.1);
2933
+ }
2934
+ }
2935
+ function drawRamp(ctx, view, ramp) {
2936
+ const { point } = view;
2937
+ const cx2 = ramp.x + 0.5 + ramp.dx * 0.5, cy = ramp.y + 0.5 + ramp.dy * 0.5;
2938
+ const sx = ramp.dy * 0.2, sy = ramp.dx * 0.2, lx = -ramp.dx * 0.38, ly = -ramp.dy * 0.38;
2939
+ const a = [cx2 - sx, cy - sy], b = [cx2 + sx, cy + sy];
2940
+ const c = [cx2 + sx + lx, cy + sy + ly], d = [cx2 - sx + lx, cy - sy + ly];
2941
+ const color = ELEVATION_TILE_COLORS[1];
2942
+ face(ctx, [point(a[0], a[1], ramp.from), point(d[0], d[1], ramp.from), point(a[0], a[1], ramp.to)], shade(color, 0.66));
2943
+ face(ctx, [point(b[0], b[1], ramp.from), point(c[0], c[1], ramp.from), point(b[0], b[1], ramp.to)], shade(color, 0.66));
2944
+ polygon(ctx, [point(a[0], a[1], ramp.to), point(b[0], b[1], ramp.to), point(c[0], c[1], ramp.from), point(d[0], d[1], ramp.from)]);
2945
+ ctx.fillStyle = color;
2946
+ ctx.fill();
2947
+ ctx.strokeStyle = "#ff0000";
2948
+ ctx.lineWidth = 1.5;
2949
+ ctx.stroke();
2950
+ }
2951
+ function drawWall(ctx, view, wall) {
2952
+ if (wall.x0 !== wall.x1 && wall.y0 !== wall.y1) {
2953
+ face(ctx, [view.point(wall.x0, wall.y0, wall.z0), view.point(wall.x1, wall.y1, wall.z0), view.point(wall.x1, wall.y1, wall.z1), view.point(wall.x0, wall.y0, wall.z1)], "#30343c");
2954
+ return;
2955
+ }
2956
+ const half = WALL_THICKNESS / 2;
2957
+ const ox = wall.y0 === wall.y1 ? 0 : half, oy = wall.y0 === wall.y1 ? half : 0;
2958
+ const x0 = Math.min(wall.x0, wall.x1) - ox, x1 = Math.max(wall.x0, wall.x1) + ox;
2959
+ const y0 = Math.min(wall.y0, wall.y1) - oy, y1 = Math.max(wall.y0, wall.y1) + oy;
2960
+ const alpha = TRANSLUCENT_WALLS[wall.type];
2961
+ const color = GLASS_COLORS[wall.type] ?? (wall.type === "normal" ? "#30343c" : WALL_COLORS[wall.type] ?? "#30343c");
2962
+ ctx.save();
2963
+ if (alpha) ctx.globalAlpha = alpha;
2964
+ if (wall.type === "door" || wall.type === "door2") {
2965
+ const along = wall.y0 === wall.y1;
2966
+ const post = 0.16;
2967
+ const pieces = along ? [[x0, y0, x0 + post, y1, wall.z0, wall.z1], [x1 - post, y0, x1, y1, wall.z0, wall.z1], [x0, y0, x1, y1, wall.z1 - 0.14, wall.z1]] : [[x0, y0, x1, y0 + post, wall.z0, wall.z1], [x0, y1 - post, x1, y1, wall.z0, wall.z1], [x0, y0, x1, y1, wall.z1 - 0.14, wall.z1]];
2968
+ const doorColor = wall.type === "door2" ? "#dddddd" : "#30343c";
2969
+ pieces.sort((a, b) => view.depth(a[0], a[1]) - view.depth(b[0], b[1]));
2970
+ for (const p of pieces) drawBox(ctx, view, p[0], p[1], p[2], p[3], p[4], p[5], doorColor);
2971
+ } else {
2972
+ drawBox(ctx, view, x0, y0, x1, y1, wall.z0, wall.z1, color, 1.25);
2973
+ }
2974
+ ctx.restore();
2975
+ }
2976
+ function drawIndoorCurb(ctx, scene, view, cell) {
2977
+ ctx.strokeStyle = INDOOR_EDGE_COLOR;
2978
+ ctx.lineWidth = 2;
2979
+ for (const side of CELL_SIDES) {
2980
+ const neighbor = sceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2981
+ if (neighbor?.environment !== "outdoor") continue;
2982
+ const [x0, y0] = view.point(cell.x + side.ax, cell.y + side.ay, cell.top), [x1, y1] = view.point(cell.x + side.bx, cell.y + side.by, cell.top);
2983
+ ctx.beginPath();
2984
+ ctx.moveTo(x0, y0);
2985
+ ctx.lineTo(x1, y1);
2986
+ ctx.stroke();
2987
+ }
2988
+ }
2989
+ function drawFootprint(ctx, scene, view, footprint) {
2990
+ const { squares, edges, corners } = footprintOutline(footprint, scene.width, scene.height);
2991
+ if (!squares.length) return;
2992
+ const { top, valid } = footprint;
2993
+ const color = valid ? MOVE_VALID_COLOR : MOVE_INVALID_COLOR;
2994
+ ctx.save();
2995
+ ctx.beginPath();
2996
+ for (const [cx2, cy] of squares) {
2997
+ const points = [[cx2, cy], [cx2 + 1, cy], [cx2 + 1, cy + 1], [cx2, cy + 1]].map(([px, py]) => view.point(px, py, 0));
2998
+ ctx.moveTo(...points[0]);
2999
+ for (const point of points.slice(1)) ctx.lineTo(...point);
3000
+ ctx.closePath();
3001
+ }
3002
+ ctx.globalAlpha = 0.28;
3003
+ ctx.fillStyle = color;
3004
+ ctx.fill();
3005
+ ctx.globalAlpha = 1;
3006
+ ctx.strokeStyle = color;
3007
+ ctx.lineWidth = 2;
3008
+ if (!valid) ctx.setLineDash([6, 4]);
3009
+ const strokeEdges = (z) => {
3010
+ ctx.beginPath();
3011
+ for (const [a, b] of edges) {
3012
+ ctx.moveTo(...view.point(a[0], a[1], z));
3013
+ ctx.lineTo(...view.point(b[0], b[1], z));
3014
+ }
3015
+ ctx.stroke();
3016
+ };
3017
+ strokeEdges(0);
3018
+ if (top > 0.01) {
3019
+ strokeEdges(top);
3020
+ ctx.beginPath();
3021
+ for (const [cx2, cy] of corners) {
3022
+ ctx.moveTo(...view.point(cx2, cy, 0));
3023
+ ctx.lineTo(...view.point(cx2, cy, top));
3024
+ }
3025
+ ctx.stroke();
3026
+ }
3027
+ ctx.restore();
3028
+ }
3029
+ function drawHighlight(ctx, scene, view, highlight) {
3030
+ if (highlight.footprint) {
3031
+ drawFootprint(ctx, scene, view, highlight.footprint);
3032
+ return;
3033
+ }
3034
+ if (highlight.line) {
3035
+ const { x0, y0, x1, y1 } = highlight.line;
3036
+ const mx = (x0 + x1) / 2, my = (y0 + y1) / 2;
3037
+ const near = [[Math.floor(mx - 0.01), Math.floor(my - 0.01)], [Math.floor(mx + 0.01), Math.floor(my + 0.01)]].map(([x, y]) => sceneCell(scene, x, y)?.top ?? 0);
3038
+ const z = Math.max(...near);
3039
+ const [ax, ay] = view.point(x0, y0, z), [bx, by] = view.point(x1, y1, z);
3040
+ ctx.strokeStyle = "rgba(0,0,0,.6)";
3041
+ ctx.lineWidth = 4;
3042
+ ctx.lineCap = "round";
3043
+ ctx.beginPath();
3044
+ ctx.moveTo(ax, ay);
3045
+ ctx.lineTo(bx, by);
3046
+ ctx.stroke();
3047
+ ctx.lineCap = "butt";
3048
+ return;
3049
+ }
3050
+ const cell = highlight.square && sceneCell(scene, highlight.square.x, highlight.square.y);
3051
+ if (!cell) return;
3052
+ polygon(ctx, [view.point(cell.x, cell.y, cell.top), view.point(cell.x + 1, cell.y, cell.top), view.point(cell.x + 1, cell.y + 1, cell.top), view.point(cell.x, cell.y + 1, cell.top)]);
3053
+ ctx.fillStyle = "rgba(0,0,0,.16)";
3054
+ ctx.fill();
3055
+ ctx.strokeStyle = "rgba(0,0,0,.6)";
3056
+ ctx.lineWidth = 2;
3057
+ ctx.stroke();
3058
+ }
3059
+ function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true } = {}) {
3060
+ const pad = 0.35;
3061
+ polygon(ctx, [view.point(-pad, -pad, BASE_Z - 0.16), view.point(scene.width + pad, -pad, BASE_Z - 0.16), view.point(scene.width + pad, scene.height + pad, BASE_Z - 0.16), view.point(-pad, scene.height + pad, BASE_Z - 0.16)]);
3062
+ ctx.fillStyle = "rgba(0,0,0,.14)";
3063
+ ctx.fill();
3064
+ const shadowDepth = 0.24;
3065
+ const shadows = [[0, 0, shadowDepth, 0], [1, 0, 1 - shadowDepth, 0], [0, 0, 0, shadowDepth], [0, 1, 0, 1 - shadowDepth]].map(([x0, y0, x1, y1]) => {
3066
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
3067
+ gradient.addColorStop(0, "rgba(0,0,0,.26)");
3068
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
3069
+ return gradient;
3070
+ });
3071
+ const rampsByCell = /* @__PURE__ */ new Map();
3072
+ for (const ramp of scene.ramps) {
3073
+ const key = ramp.y * scene.width + ramp.x;
3074
+ rampsByCell.set(key, [...rampsByCell.get(key) ?? [], ramp]);
3075
+ }
3076
+ const mixed = scene.cells.some((cell) => cell.environment === "indoor") && scene.cells.some((cell) => cell.environment === "outdoor");
3077
+ const items = [];
3078
+ for (const cell of scene.cells) {
3079
+ items.push({
3080
+ depth: view.depth(cell.x + 0.5, cell.y + 0.5),
3081
+ draw() {
3082
+ drawCellSides(ctx, scene, view, cell);
3083
+ drawCellTop(ctx, scene, view, cell, texture, shadows);
3084
+ if (mixed && cell.environment === "indoor") drawIndoorCurb(ctx, scene, view, cell);
3085
+ for (const ramp of rampsByCell.get(cell.y * scene.width + cell.x) ?? []) drawRamp(ctx, view, ramp);
3086
+ if (tufts && cell.terrain === "hindering") drawHindering(ctx, view, cell);
3087
+ }
3088
+ });
3089
+ }
3090
+ for (const wall of scene.walls) {
3091
+ items.push({ depth: view.depth((wall.x0 + wall.x1) / 2, (wall.y0 + wall.y1) / 2), draw: () => drawWall(ctx, view, wall) });
3092
+ }
3093
+ items.sort((a, b) => a.depth - b.depth);
3094
+ for (const item of items) item.draw();
3095
+ ctx.strokeStyle = "#111";
3096
+ ctx.lineWidth = 2;
3097
+ for (const line of scene.ziplines) {
3098
+ const [ax, ay] = view.point(line.x0, line.y0, line.z), [bx, by] = view.point(line.x1, line.y1, line.z);
3099
+ ctx.beginPath();
3100
+ ctx.moveTo(ax, ay);
3101
+ ctx.lineTo(bx, by);
3102
+ ctx.stroke();
3103
+ }
3104
+ if (highlight) drawHighlight(ctx, scene, view, highlight);
3105
+ }
3106
+ var MAP_THUMBNAIL_HEIGHT = 192;
3107
+ function renderMapThumbnail(json, height = MAP_THUMBNAIL_HEIGHT) {
3108
+ if (!json?.width || !json?.height) return null;
3109
+ const full = document.createElement("canvas");
3110
+ full.width = json.width * TILE_SIZE;
3111
+ full.height = json.height * TILE_SIZE;
3112
+ const fctx = full.getContext("2d");
3113
+ if (!fctx) return null;
3114
+ new HCMap(json).draw(fctx);
3115
+ const scale = height / full.height;
3116
+ const thumb = document.createElement("canvas");
3117
+ thumb.width = Math.max(1, Math.round(full.width * scale));
3118
+ thumb.height = height;
3119
+ const tctx = thumb.getContext("2d");
3120
+ if (!tctx) return null;
3121
+ tctx.imageSmoothingQuality = "high";
3122
+ tctx.drawImage(full, 0, 0, thumb.width, thumb.height);
3123
+ return thumb.toDataURL("image/png");
3124
+ }
3125
+ var MapBoardView = forwardRef(function MapBoardView2({ map, canvasLabel, selection, onInspect, onLeave, onDrawError, className, style, ...rest }, ref) {
3126
+ const canvasRef = useRef(null);
3127
+ const failed = useRef(onDrawError);
3128
+ failed.current = onDrawError;
3129
+ useEffect(() => {
3130
+ const canvas = canvasRef.current;
3131
+ if (!canvas) return;
3132
+ const ctx = canvas.getContext("2d");
3133
+ if (!ctx) return;
3134
+ try {
3135
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
3136
+ new HCMap(map).draw(ctx);
3137
+ } catch {
3138
+ failed.current?.();
3139
+ }
3140
+ }, [map]);
3141
+ function pointer(event) {
3142
+ const bounds = event.currentTarget.getBoundingClientRect();
3143
+ onInspect?.((event.clientX - bounds.left) / bounds.width * map.width, (event.clientY - bounds.top) / bounds.height * map.height, event.clientX, event.clientY);
3144
+ }
3145
+ return /* @__PURE__ */ jsxs(
3146
+ "div",
3147
+ {
3148
+ ref,
3149
+ className: cx("hc-ui-map-board", className),
3150
+ style: { aspectRatio: `${map.width} / ${map.height}`, ...style },
3151
+ ...rest,
3152
+ onPointerMove: (event) => {
3153
+ if (event.pointerType !== "touch") pointer(event);
3154
+ },
3155
+ onPointerDown: pointer,
3156
+ onPointerLeave: (event) => {
3157
+ if (event.pointerType !== "touch") onLeave?.();
3158
+ },
3159
+ children: [
3160
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, width: map.width * TILE_SIZE, height: map.height * TILE_SIZE, role: "img", "aria-label": canvasLabel }),
3161
+ selection && /* @__PURE__ */ jsx("svg", { className: "hc-ui-map-board__selection", viewBox: `0 0 ${map.width} ${map.height}`, "aria-hidden": "true", children: selection.line ? /* @__PURE__ */ jsx("line", { x1: selection.line.x0, y1: selection.line.y0, x2: selection.line.x1, y2: selection.line.y1 }) : /* @__PURE__ */ jsx("rect", { x: selection.square.x, y: selection.square.y, width: "1", height: "1" }) })
3162
+ ]
3163
+ }
3164
+ );
3165
+ });
3166
+ function MapBoardPlaceholder({ label }) {
3167
+ return /* @__PURE__ */ jsx("div", { className: "hc-ui-map-board", "data-loading": "", role: "status", "aria-busy": "true", children: /* @__PURE__ */ jsx("span", { className: "hc-ui-map-board__status", children: label }) });
3168
+ }
3169
+ function MapSwatch({ fill, edge, children, lightText }) {
3170
+ return /* @__PURE__ */ jsx("span", { className: "hc-ui-map-swatch", "data-light-text": flag(lightText), style: { backgroundColor: fill, borderColor: edge }, "aria-hidden": "true", children });
3171
+ }
3172
+ function MapSquareInfo({ square }) {
3173
+ const { coordinate, terrain, elevation, environment, environmentNote, notes = [] } = square;
3174
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-map-tooltip__square", children: [
3175
+ coordinate && /* @__PURE__ */ jsx("span", { className: "hc-ui-map-tooltip__coordinate", children: coordinate }),
3176
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-tooltip__row", "data-kind": "terrain", children: [
3177
+ /* @__PURE__ */ jsx(MapSwatch, { fill: terrain.fill, edge: terrain.edge }),
3178
+ terrain.label
3179
+ ] }),
3180
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-tooltip__row", children: [
3181
+ /* @__PURE__ */ jsx(MapSwatch, { fill: elevation.fill, edge: elevation.edge, lightText: elevation.lightText, children: elevation.level }),
3182
+ elevation.label
3183
+ ] }),
3184
+ environmentNote !== void 0 ? /* @__PURE__ */ jsx("span", { className: "hc-ui-map-tooltip__row", children: environmentNote }) : environment,
3185
+ notes.map((note, index) => /* @__PURE__ */ jsx("span", { className: "hc-ui-map-tooltip__note", children: note }, index))
3186
+ ] });
3187
+ }
3188
+ function MapTooltipView({ id, anchor, title, wallColor, squares }) {
3189
+ const ref = useRef(null);
3190
+ const [position, setPosition] = useState({ left: 0, top: 0 });
3191
+ useLayoutEffect(() => {
3192
+ const bounds = ref.current?.getBoundingClientRect();
3193
+ if (!bounds) return;
3194
+ setPosition({ left: Math.max(8, Math.min(anchor.x + 14, window.innerWidth - bounds.width - 8)), top: Math.max(8, anchor.y + 18 + bounds.height > window.innerHeight - 8 ? anchor.y - bounds.height - 14 : anchor.y + 18) });
3195
+ }, [anchor]);
3196
+ return /* @__PURE__ */ jsxs("div", { ref, id, role: "tooltip", className: "hc-ui-map-tooltip", style: position, children: [
3197
+ /* @__PURE__ */ jsxs("strong", { className: "hc-ui-map-tooltip__title", children: [
3198
+ wallColor && /* @__PURE__ */ jsx("span", { className: "hc-ui-map-swatch", "data-wall": "", style: { backgroundColor: wallColor }, "aria-hidden": "true" }),
3199
+ title
3200
+ ] }),
3201
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-map-tooltip__squares", children: squares.map((square, index) => /* @__PURE__ */ jsx(MapSquareInfo, { square }, index)) })
3202
+ ] });
3203
+ }
3204
+ function MapLegendView({ items, children }) {
3205
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-map-legend", children: [
3206
+ items.map((item) => /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-legend__key", children: [
3207
+ /* @__PURE__ */ jsx(MapSwatch, { fill: item.fill, edge: item.edge }),
3208
+ item.label
3209
+ ] }, item.label)),
3210
+ children
3211
+ ] });
3212
+ }
3213
+ function mapEnvironments(types) {
3214
+ return [...new Set(types.flatMap((type) => {
3215
+ const value = type.toLowerCase().replace(/[^a-z]/g, "");
3216
+ return value === "indooroutdoor" ? ["indoor", "outdoor"] : value === "indoor" || value === "outdoor" ? [value] : [];
3217
+ }))];
3218
+ }
3219
+ function MapEnvironmentView({ types, icons, labels, className }) {
3220
+ const environments = mapEnvironments(types);
3221
+ if (!environments.length) return null;
3222
+ return /* @__PURE__ */ jsx("span", { className: cx("hc-ui-map-environment", className), children: environments.map((environment) => /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-environment__item", children: [
3223
+ /* @__PURE__ */ jsx("img", { src: icons[environment], alt: "", width: "18", height: "18" }),
3224
+ /* @__PURE__ */ jsx("span", { children: labels[environment] })
3225
+ ] }, environment)) });
3226
+ }
172
3227
  export {
3228
+ AvatarView,
3229
+ BACK_TABLE_HEIGHT,
3230
+ BASE_Z,
3231
+ BYSTANDER_DIAL_STEP,
173
3232
  Button,
3233
+ CARD_HEIGHT,
3234
+ CARD_WIDTH,
3235
+ CELL_SIDES,
3236
+ COMBAT_DIAL_STATS,
3237
+ Checkbox,
174
3238
  Chip,
3239
+ CombatDialView,
3240
+ CommandPalette,
3241
+ DEFAULT_CAMERA,
3242
+ Dialog,
3243
+ ELEVATION_EDGE_COLOR,
3244
+ ELEVATION_TILE_COLORS,
3245
+ EquipmentIcon,
175
3246
  Field,
3247
+ FilterBar,
3248
+ HCMap,
3249
+ INDOOR_EDGE_COLOR,
176
3250
  IconButton,
3251
+ LEVEL_HEIGHT,
3252
+ ListRow,
3253
+ MAP_THUMBNAIL_HEIGHT,
3254
+ MINI_DIAL_STATS,
3255
+ MapBoardPlaceholder,
3256
+ MapBoardView,
3257
+ MapEnvironmentView,
3258
+ MapLegendView,
3259
+ MapSwatch,
3260
+ MapTooltipView,
3261
+ Menu,
3262
+ MenuItem,
3263
+ MenuItemCheckbox,
3264
+ MenuItemRadio,
3265
+ MenuLabel,
3266
+ MenuRadioGroup,
3267
+ MenuSeparator,
3268
+ Meter,
3269
+ MiniDialView,
3270
+ PHYSICAL_CARD_STATS,
3271
+ PageHeader,
3272
+ PanelHeader,
3273
+ PhysicalCardFocusView,
3274
+ PhysicalCardView,
3275
+ Popover,
3276
+ PowerIconView,
177
3277
  SegmentedControl,
3278
+ SetIconView,
3279
+ Sheet,
3280
+ SheetChips,
3281
+ SheetOptions,
178
3282
  Skeleton,
179
- Surface
3283
+ SortMenu,
3284
+ SpecialIconView,
3285
+ StatGlyphView,
3286
+ StatIcon,
3287
+ Surface,
3288
+ TERRAIN_EDGE_COLORS,
3289
+ TERRAIN_TILE_COLORS,
3290
+ TILE_SIZE,
3291
+ TILT_RANGE,
3292
+ Table,
3293
+ WALL_COLORS,
3294
+ ZOOM_RANGE,
3295
+ backCount,
3296
+ buildMapScene,
3297
+ clamp,
3298
+ clampPan,
3299
+ devicePosition,
3300
+ drawMapScene,
3301
+ elevationLevel,
3302
+ footprintOutline,
3303
+ inspectMap,
3304
+ makeProjection,
3305
+ mapEnvironments,
3306
+ mapMaxLevel,
3307
+ mapSquare,
3308
+ nextDistribution,
3309
+ pickScene,
3310
+ placePopup,
3311
+ renderMapThumbnail,
3312
+ requestMotionAccess,
3313
+ sceneCell,
3314
+ squareCoordinate,
3315
+ tiltAt,
3316
+ tiltStyles,
3317
+ useImageChain,
3318
+ zoomCamera
180
3319
  };