@wipco/sdui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +27 -0
  3. package/dist/chunk-4SU2KYPU.js +59 -0
  4. package/dist/chunk-56KKWLNI.js +186 -0
  5. package/dist/chunk-7Y4J7EOK.js +115 -0
  6. package/dist/chunk-APCXOCAO.js +110 -0
  7. package/dist/chunk-AROOQ5W6.js +119 -0
  8. package/dist/chunk-DGPRX4MU.js +251 -0
  9. package/dist/chunk-IL2KS4WS.js +25 -0
  10. package/dist/chunk-KADFN76X.js +541 -0
  11. package/dist/chunk-KBHI3IUR.js +40 -0
  12. package/dist/chunk-LZJTKVMO.js +767 -0
  13. package/dist/chunk-MOIU7MUA.js +282 -0
  14. package/dist/chunk-MSAV4J4I.js +77 -0
  15. package/dist/chunk-OOHHW4K4.js +159 -0
  16. package/dist/chunk-Q6SBH63R.js +190 -0
  17. package/dist/chunk-QZQDHSMD.js +78 -0
  18. package/dist/chunk-RH7ESUE6.js +309 -0
  19. package/dist/chunk-SEBPBUZR.js +260 -0
  20. package/dist/chunk-USKRM22W.js +39 -0
  21. package/dist/chunk-WNYWTFGW.js +61 -0
  22. package/dist/chunk-WVRCIVAD.js +72 -0
  23. package/dist/chunk-YJMD6MOE.js +404 -0
  24. package/dist/chunk-ZNITJ4KK.js +433 -0
  25. package/dist/chunk-ZQR3ULVH.js +193 -0
  26. package/dist/{chunk-QJHD6HVQ.js → chunk-ZUCI34QY.js} +106 -390
  27. package/dist/index.d.ts +306 -53
  28. package/dist/index.js +276 -720
  29. package/dist/meta.d.ts +8 -0
  30. package/dist/meta.js +147 -8
  31. package/dist/rich.js +3 -2
  32. package/dist/styles.css +246 -0
  33. package/dist/widgets/AutocompleteField.d.ts +96 -0
  34. package/dist/widgets/AutocompleteField.js +3 -0
  35. package/dist/widgets/ColumnBoard.d.ts +47 -0
  36. package/dist/widgets/ColumnBoard.js +2 -0
  37. package/dist/widgets/DiffView.d.ts +30 -0
  38. package/dist/widgets/DiffView.js +2 -0
  39. package/dist/widgets/Disclosure.d.ts +20 -0
  40. package/dist/widgets/Disclosure.js +2 -0
  41. package/dist/widgets/EventCalendar.d.ts +59 -0
  42. package/dist/widgets/EventCalendar.js +2 -0
  43. package/dist/widgets/ImageLightbox.d.ts +25 -0
  44. package/dist/widgets/ImageLightbox.js +2 -0
  45. package/dist/widgets/InlineEdit.d.ts +23 -0
  46. package/dist/widgets/InlineEdit.js +2 -0
  47. package/dist/widgets/JsonField.d.ts +24 -0
  48. package/dist/widgets/JsonField.js +2 -0
  49. package/dist/widgets/LiveClock.d.ts +21 -0
  50. package/dist/widgets/LiveClock.js +1 -0
  51. package/dist/widgets/OrgChart.d.ts +32 -0
  52. package/dist/widgets/OrgChart.js +2 -0
  53. package/dist/widgets/PeriodFilter.d.ts +48 -0
  54. package/dist/widgets/PeriodFilter.js +2 -0
  55. package/dist/widgets/PrintSheet.d.ts +23 -0
  56. package/dist/widgets/PrintSheet.js +2 -0
  57. package/dist/widgets/SearchableMultiPicker.d.ts +29 -0
  58. package/dist/widgets/SearchableMultiPicker.js +3 -0
  59. package/dist/widgets/SecureScreen.d.ts +25 -0
  60. package/dist/widgets/SecureScreen.js +2 -0
  61. package/dist/widgets/StackedMeter.d.ts +26 -0
  62. package/dist/widgets/StackedMeter.js +1 -0
  63. package/dist/widgets/Strip.d.ts +20 -0
  64. package/dist/widgets/Strip.js +1 -0
  65. package/dist/widgets/SummaryTable.d.ts +31 -0
  66. package/dist/widgets/SummaryTable.js +1 -0
  67. package/dist/widgets/TagMultiSelect.d.ts +25 -0
  68. package/dist/widgets/TagMultiSelect.js +2 -0
  69. package/dist/widgets/Timeline.d.ts +30 -0
  70. package/dist/widgets/Timeline.js +3 -0
  71. package/dist/widgets/TreePicker.d.ts +21 -0
  72. package/dist/widgets/TreePicker.js +3 -0
  73. package/dist/widgets/TreeView.d.ts +26 -0
  74. package/dist/widgets/TreeView.js +2 -0
  75. package/dist/widgets/index.d.ts +76 -0
  76. package/dist/widgets/index.js +69 -0
  77. package/package.json +93 -3
