@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.
- package/LICENSE +21 -0
- package/NOTICE +27 -0
- package/dist/chunk-4SU2KYPU.js +59 -0
- package/dist/chunk-56KKWLNI.js +186 -0
- package/dist/chunk-7Y4J7EOK.js +115 -0
- package/dist/chunk-APCXOCAO.js +110 -0
- package/dist/chunk-AROOQ5W6.js +119 -0
- package/dist/chunk-DGPRX4MU.js +251 -0
- package/dist/chunk-IL2KS4WS.js +25 -0
- package/dist/chunk-KADFN76X.js +541 -0
- package/dist/chunk-KBHI3IUR.js +40 -0
- package/dist/chunk-LZJTKVMO.js +767 -0
- package/dist/chunk-MOIU7MUA.js +282 -0
- package/dist/chunk-MSAV4J4I.js +77 -0
- package/dist/chunk-OOHHW4K4.js +159 -0
- package/dist/chunk-Q6SBH63R.js +190 -0
- package/dist/chunk-QZQDHSMD.js +78 -0
- package/dist/chunk-RH7ESUE6.js +309 -0
- package/dist/chunk-SEBPBUZR.js +260 -0
- package/dist/chunk-USKRM22W.js +39 -0
- package/dist/chunk-WNYWTFGW.js +61 -0
- package/dist/chunk-WVRCIVAD.js +72 -0
- package/dist/chunk-YJMD6MOE.js +404 -0
- package/dist/chunk-ZNITJ4KK.js +433 -0
- package/dist/chunk-ZQR3ULVH.js +193 -0
- package/dist/{chunk-QJHD6HVQ.js → chunk-ZUCI34QY.js} +106 -390
- package/dist/index.d.ts +306 -53
- package/dist/index.js +276 -720
- package/dist/meta.d.ts +8 -0
- package/dist/meta.js +147 -8
- package/dist/rich.js +3 -2
- package/dist/styles.css +246 -0
- package/dist/widgets/AutocompleteField.d.ts +96 -0
- package/dist/widgets/AutocompleteField.js +3 -0
- package/dist/widgets/ColumnBoard.d.ts +47 -0
- package/dist/widgets/ColumnBoard.js +2 -0
- package/dist/widgets/DiffView.d.ts +30 -0
- package/dist/widgets/DiffView.js +2 -0
- package/dist/widgets/Disclosure.d.ts +20 -0
- package/dist/widgets/Disclosure.js +2 -0
- package/dist/widgets/EventCalendar.d.ts +59 -0
- package/dist/widgets/EventCalendar.js +2 -0
- package/dist/widgets/ImageLightbox.d.ts +25 -0
- package/dist/widgets/ImageLightbox.js +2 -0
- package/dist/widgets/InlineEdit.d.ts +23 -0
- package/dist/widgets/InlineEdit.js +2 -0
- package/dist/widgets/JsonField.d.ts +24 -0
- package/dist/widgets/JsonField.js +2 -0
- package/dist/widgets/LiveClock.d.ts +21 -0
- package/dist/widgets/LiveClock.js +1 -0
- package/dist/widgets/OrgChart.d.ts +32 -0
- package/dist/widgets/OrgChart.js +2 -0
- package/dist/widgets/PeriodFilter.d.ts +48 -0
- package/dist/widgets/PeriodFilter.js +2 -0
- package/dist/widgets/PrintSheet.d.ts +23 -0
- package/dist/widgets/PrintSheet.js +2 -0
- package/dist/widgets/SearchableMultiPicker.d.ts +29 -0
- package/dist/widgets/SearchableMultiPicker.js +3 -0
- package/dist/widgets/SecureScreen.d.ts +25 -0
- package/dist/widgets/SecureScreen.js +2 -0
- package/dist/widgets/StackedMeter.d.ts +26 -0
- package/dist/widgets/StackedMeter.js +1 -0
- package/dist/widgets/Strip.d.ts +20 -0
- package/dist/widgets/Strip.js +1 -0
- package/dist/widgets/SummaryTable.d.ts +31 -0
- package/dist/widgets/SummaryTable.js +1 -0
- package/dist/widgets/TagMultiSelect.d.ts +25 -0
- package/dist/widgets/TagMultiSelect.js +2 -0
- package/dist/widgets/Timeline.d.ts +30 -0
- package/dist/widgets/Timeline.js +3 -0
- package/dist/widgets/TreePicker.d.ts +21 -0
- package/dist/widgets/TreePicker.js +3 -0
- package/dist/widgets/TreeView.d.ts +26 -0
- package/dist/widgets/TreeView.js +2 -0
- package/dist/widgets/index.d.ts +76 -0
- package/dist/widgets/index.js +69 -0
- 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 };
|