klun-ui 0.1.8 → 0.1.10

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/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to **klun-ui** are documented here. The format follows
5
5
  [Semantic Versioning](https://semver.org/) (pre-1.0: minor/patch bumps may carry
6
6
  small breaking changes).
7
7
 
8
+ ## [0.1.10] — 2026-06-21
9
+
10
+ ### Added
11
+ - **`ImageViewer` — a full-screen image lightbox.** Single image or gallery
12
+ (prev/next + counter), wheel & button zoom, rotate, drag-to-pan when zoomed,
13
+ double-click to toggle fit/100%, optional download, and keyboard control
14
+ (Esc / ←→ / `+` `-` / `0`). Portals to `<body>` and locks body scroll.
15
+ - **`ContextMenu` + `useContextMenu` — a cursor-positioned right-click menu.**
16
+ Nested submenus via `item.children` (open on hover or `→`, any depth, each
17
+ level flips to stay on-screen), clamps/flips to the viewport, keyboard
18
+ navigation (↑ ↓ → ← Enter Esc Home End), disabled items, shortcuts, dividers
19
+ and danger items. The `useContextMenu` hook wires up `onContextMenu` →
20
+ open-at-cursor.
21
+
22
+ ## [0.1.9] — 2026-06-21
23
+
24
+ ### Fixed
25
+ - **`Popconfirm` bubble is no longer clipped by scroll containers.** The bubble
26
+ rendered as an `absolute` child of the trigger, so when the trigger sat inside
27
+ a scrollable/`overflow` pane (e.g. an app content area) the bubble was clipped
28
+ at that pane's edge — near the left edge its title/description got cut off. It
29
+ is now portaled to `<body>`, positioned with fixed viewport coordinates, and
30
+ clamped to stay fully on-screen (recomputed on scroll/resize). Outside-click
31
+ and Escape dismissal still work across the portal boundary.
32
+
8
33
  ## [0.1.8] — 2026-06-21
9
34
 
10
35
  ### Fixed