@@ -0,0 +1,190 @@
1
+ import { Badge, Checkbox, IconButton, Icon } from './chunk-ZUCI34QY.js';
2
+ import { useState, useEffect } from 'react';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ var BOARD_CSS = `
6
+ .erp-colboard{display:flex;align-items:flex-start;gap:var(--wp-space-3);overflow-x:auto;padding-bottom:var(--wp-space-1)}
7
+ .erp-colboard__col{flex:1 1 0;min-width:230px;display:flex;flex-direction:column;gap:var(--wp-space-2)}
8
+ .erp-colboard__head{display:flex;align-items:center;justify-content:space-between;gap:var(--wp-space-2);padding:var(--wp-space-2) var(--wp-space-3);border:var(--wp-border-w) solid;border-radius:var(--wp-radius-md)}
9
+ .erp-colboard__head-label{font-size:var(--wp-size-sm);font-weight:var(--wp-weight-semibold);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
10
+ .erp-colboard__body{display:flex;flex-direction:column;gap:var(--wp-space-2);padding:var(--wp-space-2);background:var(--wp-surface-sunken);border-radius:var(--wp-radius-lg);min-height:96px}
11
+ .erp-colboard__empty{padding:var(--wp-space-4) var(--wp-space-2);text-align:center}
12
+ .erp-colboard__card{display:flex;align-items:flex-start;gap:var(--wp-space-2);padding:var(--wp-space-2) var(--wp-space-3);background:var(--wp-surface-card);border:var(--wp-border-w) solid var(--wp-border-subtle);border-radius:var(--wp-radius-md);box-shadow:var(--wp-shadow-xs)}
13
+ .erp-colboard__card--selected{border-color:var(--wp-border-focus);background:var(--wp-surface-selected)}
14
+ .erp-colboard__main{flex:1 1 auto;min-width:0;display:block;text-align:left;background:none;border:0;padding:0;margin:0;font:inherit;color:inherit}
15
+ .erp-colboard__main--click{cursor:pointer}
16
+ .erp-colboard__main--click:hover .erp-colboard__title{color:var(--wp-text-link)}
17
+ .erp-colboard__trow{display:flex;align-items:center;gap:var(--wp-space-2);min-width:0}
18
+ .erp-colboard__title{flex:0 1 auto;min-width:0;font-size:var(--wp-size-sm);font-weight:var(--wp-weight-medium);color:var(--wp-text-strong);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
19
+ .erp-colboard__ellip{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
20
+ .erp-colboard__menu{flex:none}
21
+ .erp-colboard__opt{display:flex;align-items:center;gap:var(--wp-space-2);width:100%;text-align:left;padding:var(--wp-space-2) var(--wp-space-3);border:0;border-radius:var(--wp-radius-sm);background:transparent;color:var(--wp-text-default);font:inherit;font-size:var(--wp-size-sm);cursor:pointer;white-space:nowrap}
22
+ .erp-colboard__opt:hover{background:var(--wp-surface-hover)}
23
+ `;
24
+ function BoardStyles() {
25
+ return /* @__PURE__ */ jsx("style", { href: "erp-colboard", precedence: "medium", children: BOARD_CSS });
26
+ }
27
+ function toneStyle(tone) {
28
+ return {
29
+ background: `var(--wp-status-${tone}-bg)`,
30
+ borderColor: `var(--wp-status-${tone}-border)`,
31
+ color: `var(--wp-status-${tone}-fg)`
32
+ };
33
+ }
34
+ function ColumnBoard({
35
+ columns,
36
+ items,
37
+ onMove,
38
+ onItemClick,
39
+ selectable = false,
40
+ selectedIds,
41
+ onSelectChange
42
+ }) {
43
+ const [openMenuFor, setOpenMenuFor] = useState(null);
44
+ const [internalSelected, setInternalSelected] = useState([]);
45
+ const selected = selectedIds ?? internalSelected;
46
+ const toggleSelect = (id) => {
47
+ const next = selected.includes(id) ? selected.filter((existing) => existing !== id) : [...selected, id];
48
+ if (selectedIds === void 0) setInternalSelected(next);
49
+ onSelectChange?.(next);
50
+ };
51
+ useEffect(() => {
52
+ if (openMenuFor === null) return;
53
+ const closeOnOutsideClick = (event) => {
54
+ if (event.target instanceof Element && event.target.closest(".erp-colboard__menu")) return;
55
+ setOpenMenuFor(null);
56
+ };
57
+ const closeOnEscape = (event) => {
58
+ if (event.key === "Escape") setOpenMenuFor(null);
59
+ };
60
+ document.addEventListener("pointerdown", closeOnOutsideClick);
61
+ document.addEventListener("keydown", closeOnEscape);
62
+ return () => {
63
+ document.removeEventListener("pointerdown", closeOnOutsideClick);
64
+ document.removeEventListener("keydown", closeOnEscape);
65
+ };
66
+ }, [openMenuFor]);
67
+ const moveItem = (id, toColumn) => {
68
+ setOpenMenuFor(null);
69
+ onMove?.({ id, toColumn });
70
+ };
71
+ const knownKeys = new Set(columns.map((column) => column.key));
72
+ const orphanCount = items.filter((item) => !knownKeys.has(item.column)).length;
73
+ return /* @__PURE__ */ jsxs("div", { children: [
74
+ /* @__PURE__ */ jsx(BoardStyles, {}),
75
+ /* @__PURE__ */ jsx("div", { className: "erp-colboard", children: columns.map((column) => {
76
+ const tone = column.tone ?? "neutral";
77
+ const columnItems = items.filter((item) => item.column === column.key);
78
+ return /* @__PURE__ */ jsxs("section", { className: "erp-colboard__col", "aria-label": column.label, children: [
79
+ /* @__PURE__ */ jsxs("header", { className: "erp-colboard__head", style: toneStyle(tone), children: [
80
+ /* @__PURE__ */ jsx("span", { className: "erp-colboard__head-label", children: column.label }),
81
+ /* @__PURE__ */ jsx(Badge, { tone, children: columnItems.length })
82
+ ] }),
83
+ /* @__PURE__ */ jsx("div", { className: "erp-colboard__body", children: columnItems.length === 0 ? /* @__PURE__ */ jsx("div", { className: "wp-caption erp-colboard__empty", children: "\uD56D\uBAA9 \uC5C6\uC74C" }) : columnItems.map((item) => {
84
+ const isSelected = selectable && selected.includes(item.id);
85
+ const targets = columns.filter((target) => target.key !== column.key);
86
+ const mainContent = /* @__PURE__ */ jsxs(Fragment, { children: [
87
+ /* @__PURE__ */ jsxs("span", { className: "erp-colboard__trow", children: [
88
+ /* @__PURE__ */ jsx("span", { className: "erp-colboard__title", children: item.title }),
89
+ item.badge ? /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: item.badge }) : null
90
+ ] }),
91
+ item.sub ? /* @__PURE__ */ jsx("span", { className: "wp-caption erp-colboard__ellip", children: item.sub }) : null
92
+ ] });
93
+ return /* @__PURE__ */ jsxs(
94
+ "div",
95
+ {
96
+ className: isSelected ? "erp-colboard__card erp-colboard__card--selected" : "erp-colboard__card",
97
+ children: [
98
+ selectable ? /* @__PURE__ */ jsx(
99
+ Checkbox,
100
+ {
101
+ checked: selected.includes(item.id),
102
+ onChange: () => toggleSelect(item.id),
103
+ "aria-label": `${item.title} \uC120\uD0DD`
104
+ }
105
+ ) : null,
106
+ onItemClick ? /* @__PURE__ */ jsx(
107
+ "button",
108
+ {
109
+ type: "button",
110
+ className: "erp-colboard__main erp-colboard__main--click",
111
+ onClick: () => onItemClick(item),
112
+ children: mainContent
113
+ }
114
+ ) : /* @__PURE__ */ jsx("div", { className: "erp-colboard__main", children: mainContent }),
115
+ /* @__PURE__ */ jsxs("div", { className: "erp-popover-host erp-colboard__menu", children: [
116
+ /* @__PURE__ */ jsx(
117
+ IconButton,
118
+ {
119
+ icon: "chevron-down",
120
+ size: "xs",
121
+ label: onMove ? `${item.title} \u2014 \uB2E4\uB978 \uCE78\uC73C\uB85C \uC774\uB3D9` : "\uC774\uB3D9 \uBD88\uAC00 \u2014 \uC774\uB3D9 \uB3D9\uC791\uC774 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4",
122
+ disabled: !onMove,
123
+ "aria-haspopup": "menu",
124
+ "aria-expanded": openMenuFor === item.id,
125
+ onClick: () => setOpenMenuFor((current) => current === item.id ? null : item.id)
126
+ }
127
+ ),
128
+ openMenuFor === item.id && onMove ? /* @__PURE__ */ jsx("div", { className: "erp-popover", style: { minWidth: 180, width: "max-content" }, children: /* @__PURE__ */ jsxs(
129
+ "div",
130
+ {
131
+ className: "erp-popover__body",
132
+ role: "menu",
133
+ style: { padding: "var(--wp-space-1)" },
134
+ children: [
135
+ /* @__PURE__ */ jsx(
136
+ "div",
137
+ {
138
+ className: "wp-overline",
139
+ style: { padding: "var(--wp-space-1) var(--wp-space-3)" },
140
+ children: "\uC774\uB3D9\uD560 \uCE78"
141
+ }
142
+ ),
143
+ targets.length === 0 ? /* @__PURE__ */ jsx(
144
+ "div",
145
+ {
146
+ className: "wp-caption",
147
+ style: { padding: "var(--wp-space-2) var(--wp-space-3)" },
148
+ children: "\uC774\uB3D9\uD560 \uB2E4\uB978 \uCE78\uC774 \uC5C6\uC2B5\uB2C8\uB2E4"
149
+ }
150
+ ) : targets.map((target) => /* @__PURE__ */ jsxs(
151
+ "button",
152
+ {
153
+ type: "button",
154
+ role: "menuitem",
155
+ className: "erp-colboard__opt",
156
+ onClick: () => moveItem(item.id, target.key),
157
+ children: [
158
+ /* @__PURE__ */ jsx(
159
+ Icon,
160
+ {
161
+ name: "arrow-right",
162
+ size: 14,
163
+ style: { color: "var(--wp-text-subtle)", flex: "none" }
164
+ }
165
+ ),
166
+ target.label
167
+ ]
168
+ },
169
+ target.key
170
+ ))
171
+ ]
172
+ }
173
+ ) }) : null
174
+ ] })
175
+ ]
176
+ },
177
+ item.id
178
+ );
179
+ }) })
180
+ ] }, column.key);
181
+ }) }),
182
+ orphanCount > 0 ? /* @__PURE__ */ jsxs("p", { className: "wp-caption", style: { margin: "var(--wp-space-2) 0 0" }, children: [
183
+ "\uBBF8\uBD84\uB958 \uD56D\uBAA9 ",
184
+ orphanCount,
185
+ "\uAC74 \u2014 \uCEEC\uB7FC \uC815\uC758\uC5D0 \uC5C6\uB294 \uBD84\uB958\uC785\uB2C8\uB2E4"
186
+ ] }) : null
187
+ ] });
188
+ }
189
+
190
+ export { ColumnBoard };
@@ -0,0 +1,78 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/widgets/LiveClock.tsx
5
+ var PLACEHOLDER = "--:--:--";
6
+ var WEEKDAYS = ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"];
7
+ function pad2(value) {
8
+ return String(value).padStart(2, "0");
9
+ }
10
+ function formatClock(date) {
11
+ return `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
12
+ }
13
+ function formatDate(date) {
14
+ const weekday = WEEKDAYS[date.getDay()] ?? "";
15
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} (${weekday})`;
16
+ }
17
+ function formatElapsed(elapsedMs) {
18
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
19
+ const hours = Math.floor(totalSeconds / 3600);
20
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
21
+ const seconds = totalSeconds % 60;
22
+ return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
23
+ }
24
+ function subscribeEverySecond(onTick) {
25
+ const timer = window.setInterval(onTick, 1e3);
26
+ return () => window.clearInterval(timer);
27
+ }
28
+ function getSecondSnapshot() {
29
+ return Math.floor(Date.now() / 1e3);
30
+ }
31
+ function getServerSnapshot() {
32
+ return null;
33
+ }
34
+ function LiveClock({ startedAt, showDate = false }) {
35
+ const nowSeconds = useSyncExternalStore(
36
+ subscribeEverySecond,
37
+ getSecondSnapshot,
38
+ getServerSnapshot
39
+ );
40
+ const now = nowSeconds === null ? null : nowSeconds * 1e3;
41
+ const isElapsedMode = startedAt !== void 0;
42
+ const startedMs = startedAt !== void 0 ? Date.parse(startedAt) : Number.NaN;
43
+ const startInvalid = isElapsedMode && Number.isNaN(startedMs);
44
+ let display = PLACEHOLDER;
45
+ let dateLine = null;
46
+ if (now !== null) {
47
+ if (isElapsedMode) {
48
+ display = startInvalid ? PLACEHOLDER : formatElapsed(now - startedMs);
49
+ if (showDate && !startInvalid) {
50
+ const started = new Date(startedMs);
51
+ dateLine = `\uC2DC\uC791 ${formatDate(started)} ${formatClock(started)}`;
52
+ }
53
+ } else {
54
+ const current = new Date(now);
55
+ display = formatClock(current);
56
+ if (showDate) dateLine = formatDate(current);
57
+ }
58
+ }
59
+ return /* @__PURE__ */ jsxs(
60
+ "div",
61
+ {
62
+ style: {
63
+ display: "inline-flex",
64
+ flexDirection: "column",
65
+ alignItems: "flex-start",
66
+ gap: "var(--wp-space-1)"
67
+ },
68
+ children: [
69
+ isElapsedMode ? /* @__PURE__ */ jsx("span", { className: "wp-overline", children: "\uACBD\uACFC \uC2DC\uAC04" }) : null,
70
+ dateLine !== null ? /* @__PURE__ */ jsx("span", { className: "wp-caption", children: dateLine }) : null,
71
+ /* @__PURE__ */ jsx("time", { className: "wp-metric", "aria-live": "off", children: display }),
72
+ startInvalid ? /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-danger)" }, children: "\uC2DC\uC791 \uC2DC\uAC01 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4" }) : null
73
+ ]
74
+ }
75
+ );
76
+ }
77
+
78
+ export { LiveClock };
@@ -0,0 +1,309 @@
1
+ import { IconButton } from './chunk-ZUCI34QY.js';
2
+ import { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ var MIN_SCALE = 1;
7
+ var MAX_SCALE = 8;
8
+ var BUTTON_ZOOM_STEP = 1.5;
9
+ var DRAG_CLOSE_THRESHOLD = 4;
10
+ var INITIAL_VIEW = { scale: 1, x: 0, y: 0 };
11
+ function clamp(value, min, max) {
12
+ return Math.min(max, Math.max(min, value));
13
+ }
14
+ function ImageLightbox({ src, alt, thumbClassName, thumbStyle, children }) {
15
+ const [open, setOpen] = useState(false);
16
+ const [view, setView] = useState(INITIAL_VIEW);
17
+ const [dragging, setDragging] = useState(false);
18
+ const stageRef = useRef(null);
19
+ const imgRef = useRef(null);
20
+ const viewRef = useRef(view);
21
+ viewRef.current = view;
22
+ const dragRef = useRef(null);
23
+ const openViewer = (event) => {
24
+ event.stopPropagation();
25
+ setView(INITIAL_VIEW);
26
+ setOpen(true);
27
+ };
28
+ const close = useCallback(() => {
29
+ dragRef.current = null;
30
+ setDragging(false);
31
+ setOpen(false);
32
+ }, []);
33
+ const clampOffset = useCallback((x, y, scale) => {
34
+ const stage = stageRef.current;
35
+ const img = imgRef.current;
36
+ if (!stage || !img) return { x: 0, y: 0 };
37
+ const rect = stage.getBoundingClientRect();
38
+ const scaledW = img.clientWidth * scale;
39
+ const scaledH = img.clientHeight * scale;
40
+ const maxX = Math.max(0, (scaledW - rect.width) / 2);
41
+ const maxY = Math.max(0, (scaledH - rect.height) / 2);
42
+ return { x: clamp(x, -maxX, maxX), y: clamp(y, -maxY, maxY) };
43
+ }, []);
44
+ const zoomAt = useCallback(
45
+ (clientX, clientY, factor) => {
46
+ const stage = stageRef.current;
47
+ if (!stage) return;
48
+ const prev = viewRef.current;
49
+ const nextScale = clamp(prev.scale * factor, MIN_SCALE, MAX_SCALE);
50
+ if (nextScale === prev.scale) return;
51
+ const rect = stage.getBoundingClientRect();
52
+ const cx = clientX - rect.left - rect.width / 2;
53
+ const cy = clientY - rect.top - rect.height / 2;
54
+ const px = (cx - prev.x) / prev.scale;
55
+ const py = (cy - prev.y) / prev.scale;
56
+ const next = clampOffset(cx - px * nextScale, cy - py * nextScale, nextScale);
57
+ setView({ scale: nextScale, x: next.x, y: next.y });
58
+ },
59
+ [clampOffset]
60
+ );
61
+ const zoomAtCenter = useCallback(
62
+ (factor) => {
63
+ const stage = stageRef.current;
64
+ if (!stage) return;
65
+ const rect = stage.getBoundingClientRect();
66
+ zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, factor);
67
+ },
68
+ [zoomAt]
69
+ );
70
+ const resetView = useCallback(() => setView(INITIAL_VIEW), []);
71
+ useEffect(() => {
72
+ if (!open) return;
73
+ const onKeyDown = (event) => {
74
+ if (event.key === "Escape") {
75
+ event.stopPropagation();
76
+ close();
77
+ }
78
+ };
79
+ document.addEventListener("keydown", onKeyDown);
80
+ const previousOverflow = document.body.style.overflow;
81
+ document.body.style.overflow = "hidden";
82
+ return () => {
83
+ document.removeEventListener("keydown", onKeyDown);
84
+ document.body.style.overflow = previousOverflow;
85
+ };
86
+ }, [open, close]);
87
+ useEffect(() => {
88
+ if (!open) return;
89
+ const stage = stageRef.current;
90
+ if (!stage) return;
91
+ const onWheel = (event) => {
92
+ event.preventDefault();
93
+ event.stopPropagation();
94
+ zoomAt(event.clientX, event.clientY, Math.exp(-event.deltaY * 2e-3));
95
+ };
96
+ stage.addEventListener("wheel", onWheel, { passive: false });
97
+ return () => stage.removeEventListener("wheel", onWheel);
98
+ }, [open, zoomAt]);
99
+ const onStagePointerDown = (event) => {
100
+ if (viewRef.current.scale <= MIN_SCALE) return;
101
+ event.preventDefault();
102
+ event.currentTarget.setPointerCapture(event.pointerId);
103
+ dragRef.current = {
104
+ pointerId: event.pointerId,
105
+ startX: event.clientX,
106
+ startY: event.clientY,
107
+ originX: viewRef.current.x,
108
+ originY: viewRef.current.y,
109
+ moved: false
110
+ };
111
+ setDragging(true);
112
+ };
113
+ const onStagePointerMove = (event) => {
114
+ const drag = dragRef.current;
115
+ if (!drag || event.pointerId !== drag.pointerId) return;
116
+ const dx = event.clientX - drag.startX;
117
+ const dy = event.clientY - drag.startY;
118
+ if (Math.abs(dx) + Math.abs(dy) > DRAG_CLOSE_THRESHOLD) drag.moved = true;
119
+ const next = clampOffset(drag.originX + dx, drag.originY + dy, viewRef.current.scale);
120
+ setView((prev) => ({ ...prev, x: next.x, y: next.y }));
121
+ };
122
+ const endDrag = (event) => {
123
+ const drag = dragRef.current;
124
+ if (!drag || event.pointerId !== drag.pointerId) return;
125
+ try {
126
+ event.currentTarget.releasePointerCapture(event.pointerId);
127
+ } catch {
128
+ }
129
+ setTimeout(() => {
130
+ dragRef.current = null;
131
+ }, 0);
132
+ setDragging(false);
133
+ };
134
+ const onStageClick = (event) => {
135
+ if (dragRef.current?.moved) return;
136
+ if (event.target === event.currentTarget) close();
137
+ };
138
+ const zoomPercent = Math.round(view.scale * 100);
139
+ const isInitial = view.scale === 1 && view.x === 0 && view.y === 0;
140
+ const overlay = open ? createPortal(
141
+ /* @__PURE__ */ jsxs(
142
+ "div",
143
+ {
144
+ role: "dialog",
145
+ "aria-modal": "true",
146
+ "aria-label": alt ?? "\uC774\uBBF8\uC9C0 \uBCF4\uAE30",
147
+ style: {
148
+ position: "fixed",
149
+ inset: 0,
150
+ zIndex: "var(--wp-z-dialog)",
151
+ background: "var(--wp-surface-scrim)"
152
+ },
153
+ onClick: (event) => event.stopPropagation(),
154
+ onPointerDown: (event) => event.stopPropagation(),
155
+ onDoubleClick: (event) => event.stopPropagation(),
156
+ children: [
157
+ /* @__PURE__ */ jsx(
158
+ "div",
159
+ {
160
+ ref: stageRef,
161
+ style: {
162
+ position: "absolute",
163
+ inset: 0,
164
+ overflow: "hidden",
165
+ display: "flex",
166
+ alignItems: "center",
167
+ justifyContent: "center",
168
+ touchAction: "none",
169
+ cursor: view.scale > MIN_SCALE ? dragging ? "grabbing" : "grab" : "default"
170
+ },
171
+ onClick: onStageClick,
172
+ onPointerDown: onStagePointerDown,
173
+ onPointerMove: onStagePointerMove,
174
+ onPointerUp: endDrag,
175
+ onPointerCancel: endDrag,
176
+ children: /* @__PURE__ */ jsx(
177
+ "img",
178
+ {
179
+ ref: imgRef,
180
+ src,
181
+ alt: alt ?? "",
182
+ draggable: false,
183
+ style: {
184
+ maxWidth: "calc(100vw - 96px)",
185
+ maxHeight: "calc(100vh - 96px)",
186
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
187
+ userSelect: "none",
188
+ boxShadow: "var(--wp-shadow-xl)",
189
+ borderRadius: view.scale === 1 ? "var(--wp-radius-sm)" : 0
190
+ }
191
+ }
192
+ )
193
+ }
194
+ ),
195
+ /* @__PURE__ */ jsxs(
196
+ "div",
197
+ {
198
+ style: {
199
+ position: "absolute",
200
+ top: "var(--wp-space-4)",
201
+ right: "var(--wp-space-4)",
202
+ display: "flex",
203
+ alignItems: "center",
204
+ gap: "var(--wp-space-1)",
205
+ padding: "var(--wp-space-1)",
206
+ background: "var(--wp-surface-card)",
207
+ borderRadius: "var(--wp-radius-pill)",
208
+ boxShadow: "var(--wp-shadow-lg)"
209
+ },
210
+ children: [
211
+ /* @__PURE__ */ jsxs(
212
+ "span",
213
+ {
214
+ className: "wp-mono-xs",
215
+ "aria-live": "polite",
216
+ style: {
217
+ minWidth: 44,
218
+ textAlign: "center",
219
+ color: "var(--wp-text-muted)",
220
+ fontVariantNumeric: "tabular-nums"
221
+ },
222
+ children: [
223
+ zoomPercent,
224
+ "%"
225
+ ]
226
+ }
227
+ ),
228
+ /* @__PURE__ */ jsx(
229
+ IconButton,
230
+ {
231
+ icon: "zoom-out",
232
+ label: "\uCD95\uC18C",
233
+ size: "sm",
234
+ disabled: view.scale <= MIN_SCALE,
235
+ onClick: () => zoomAtCenter(1 / BUTTON_ZOOM_STEP)
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsx(
239
+ IconButton,
240
+ {
241
+ icon: "zoom-in",
242
+ label: "\uD655\uB300",
243
+ size: "sm",
244
+ disabled: view.scale >= MAX_SCALE,
245
+ onClick: () => zoomAtCenter(BUTTON_ZOOM_STEP)
246
+ }
247
+ ),
248
+ /* @__PURE__ */ jsx(
249
+ IconButton,
250
+ {
251
+ icon: "rotate-ccw",
252
+ label: "\uC6D0\uB798 \uD06C\uAE30",
253
+ size: "sm",
254
+ disabled: isInitial,
255
+ onClick: resetView
256
+ }
257
+ ),
258
+ /* @__PURE__ */ jsx(IconButton, { icon: "x", label: "\uB2EB\uAE30", size: "sm", onClick: close })
259
+ ]
260
+ }
261
+ )
262
+ ]
263
+ }
264
+ ),
265
+ document.body
266
+ ) : null;
267
+ if (children) {
268
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
269
+ /* @__PURE__ */ jsx(
270
+ "span",
271
+ {
272
+ style: { display: "contents" },
273
+ onClick: openViewer,
274
+ onKeyDown: (event) => {
275
+ if (event.key === "Enter" || event.key === " ") {
276
+ event.preventDefault();
277
+ openViewer(event);
278
+ }
279
+ },
280
+ children
281
+ }
282
+ ),
283
+ overlay
284
+ ] });
285
+ }
286
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
287
+ /* @__PURE__ */ jsx(
288
+ "img",
289
+ {
290
+ src,
291
+ alt: alt ?? "",
292
+ role: "button",
293
+ tabIndex: 0,
294
+ className: thumbClassName,
295
+ style: { cursor: "zoom-in", ...thumbStyle },
296
+ onClick: openViewer,
297
+ onKeyDown: (event) => {
298
+ if (event.key === "Enter" || event.key === " ") {
299
+ event.preventDefault();
300
+ openViewer(event);
301
+ }
302
+ }
303
+ }
304
+ ),
305
+ overlay
306
+ ] });
307
+ }
308
+
309
+ export { ImageLightbox };