@@ -4372,9 +4372,9 @@ function useExpandedRows({
4372
4372
  list.forEach((row, i) => {
4373
4373
  const id = getRowId(row, i);
4374
4374
  const kids = row[childrenKey];
4375
- const hasChildren = Array.isArray(kids) && kids.length > 0;
4376
- out.push({ row, id, depth, hasChildren });
4377
- if (hasChildren && treeOpen.has(id)) walk(kids, depth + 1);
4375
+ const hasChildren2 = Array.isArray(kids) && kids.length > 0;
4376
+ out.push({ row, id, depth, hasChildren: hasChildren2 });
4377
+ if (hasChildren2 && treeOpen.has(id)) walk(kids, depth + 1);
4378
4378
  });
4379
4379
  };
4380
4380
  walk(pageRows, 0);
@@ -4912,7 +4912,7 @@ var Tree = react.forwardRef(function Tree2({
4912
4912
  className: chunkTPGAXYFU_cjs.cx("klun-tree", showLine && "klun-tree--line", className),
4913
4913
  ...props,
4914
4914
  children: rows.filter((r) => !r.hidden).map(({ node, level }) => {
4915
- const hasChildren = !!(node.children && node.children.length);
4915
+ const hasChildren2 = !!(node.children && node.children.length);
4916
4916
  const isExpanded = exp.includes(node.key);
4917
4917
  const selected = selectedKey === node.key;
4918
4918
  const cs = checkable ? checkState(node) : null;
@@ -4920,7 +4920,7 @@ var Tree = react.forwardRef(function Tree2({
4920
4920
  "div",
4921
4921
  {
4922
4922
  role: "treeitem",
4923
- "aria-expanded": hasChildren ? isExpanded : void 0,
4923
+ "aria-expanded": hasChildren2 ? isExpanded : void 0,
4924
4924
  "aria-selected": selected,
4925
4925
  className: chunkTPGAXYFU_cjs.cx(
4926
4926
  "klun-tree__item",
@@ -4939,12 +4939,12 @@ var Tree = react.forwardRef(function Tree2({
4939
4939
  {
4940
4940
  className: chunkTPGAXYFU_cjs.cx(
4941
4941
  "klun-tree__caret",
4942
- hasChildren && isExpanded && "klun-tree__caret--expanded"
4942
+ hasChildren2 && isExpanded && "klun-tree__caret--expanded"
4943
4943
  ),
4944
- "data-leaf": hasChildren ? void 0 : "",
4944
+ "data-leaf": hasChildren2 ? void 0 : "",
4945
4945
  onClick: (e) => {
4946
4946
  e.stopPropagation();
4947
- if (hasChildren) toggle(node.key);
4947
+ if (hasChildren2) toggle(node.key);
4948
4948
  },
4949
4949
  children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-arrow-right-s-line" })
4950
4950
  }
@@ -5920,7 +5920,7 @@ var Cascader = react.forwardRef(function Cascader2({
5920
5920
  ),
5921
5921
  children: col.map((opt) => {
5922
5922
  const selected = active[ci] === opt.value;
5923
- const hasChildren = !!(opt.children && opt.children.length);
5923
+ const hasChildren2 = !!(opt.children && opt.children.length);
5924
5924
  return /* @__PURE__ */ jsxRuntime.jsxs(
5925
5925
  "button",
5926
5926
  {
@@ -5931,7 +5931,7 @@ var Cascader = react.forwardRef(function Cascader2({
5931
5931
  "data-selected": selected || void 0,
5932
5932
  children: [
5933
5933
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-cascader__option-label", children: opt.label }),
5934
- hasChildren ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-arrow-right-s-line klun-cascader__option-arrow" }) : selected ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-check-line klun-cascader__option-check" }) : null
5934
+ hasChildren2 ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-arrow-right-s-line klun-cascader__option-arrow" }) : selected ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-check-line klun-cascader__option-check" }) : null
5935
5935
  ]
5936
5936
  },
5937
5937
  opt.value
@@ -9838,6 +9838,203 @@ function Confirm(opts = {}) {
9838
9838
  root.render(/* @__PURE__ */ jsxRuntime.jsx(ConfirmDialog, { opts, onClose: close }));
9839
9839
  });
9840
9840
  }
9841
+ var isSelectable = (it) => !it.divider && !it.disabled;
9842
+ var hasChildren = (it) => !!it.children && it.children.length > 0;
9843
+ var GAP2 = 8;
9844
+ function place(w, h, opts) {
9845
+ const vw = window.innerWidth;
9846
+ const vh = window.innerHeight;
9847
+ let left;
9848
+ let top;
9849
+ if (opts.rect) {
9850
+ const r = opts.rect;
9851
+ left = r.right - 4 + w > vw - GAP2 ? r.left - w + 4 : r.right - 4;
9852
+ top = r.top - 4;
9853
+ } else {
9854
+ const p = opts.point ?? { x: 0, y: 0 };
9855
+ left = p.x + w > vw - GAP2 ? p.x - w : p.x;
9856
+ top = p.y + h > vh - GAP2 ? p.y - h : p.y;
9857
+ }
9858
+ left = Math.min(Math.max(GAP2, left), vw - w - GAP2);
9859
+ top = Math.min(Math.max(GAP2, top), vh - h - GAP2);
9860
+ return { left, top };
9861
+ }
9862
+ function MenuPanel({ items: items2, width, className, point, anchorEl, autoActive, depth, onCloseAll, onCloseSelf }) {
9863
+ const ref = react.useRef(null);
9864
+ const itemEls = react.useRef([]);
9865
+ const [pos, setPos] = react.useState(null);
9866
+ const [active, setActive] = react.useState(autoActive ? items2.findIndex(isSelectable) : -1);
9867
+ const [sub, setSub] = react.useState(null);
9868
+ const isLeaf = sub === null;
9869
+ react.useLayoutEffect(() => {
9870
+ const el = ref.current;
9871
+ const w = el?.offsetWidth ?? width;
9872
+ const h = el?.offsetHeight ?? 0;
9873
+ setPos(place(w, h, anchorEl ? { rect: anchorEl.getBoundingClientRect() } : { point }));
9874
+ }, [width, point?.x, point?.y, anchorEl, items2]);
9875
+ const move = react.useCallback(
9876
+ (dir) => {
9877
+ setActive((cur) => {
9878
+ const n = items2.length;
9879
+ for (let s = 1; s <= n; s++) {
9880
+ const i = (cur + dir * s + n * s) % n;
9881
+ if (isSelectable(items2[i])) return i;
9882
+ }
9883
+ return cur;
9884
+ });
9885
+ },
9886
+ [items2]
9887
+ );
9888
+ const activate = react.useCallback(
9889
+ (i, viaKeyboard) => {
9890
+ const it = items2[i];
9891
+ if (!it || !isSelectable(it)) return;
9892
+ if (hasChildren(it)) setSub({ i, kbd: viaKeyboard });
9893
+ else {
9894
+ onCloseAll();
9895
+ it.onClick?.();
9896
+ }
9897
+ },
9898
+ [items2, onCloseAll]
9899
+ );
9900
+ react.useEffect(() => {
9901
+ if (!isLeaf) return;
9902
+ const onKey = (e) => {
9903
+ if (e.key === "ArrowDown") {
9904
+ e.preventDefault();
9905
+ move(1);
9906
+ } else if (e.key === "ArrowUp") {
9907
+ e.preventDefault();
9908
+ move(-1);
9909
+ } else if (e.key === "Home") {
9910
+ e.preventDefault();
9911
+ setActive(items2.findIndex(isSelectable));
9912
+ } else if (e.key === "End") {
9913
+ e.preventDefault();
9914
+ for (let i = items2.length - 1; i >= 0; i--)
9915
+ if (isSelectable(items2[i])) {
9916
+ setActive(i);
9917
+ break;
9918
+ }
9919
+ } else if (e.key === "ArrowRight") {
9920
+ if (active >= 0 && hasChildren(items2[active])) {
9921
+ e.preventDefault();
9922
+ setSub({ i: active, kbd: true });
9923
+ }
9924
+ } else if (e.key === "ArrowLeft") {
9925
+ if (depth > 0) {
9926
+ e.preventDefault();
9927
+ onCloseSelf?.();
9928
+ }
9929
+ } else if (e.key === "Enter") {
9930
+ if (active >= 0) {
9931
+ e.preventDefault();
9932
+ activate(active, true);
9933
+ }
9934
+ } else if (e.key === "Escape") {
9935
+ e.preventDefault();
9936
+ onCloseAll();
9937
+ }
9938
+ };
9939
+ window.addEventListener("keydown", onKey, true);
9940
+ return () => window.removeEventListener("keydown", onKey, true);
9941
+ }, [isLeaf, active, items2, depth, move, activate, onCloseAll, onCloseSelf]);
9942
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9943
+ "div",
9944
+ {
9945
+ ref,
9946
+ role: "menu",
9947
+ className: chunkTPGAXYFU_cjs.cx("klun-context-menu", className),
9948
+ style: pos ? { left: pos.left, top: pos.top, width } : { left: -9999, top: -9999, width },
9949
+ onMouseDown: (e) => e.stopPropagation(),
9950
+ children: [
9951
+ items2.map(
9952
+ (it, i) => it.divider ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-context-menu__divider", role: "separator" }, i) : /* @__PURE__ */ jsxRuntime.jsxs(
9953
+ "button",
9954
+ {
9955
+ ref: (el) => {
9956
+ itemEls.current[i] = el;
9957
+ },
9958
+ type: "button",
9959
+ role: "menuitem",
9960
+ "aria-haspopup": hasChildren(it) || void 0,
9961
+ "aria-expanded": hasChildren(it) ? sub?.i === i : void 0,
9962
+ disabled: it.disabled,
9963
+ className: chunkTPGAXYFU_cjs.cx(
9964
+ "klun-context-menu__item",
9965
+ it.danger && "klun-context-menu__item--danger",
9966
+ i === active && "is-active"
9967
+ ),
9968
+ onMouseEnter: () => {
9969
+ setActive(i);
9970
+ if (isSelectable(it)) setSub(hasChildren(it) ? { i, kbd: false } : null);
9971
+ },
9972
+ onClick: () => activate(i, false),
9973
+ children: [
9974
+ it.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-context-menu__icon", children: it.icon }) : null,
9975
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-context-menu__label", children: it.label }),
9976
+ hasChildren(it) ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-arrow-right-s-line klun-context-menu__arrow" }) : it.shortcut ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-context-menu__shortcut", children: it.shortcut }) : null
9977
+ ]
9978
+ },
9979
+ i
9980
+ )
9981
+ ),
9982
+ sub && items2[sub.i]?.children ? /* @__PURE__ */ jsxRuntime.jsx(
9983
+ MenuPanel,
9984
+ {
9985
+ items: items2[sub.i].children,
9986
+ width,
9987
+ anchorEl: itemEls.current[sub.i],
9988
+ autoActive: sub.kbd,
9989
+ depth: depth + 1,
9990
+ onCloseAll,
9991
+ onCloseSelf: () => setSub(null)
9992
+ },
9993
+ `sub-${sub.i}`
9994
+ ) : null
9995
+ ]
9996
+ }
9997
+ );
9998
+ }
9999
+ function ContextMenu2({ open, x, y, items: items2, onClose, width = 208, className }) {
10000
+ if (!open || typeof document === "undefined") return null;
10001
+ return reactDom.createPortal(
10002
+ /* @__PURE__ */ jsxRuntime.jsx(
10003
+ "div",
10004
+ {
10005
+ className: "klun-context-menu__layer",
10006
+ onMouseDown: onClose,
10007
+ onContextMenu: (e) => {
10008
+ e.preventDefault();
10009
+ onClose();
10010
+ },
10011
+ onWheel: onClose,
10012
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10013
+ MenuPanel,
10014
+ {
10015
+ items: items2,
10016
+ width,
10017
+ className,
10018
+ point: { x, y },
10019
+ depth: 0,
10020
+ onCloseAll: onClose
10021
+ }
10022
+ )
10023
+ }
10024
+ ),
10025
+ document.body
10026
+ );
10027
+ }
10028
+ ContextMenu2.displayName = "ContextMenu";
10029
+ function useContextMenu() {
10030
+ const [state, setState] = react.useState(null);
10031
+ const open = react.useCallback((e, items2) => {
10032
+ e.preventDefault();
10033
+ setState({ x: e.clientX, y: e.clientY, items: items2 });
10034
+ }, []);
10035
+ const close = react.useCallback(() => setState(null), []);
10036
+ return { state, open, close };
10037
+ }
9841
10038
  var Drawer = react.forwardRef(function Drawer2({
9842
10039
  open,
9843
10040
  onClose,
@@ -9861,8 +10058,8 @@ var Drawer = react.forwardRef(function Drawer2({
9861
10058
  ...props
9862
10059
  }, ref) {
9863
10060
  const { drawer } = useLocale();
9864
- const place = placement ?? side ?? "right";
9865
- const horizontal = place === "left" || place === "right";
10061
+ const place2 = placement ?? side ?? "right";
10062
+ const horizontal = place2 === "left" || place2 === "right";
9866
10063
  const prevOpen = react.useRef(open);
9867
10064
  react.useEffect(() => {
9868
10065
  if (!open) return;
@@ -9889,7 +10086,7 @@ var Drawer = react.forwardRef(function Drawer2({
9889
10086
  return /* @__PURE__ */ jsxRuntime.jsx(
9890
10087
  "div",
9891
10088
  {
9892
- className: chunkTPGAXYFU_cjs.cx("klun-drawer", `klun-drawer--${place}`, !mask && "klun-drawer--no-mask"),
10089
+ className: chunkTPGAXYFU_cjs.cx("klun-drawer", `klun-drawer--${place2}`, !mask && "klun-drawer--no-mask"),
9893
10090
  style: zIndex != null ? { zIndex } : void 0,
9894
10091
  onClick: maskClosable ? onClose : void 0,
9895
10092
  children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9972,6 +10169,146 @@ var Dropdown = react.forwardRef(function Dropdown2({ trigger, items: items2 = []
9972
10169
  ] });
9973
10170
  });
9974
10171
  Dropdown.displayName = "Dropdown";
10172
+ var MIN = 0.25;
10173
+ var MAX = 6;
10174
+ var clampScale = (s) => Math.min(MAX, Math.max(MIN, +s.toFixed(2)));
10175
+ function ImageViewer({
10176
+ open,
10177
+ src,
10178
+ alt,
10179
+ items: items2,
10180
+ index,
10181
+ onIndexChange,
10182
+ onClose,
10183
+ onDownload,
10184
+ title,
10185
+ zoomable = true,
10186
+ rotatable = true,
10187
+ closeOnBackdrop = true,
10188
+ className
10189
+ }) {
10190
+ const gallery = items2 && items2.length > 0 ? items2 : src != null ? [{ src, alt }] : [];
10191
+ const [innerIndex, setInnerIndex] = react.useState(0);
10192
+ const idx = index ?? innerIndex;
10193
+ const safeIdx = Math.min(Math.max(idx, 0), Math.max(0, gallery.length - 1));
10194
+ const current = gallery[safeIdx];
10195
+ const [scale, setScale] = react.useState(1);
10196
+ const [rot, setRot] = react.useState(0);
10197
+ const [pan, setPan] = react.useState({ x: 0, y: 0 });
10198
+ const drag = react.useRef(null);
10199
+ const reset = react.useCallback(() => {
10200
+ setScale(1);
10201
+ setRot(0);
10202
+ setPan({ x: 0, y: 0 });
10203
+ }, []);
10204
+ const goTo = react.useCallback(
10205
+ (next) => {
10206
+ if (gallery.length < 2) return;
10207
+ const n = (next + gallery.length) % gallery.length;
10208
+ if (index === void 0) setInnerIndex(n);
10209
+ onIndexChange?.(n);
10210
+ reset();
10211
+ },
10212
+ [gallery.length, index, onIndexChange, reset]
10213
+ );
10214
+ react.useEffect(() => {
10215
+ reset();
10216
+ }, [safeIdx, open, reset]);
10217
+ react.useEffect(() => {
10218
+ if (!open) return;
10219
+ const prev = document.body.style.overflow;
10220
+ document.body.style.overflow = "hidden";
10221
+ const onKey = (e) => {
10222
+ if (e.key === "Escape") onClose();
10223
+ else if (e.key === "ArrowLeft") goTo(safeIdx - 1);
10224
+ else if (e.key === "ArrowRight") goTo(safeIdx + 1);
10225
+ else if (zoomable && (e.key === "+" || e.key === "=")) setScale((s) => clampScale(s + 0.25));
10226
+ else if (zoomable && (e.key === "-" || e.key === "_")) setScale((s) => clampScale(s - 0.25));
10227
+ else if (e.key === "0") reset();
10228
+ };
10229
+ window.addEventListener("keydown", onKey);
10230
+ return () => {
10231
+ document.body.style.overflow = prev;
10232
+ window.removeEventListener("keydown", onKey);
10233
+ };
10234
+ }, [open, safeIdx, zoomable, onClose, goTo, reset]);
10235
+ if (!open || !current || typeof document === "undefined") return null;
10236
+ const onWheel = (e) => {
10237
+ if (!zoomable) return;
10238
+ e.preventDefault();
10239
+ setScale((s) => clampScale(s + (e.deltaY < 0 ? 0.2 : -0.2)));
10240
+ };
10241
+ const onImgDown = (e) => {
10242
+ if (scale <= 1) return;
10243
+ e.preventDefault();
10244
+ drag.current = { x: e.clientX, y: e.clientY, px: pan.x, py: pan.y };
10245
+ };
10246
+ const onMove = (e) => {
10247
+ if (!drag.current) return;
10248
+ setPan({ x: drag.current.px + (e.clientX - drag.current.x), y: drag.current.py + (e.clientY - drag.current.y) });
10249
+ };
10250
+ const endDrag = () => {
10251
+ drag.current = null;
10252
+ };
10253
+ const toggleZoom = () => scale > 1 ? reset() : setScale(2);
10254
+ const heading = title ?? current.title ?? current.alt;
10255
+ return reactDom.createPortal(
10256
+ /* @__PURE__ */ jsxRuntime.jsxs(
10257
+ "div",
10258
+ {
10259
+ className: chunkTPGAXYFU_cjs.cx("klun-image-viewer", className),
10260
+ onMouseDown: (e) => {
10261
+ if (closeOnBackdrop && e.target === e.currentTarget) onClose();
10262
+ },
10263
+ onMouseMove: onMove,
10264
+ onMouseUp: endDrag,
10265
+ onMouseLeave: endDrag,
10266
+ children: [
10267
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-image-viewer__bar", onMouseDown: (e) => e.stopPropagation(), children: [
10268
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-image-viewer__title", children: [
10269
+ heading,
10270
+ gallery.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "klun-image-viewer__counter", children: [
10271
+ safeIdx + 1,
10272
+ " / ",
10273
+ gallery.length
10274
+ ] }) : null
10275
+ ] }),
10276
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-image-viewer__tools", children: [
10277
+ zoomable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10278
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__btn", "aria-label": "Zoom out", onClick: () => setScale((s) => clampScale(s - 0.25)), children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-zoom-out-line" }) }),
10279
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "klun-image-viewer__zoom", children: [
10280
+ Math.round(scale * 100),
10281
+ "%"
10282
+ ] }),
10283
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__btn", "aria-label": "Zoom in", onClick: () => setScale((s) => clampScale(s + 0.25)), children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-zoom-in-line" }) })
10284
+ ] }) : null,
10285
+ rotatable ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__btn", "aria-label": "Rotate", onClick: () => setRot((r) => (r + 90) % 360), children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-anticlockwise-line" }) }) : null,
10286
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__btn", "aria-label": "Reset", onClick: reset, children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-aspect-ratio-line" }) }),
10287
+ onDownload ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__btn", "aria-label": "Download", onClick: () => onDownload(current, safeIdx), children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-download-2-line" }) }) : null,
10288
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__btn", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-close-line" }) })
10289
+ ] })
10290
+ ] }),
10291
+ gallery.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__nav klun-image-viewer__nav--prev", "aria-label": "Previous", onMouseDown: (e) => e.stopPropagation(), onClick: () => goTo(safeIdx - 1), children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-arrow-left-s-line" }) }) : null,
10292
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-image-viewer__stage", onMouseDown: (e) => e.stopPropagation(), onWheel, children: /* @__PURE__ */ jsxRuntime.jsx(
10293
+ "img",
10294
+ {
10295
+ className: chunkTPGAXYFU_cjs.cx("klun-image-viewer__img", scale > 1 && "is-zoomed"),
10296
+ src: current.src,
10297
+ alt: current.alt ?? "",
10298
+ draggable: false,
10299
+ onDoubleClick: toggleZoom,
10300
+ onMouseDown: onImgDown,
10301
+ style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale}) rotate(${rot}deg)` }
10302
+ }
10303
+ ) }),
10304
+ gallery.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-image-viewer__nav klun-image-viewer__nav--next", "aria-label": "Next", onMouseDown: (e) => e.stopPropagation(), onClick: () => goTo(safeIdx + 1), children: /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-arrow-right-s-line" }) }) : null
10305
+ ]
10306
+ }
10307
+ ),
10308
+ document.body
10309
+ );
10310
+ }
10311
+ ImageViewer.displayName = "ImageViewer";
9975
10312
  var Modal = react.forwardRef(function Modal2({
9976
10313
  open,
9977
10314
  onClose,
@@ -10101,6 +10438,8 @@ var Modal = react.forwardRef(function Modal2({
10101
10438
  );
10102
10439
  });
10103
10440
  Modal.displayName = "Modal";
10441
+ var GAP3 = 8;
10442
+ var MARGIN = 8;
10104
10443
  var Popconfirm = react.forwardRef(function Popconfirm2({
10105
10444
  title: titleProp,
10106
10445
  description,
@@ -10123,11 +10462,15 @@ var Popconfirm = react.forwardRef(function Popconfirm2({
10123
10462
  const cancelText = cancelTextProp ?? popconfirm.cancel;
10124
10463
  const [open, setOpen] = react.useState(false);
10125
10464
  const [busy, setBusy] = react.useState(false);
10465
+ const [pos, setPos] = react.useState(null);
10126
10466
  const innerRef = react.useRef(null);
10467
+ const bubbleRef = react.useRef(null);
10127
10468
  react.useEffect(() => {
10128
10469
  if (!open) return;
10129
10470
  const h = (e) => {
10130
- if (innerRef.current && !innerRef.current.contains(e.target)) setOpen(false);
10471
+ const t = e.target;
10472
+ if (innerRef.current?.contains(t) || bubbleRef.current?.contains(t)) return;
10473
+ setOpen(false);
10131
10474
  };
10132
10475
  const k = (e) => {
10133
10476
  if (e.key === "Escape") setOpen(false);
@@ -10139,6 +10482,51 @@ var Popconfirm = react.forwardRef(function Popconfirm2({
10139
10482
  document.removeEventListener("keydown", k);
10140
10483
  };
10141
10484
  }, [open]);
10485
+ react.useLayoutEffect(() => {
10486
+ if (!open) {
10487
+ setPos(null);
10488
+ return;
10489
+ }
10490
+ const place2 = () => {
10491
+ const trigger = innerRef.current;
10492
+ const bubble = bubbleRef.current;
10493
+ if (!trigger || !bubble) return;
10494
+ const tr = trigger.getBoundingClientRect();
10495
+ const bw = bubble.offsetWidth;
10496
+ const bh = bubble.offsetHeight;
10497
+ const vw = document.documentElement.clientWidth;
10498
+ const vh = document.documentElement.clientHeight;
10499
+ let top;
10500
+ let left;
10501
+ switch (placement) {
10502
+ case "bottom":
10503
+ top = tr.bottom + GAP3;
10504
+ left = tr.left + tr.width / 2 - bw / 2;
10505
+ break;
10506
+ case "left":
10507
+ left = tr.left - bw - GAP3;
10508
+ top = tr.top + tr.height / 2 - bh / 2;
10509
+ break;
10510
+ case "right":
10511
+ left = tr.right + GAP3;
10512
+ top = tr.top + tr.height / 2 - bh / 2;
10513
+ break;
10514
+ default:
10515
+ top = tr.top - bh - GAP3;
10516
+ left = tr.left + tr.width / 2 - bw / 2;
10517
+ }
10518
+ left = Math.max(MARGIN, Math.min(left, vw - bw - MARGIN));
10519
+ top = Math.max(MARGIN, Math.min(top, vh - bh - MARGIN));
10520
+ setPos({ top, left });
10521
+ };
10522
+ place2();
10523
+ window.addEventListener("scroll", place2, true);
10524
+ window.addEventListener("resize", place2);
10525
+ return () => {
10526
+ window.removeEventListener("scroll", place2, true);
10527
+ window.removeEventListener("resize", place2);
10528
+ };
10529
+ }, [open, placement, title, description]);
10142
10530
  const setRefs = (node) => {
10143
10531
  innerRef.current = node;
10144
10532
  if (typeof ref === "function") ref(node);
@@ -10157,6 +10545,16 @@ var Popconfirm = react.forwardRef(function Popconfirm2({
10157
10545
  onCancel?.();
10158
10546
  setOpen(false);
10159
10547
  };
10548
+ const bubbleStyle = {
10549
+ position: "fixed",
10550
+ top: pos ? pos.top : 0,
10551
+ left: pos ? pos.left : 0,
10552
+ right: "auto",
10553
+ bottom: "auto",
10554
+ transform: "none",
10555
+ visibility: pos ? "visible" : "hidden",
10556
+ ["--klun-popconfirm-tint"]: `var(--klun-state-${status}-base)`
10557
+ };
10160
10558
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { ref: setRefs, className: chunkTPGAXYFU_cjs.cx("klun-popconfirm", className), ...props, children: [
10161
10559
  /* @__PURE__ */ jsxRuntime.jsx(
10162
10560
  "span",
@@ -10169,42 +10567,46 @@ var Popconfirm = react.forwardRef(function Popconfirm2({
10169
10567
  children
10170
10568
  }
10171
10569
  ),
10172
- open ? /* @__PURE__ */ jsxRuntime.jsxs(
10173
- "div",
10174
- {
10175
- role: "dialog",
10176
- className: chunkTPGAXYFU_cjs.cx("klun-popconfirm__bubble", `klun-popconfirm__bubble--${placement}`),
10177
- style: { ["--klun-popconfirm-tint"]: `var(--klun-state-${status}-base)` },
10178
- onClick: (e) => e.stopPropagation(),
10179
- children: [
10180
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-popconfirm__head", children: [
10181
- icon ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: chunkTPGAXYFU_cjs.cx("klun-popconfirm__icon", icon) }) : null,
10182
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-popconfirm__body", children: [
10183
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-popconfirm__title", children: title }),
10184
- description ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-popconfirm__desc", children: description }) : null
10570
+ open && typeof document !== "undefined" ? reactDom.createPortal(
10571
+ /* @__PURE__ */ jsxRuntime.jsxs(
10572
+ "div",
10573
+ {
10574
+ ref: bubbleRef,
10575
+ role: "dialog",
10576
+ className: chunkTPGAXYFU_cjs.cx("klun-popconfirm__bubble", `klun-popconfirm__bubble--${placement}`),
10577
+ style: bubbleStyle,
10578
+ onClick: (e) => e.stopPropagation(),
10579
+ children: [
10580
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-popconfirm__head", children: [
10581
+ icon ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: chunkTPGAXYFU_cjs.cx("klun-popconfirm__icon", icon) }) : null,
10582
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-popconfirm__body", children: [
10583
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-popconfirm__title", children: title }),
10584
+ description ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-popconfirm__desc", children: description }) : null
10585
+ ] })
10586
+ ] }),
10587
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-popconfirm__actions", children: [
10588
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-popconfirm__cancel", onClick: cancel, children: cancelText }),
10589
+ /* @__PURE__ */ jsxRuntime.jsxs(
10590
+ "button",
10591
+ {
10592
+ type: "button",
10593
+ className: chunkTPGAXYFU_cjs.cx(
10594
+ "klun-popconfirm__confirm",
10595
+ okDanger && "klun-popconfirm__confirm--danger"
10596
+ ),
10597
+ onClick: confirm,
10598
+ disabled: busy,
10599
+ children: [
10600
+ busy ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-loader-4-line klun-popconfirm__spinner" }) : null,
10601
+ okText
10602
+ ]
10603
+ }
10604
+ )
10185
10605
  ] })
10186
- ] }),
10187
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-popconfirm__actions", children: [
10188
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "klun-popconfirm__cancel", onClick: cancel, children: cancelText }),
10189
- /* @__PURE__ */ jsxRuntime.jsxs(
10190
- "button",
10191
- {
10192
- type: "button",
10193
- className: chunkTPGAXYFU_cjs.cx(
10194
- "klun-popconfirm__confirm",
10195
- okDanger && "klun-popconfirm__confirm--danger"
10196
- ),
10197
- onClick: confirm,
10198
- disabled: busy,
10199
- children: [
10200
- busy ? /* @__PURE__ */ jsxRuntime.jsx("i", { className: "ri-loader-4-line klun-popconfirm__spinner" }) : null,
10201
- okText
10202
- ]
10203
- }
10204
- )
10205
- ] })
10206
- ]
10207
- }
10606
+ ]
10607
+ }
10608
+ ),
10609
+ document.body
10208
10610
  ) : null
10209
10611
  ] });
10210
10612
  });
@@ -10243,6 +10645,7 @@ exports.CompactSelectForInput = CompactSelectForInput;
10243
10645
  exports.ConfigProvider = ConfigProvider;
10244
10646
  exports.Confirm = Confirm;
10245
10647
  exports.ContentLabel = ContentLabel;
10648
+ exports.ContextMenu = ContextMenu2;
10246
10649
  exports.CopyButton = CopyButton;
10247
10650
  exports.CounterInput = CounterInput;
10248
10651
  exports.DatePicker = DatePicker;
@@ -10265,6 +10668,7 @@ exports.Grid = Grid;
10265
10668
  exports.Hint = Hint;
10266
10669
  exports.HorizontalFilter = HorizontalFilter;
10267
10670
  exports.ImageUpload = ImageUpload;
10671
+ exports.ImageViewer = ImageViewer;
10268
10672
  exports.InlineInput = InlineInput;
10269
10673
  exports.InlineSelect = InlineSelect;
10270
10674
  exports.Input = Input;
@@ -10344,6 +10748,7 @@ exports.toast = toast;
10344
10748
  exports.trTR = trTR;
10345
10749
  exports.useBreakpoint = useBreakpoint;
10346
10750
  exports.useConfig = useConfig;
10751
+ exports.useContextMenu = useContextMenu;
10347
10752
  exports.useForm = useForm;
10348
10753
  exports.useLocale = useLocale;
10349
10754
  exports.useMappedSize = useMappedSize;
@@ -10353,5 +10758,5 @@ exports.useSize = useSize;
10353
10758
  exports.viVN = viVN;
10354
10759
  exports.zhCN = zhCN;
10355
10760
  exports.zhTW = zhTW;
10356
- //# sourceMappingURL=chunk-LDENBMG5.cjs.map
10357
- //# sourceMappingURL=chunk-LDENBMG5.cjs.map
10761
+ //# sourceMappingURL=chunk-VFEV4TOL.cjs.map
10762
+ //# sourceMappingURL=chunk-VFEV4TOL.cjs.map