klun-ui 0.1.9 → 0.1.11

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,20 @@ 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
+
8
22
  ## [0.1.9] — 2026-06-21
9
23
 
10
24
  ### Fixed
package/README.md CHANGED
@@ -75,7 +75,7 @@ You can also override tokens directly:
75
75
  | **Feedback** | Alert, Banner, InlineTip, Toast, NotificationItem, ProgressBar, GaugeBar, CircularProgress, SegmentedProgress, Spinner, Skeleton, EmptyState |
76
76
  | **Navigation** | Tabs, SegmentedControl, Breadcrumb, Pagination, StepIndicator, Divider, ContentDivider, HorizontalFilter, PageHeader |
77
77
  | **Overlays** | Modal, Drawer, Dropdown, Popover, CommandMenu |
78
- | **Disclosure** | Accordion |
78
+ | **Disclosure** | Accordion, Collapse |
79
79
  | **Layout** | Card, ContentCard, FeatureCard |
80
80
 
81
81
  All components and their prop types are named exports; a `cx` classnames helper is exported too.
@@ -86,7 +86,7 @@ Full-screen example compositions are available from the `klun-ui/templates` subp
86
86
 
87
87
  ```tsx
88
88
  import "klun-ui/styles.css";
89
- import { DashboardTemplate, AIAssistantTemplate, CryptoTemplate, MarketingTemplate } from "klun-ui/templates";
89
+ import { DashboardTemplate, AIAssistantTemplate, AgentChatTemplate, CryptoTemplate, MarketingTemplate } from "klun-ui/templates";
90
90
 
91
91
  export default function App() {
92
92
  return <DashboardTemplate />;
@@ -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
  }
@@ -5018,6 +5018,56 @@ var Accordion = react.forwardRef(function Accordion2({ items: items2 = [], defau
5018
5018
  }) });
5019
5019
  });
5020
5020
  Accordion.displayName = "Accordion";
5021
+ var Collapse = react.forwardRef(function Collapse2({
5022
+ title,
5023
+ icon,
5024
+ extra,
5025
+ defaultOpen = false,
5026
+ open,
5027
+ onOpenChange,
5028
+ hideChevron = false,
5029
+ contentStyle,
5030
+ contentClassName,
5031
+ className,
5032
+ children,
5033
+ ...props
5034
+ }, ref) {
5035
+ const isControlled = open !== void 0;
5036
+ const [internal, setInternal] = react.useState(defaultOpen);
5037
+ const isOpen = isControlled ? open : internal;
5038
+ const toggle = () => {
5039
+ const next = !isOpen;
5040
+ if (!isControlled) setInternal(next);
5041
+ onOpenChange?.(next);
5042
+ };
5043
+ const onKeyDown = (e) => {
5044
+ if (e.key === "Enter" || e.key === " ") {
5045
+ e.preventDefault();
5046
+ toggle();
5047
+ }
5048
+ };
5049
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: chunkTPGAXYFU_cjs.cx("klun-collapse", className), "data-state": isOpen ? "open" : "closed", ...props, children: [
5050
+ /* @__PURE__ */ jsxRuntime.jsxs(
5051
+ "div",
5052
+ {
5053
+ className: "klun-collapse__trigger",
5054
+ role: "button",
5055
+ tabIndex: 0,
5056
+ "aria-expanded": isOpen,
5057
+ onClick: toggle,
5058
+ onKeyDown,
5059
+ children: [
5060
+ icon != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-collapse__icon", children: icon }) : null,
5061
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-collapse__title", children: title }),
5062
+ extra != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-collapse__extra", onClick: (e) => e.stopPropagation(), children: extra }) : null,
5063
+ !hideChevron ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-collapse__chevron", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 8l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) : null
5064
+ ]
5065
+ }
5066
+ ),
5067
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-collapse__panel", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkTPGAXYFU_cjs.cx("klun-collapse__content", contentClassName), style: contentStyle, children }) })
5068
+ ] });
5069
+ });
5070
+ Collapse.displayName = "Collapse";
5021
5071
  var Alert = react.forwardRef(function Alert2({ status = "information", variant = "lighter", icon, title, action, onClose, className, children, ...props }, ref) {
5022
5072
  const { alert } = useLocale();
5023
5073
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -5920,7 +5970,7 @@ var Cascader = react.forwardRef(function Cascader2({
5920
5970
  ),
5921
5971
  children: col.map((opt) => {
5922
5972
  const selected = active[ci] === opt.value;
5923
- const hasChildren = !!(opt.children && opt.children.length);
5973
+ const hasChildren2 = !!(opt.children && opt.children.length);
5924
5974
  return /* @__PURE__ */ jsxRuntime.jsxs(
5925
5975
  "button",
5926
5976
  {
@@ -5931,7 +5981,7 @@ var Cascader = react.forwardRef(function Cascader2({
5931
5981
  "data-selected": selected || void 0,
5932
5982
  children: [
5933
5983
  /* @__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
5984
+ 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
5985
  ]
5936
5986
  },
5937
5987
  opt.value
@@ -9838,6 +9888,203 @@ function Confirm(opts = {}) {
9838
9888
  root.render(/* @__PURE__ */ jsxRuntime.jsx(ConfirmDialog, { opts, onClose: close }));
9839
9889
  });
9840
9890
  }
9891
+ var isSelectable = (it) => !it.divider && !it.disabled;
9892
+ var hasChildren = (it) => !!it.children && it.children.length > 0;
9893
+ var GAP2 = 8;
9894
+ function place(w, h, opts) {
9895
+ const vw = window.innerWidth;
9896
+ const vh = window.innerHeight;
9897
+ let left;
9898
+ let top;
9899
+ if (opts.rect) {
9900
+ const r = opts.rect;
9901
+ left = r.right - 4 + w > vw - GAP2 ? r.left - w + 4 : r.right - 4;
9902
+ top = r.top - 4;
9903
+ } else {
9904
+ const p = opts.point ?? { x: 0, y: 0 };
9905
+ left = p.x + w > vw - GAP2 ? p.x - w : p.x;
9906
+ top = p.y + h > vh - GAP2 ? p.y - h : p.y;
9907
+ }
9908
+ left = Math.min(Math.max(GAP2, left), vw - w - GAP2);
9909
+ top = Math.min(Math.max(GAP2, top), vh - h - GAP2);
9910
+ return { left, top };
9911
+ }
9912
+ function MenuPanel({ items: items2, width, className, point, anchorEl, autoActive, depth, onCloseAll, onCloseSelf }) {
9913
+ const ref = react.useRef(null);
9914
+ const itemEls = react.useRef([]);
9915
+ const [pos, setPos] = react.useState(null);
9916
+ const [active, setActive] = react.useState(autoActive ? items2.findIndex(isSelectable) : -1);
9917
+ const [sub, setSub] = react.useState(null);
9918
+ const isLeaf = sub === null;
9919
+ react.useLayoutEffect(() => {
9920
+ const el = ref.current;
9921
+ const w = el?.offsetWidth ?? width;
9922
+ const h = el?.offsetHeight ?? 0;
9923
+ setPos(place(w, h, anchorEl ? { rect: anchorEl.getBoundingClientRect() } : { point }));
9924
+ }, [width, point?.x, point?.y, anchorEl, items2]);
9925
+ const move = react.useCallback(
9926
+ (dir) => {
9927
+ setActive((cur) => {
9928
+ const n = items2.length;
9929
+ for (let s = 1; s <= n; s++) {
9930
+ const i = (cur + dir * s + n * s) % n;
9931
+ if (isSelectable(items2[i])) return i;
9932
+ }
9933
+ return cur;
9934
+ });
9935
+ },
9936
+ [items2]
9937
+ );
9938
+ const activate = react.useCallback(
9939
+ (i, viaKeyboard) => {
9940
+ const it = items2[i];
9941
+ if (!it || !isSelectable(it)) return;
9942
+ if (hasChildren(it)) setSub({ i, kbd: viaKeyboard });
9943
+ else {
9944
+ onCloseAll();
9945
+ it.onClick?.();
9946
+ }
9947
+ },
9948
+ [items2, onCloseAll]
9949
+ );
9950
+ react.useEffect(() => {
9951
+ if (!isLeaf) return;
9952
+ const onKey = (e) => {
9953
+ if (e.key === "ArrowDown") {
9954
+ e.preventDefault();
9955
+ move(1);
9956
+ } else if (e.key === "ArrowUp") {
9957
+ e.preventDefault();
9958
+ move(-1);
9959
+ } else if (e.key === "Home") {
9960
+ e.preventDefault();
9961
+ setActive(items2.findIndex(isSelectable));
9962
+ } else if (e.key === "End") {
9963
+ e.preventDefault();
9964
+ for (let i = items2.length - 1; i >= 0; i--)
9965
+ if (isSelectable(items2[i])) {
9966
+ setActive(i);
9967
+ break;
9968
+ }
9969
+ } else if (e.key === "ArrowRight") {
9970
+ if (active >= 0 && hasChildren(items2[active])) {
9971
+ e.preventDefault();
9972
+ setSub({ i: active, kbd: true });
9973
+ }
9974
+ } else if (e.key === "ArrowLeft") {
9975
+ if (depth > 0) {
9976
+ e.preventDefault();
9977
+ onCloseSelf?.();
9978
+ }
9979
+ } else if (e.key === "Enter") {
9980
+ if (active >= 0) {
9981
+ e.preventDefault();
9982
+ activate(active, true);
9983
+ }
9984
+ } else if (e.key === "Escape") {
9985
+ e.preventDefault();
9986
+ onCloseAll();
9987
+ }
9988
+ };
9989
+ window.addEventListener("keydown", onKey, true);
9990
+ return () => window.removeEventListener("keydown", onKey, true);
9991
+ }, [isLeaf, active, items2, depth, move, activate, onCloseAll, onCloseSelf]);
9992
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9993
+ "div",
9994
+ {
9995
+ ref,
9996
+ role: "menu",
9997
+ className: chunkTPGAXYFU_cjs.cx("klun-context-menu", className),
9998
+ style: pos ? { left: pos.left, top: pos.top, width } : { left: -9999, top: -9999, width },
9999
+ onMouseDown: (e) => e.stopPropagation(),
10000
+ children: [
10001
+ items2.map(
10002
+ (it, i) => it.divider ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-context-menu__divider", role: "separator" }, i) : /* @__PURE__ */ jsxRuntime.jsxs(
10003
+ "button",
10004
+ {
10005
+ ref: (el) => {
10006
+ itemEls.current[i] = el;
10007
+ },
10008
+ type: "button",
10009
+ role: "menuitem",
10010
+ "aria-haspopup": hasChildren(it) || void 0,
10011
+ "aria-expanded": hasChildren(it) ? sub?.i === i : void 0,
10012
+ disabled: it.disabled,
10013
+ className: chunkTPGAXYFU_cjs.cx(
10014
+ "klun-context-menu__item",
10015
+ it.danger && "klun-context-menu__item--danger",
10016
+ i === active && "is-active"
10017
+ ),
10018
+ onMouseEnter: () => {
10019
+ setActive(i);
10020
+ if (isSelectable(it)) setSub(hasChildren(it) ? { i, kbd: false } : null);
10021
+ },
10022
+ onClick: () => activate(i, false),
10023
+ children: [
10024
+ it.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-context-menu__icon", children: it.icon }) : null,
10025
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "klun-context-menu__label", children: it.label }),
10026
+ 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
10027
+ ]
10028
+ },
10029
+ i
10030
+ )
10031
+ ),
10032
+ sub && items2[sub.i]?.children ? /* @__PURE__ */ jsxRuntime.jsx(
10033
+ MenuPanel,
10034
+ {
10035
+ items: items2[sub.i].children,
10036
+ width,
10037
+ anchorEl: itemEls.current[sub.i],
10038
+ autoActive: sub.kbd,
10039
+ depth: depth + 1,
10040
+ onCloseAll,
10041
+ onCloseSelf: () => setSub(null)
10042
+ },
10043
+ `sub-${sub.i}`
10044
+ ) : null
10045
+ ]
10046
+ }
10047
+ );
10048
+ }
10049
+ function ContextMenu2({ open, x, y, items: items2, onClose, width = 208, className }) {
10050
+ if (!open || typeof document === "undefined") return null;
10051
+ return reactDom.createPortal(
10052
+ /* @__PURE__ */ jsxRuntime.jsx(
10053
+ "div",
10054
+ {
10055
+ className: "klun-context-menu__layer",
10056
+ onMouseDown: onClose,
10057
+ onContextMenu: (e) => {
10058
+ e.preventDefault();
10059
+ onClose();
10060
+ },
10061
+ onWheel: onClose,
10062
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10063
+ MenuPanel,
10064
+ {
10065
+ items: items2,
10066
+ width,
10067
+ className,
10068
+ point: { x, y },
10069
+ depth: 0,
10070
+ onCloseAll: onClose
10071
+ }
10072
+ )
10073
+ }
10074
+ ),
10075
+ document.body
10076
+ );
10077
+ }
10078
+ ContextMenu2.displayName = "ContextMenu";
10079
+ function useContextMenu() {
10080
+ const [state, setState] = react.useState(null);
10081
+ const open = react.useCallback((e, items2) => {
10082
+ e.preventDefault();
10083
+ setState({ x: e.clientX, y: e.clientY, items: items2 });
10084
+ }, []);
10085
+ const close = react.useCallback(() => setState(null), []);
10086
+ return { state, open, close };
10087
+ }
9841
10088
  var Drawer = react.forwardRef(function Drawer2({
9842
10089
  open,
9843
10090
  onClose,
@@ -9861,8 +10108,8 @@ var Drawer = react.forwardRef(function Drawer2({
9861
10108
  ...props
9862
10109
  }, ref) {
9863
10110
  const { drawer } = useLocale();
9864
- const place = placement ?? side ?? "right";
9865
- const horizontal = place === "left" || place === "right";
10111
+ const place2 = placement ?? side ?? "right";
10112
+ const horizontal = place2 === "left" || place2 === "right";
9866
10113
  const prevOpen = react.useRef(open);
9867
10114
  react.useEffect(() => {
9868
10115
  if (!open) return;
@@ -9889,7 +10136,7 @@ var Drawer = react.forwardRef(function Drawer2({
9889
10136
  return /* @__PURE__ */ jsxRuntime.jsx(
9890
10137
  "div",
9891
10138
  {
9892
- className: chunkTPGAXYFU_cjs.cx("klun-drawer", `klun-drawer--${place}`, !mask && "klun-drawer--no-mask"),
10139
+ className: chunkTPGAXYFU_cjs.cx("klun-drawer", `klun-drawer--${place2}`, !mask && "klun-drawer--no-mask"),
9893
10140
  style: zIndex != null ? { zIndex } : void 0,
9894
10141
  onClick: maskClosable ? onClose : void 0,
9895
10142
  children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9972,6 +10219,146 @@ var Dropdown = react.forwardRef(function Dropdown2({ trigger, items: items2 = []
9972
10219
  ] });
9973
10220
  });
9974
10221
  Dropdown.displayName = "Dropdown";
10222
+ var MIN = 0.25;
10223
+ var MAX = 6;
10224
+ var clampScale = (s) => Math.min(MAX, Math.max(MIN, +s.toFixed(2)));
10225
+ function ImageViewer({
10226
+ open,
10227
+ src,
10228
+ alt,
10229
+ items: items2,
10230
+ index,
10231
+ onIndexChange,
10232
+ onClose,
10233
+ onDownload,
10234
+ title,
10235
+ zoomable = true,
10236
+ rotatable = true,
10237
+ closeOnBackdrop = true,
10238
+ className
10239
+ }) {
10240
+ const gallery = items2 && items2.length > 0 ? items2 : src != null ? [{ src, alt }] : [];
10241
+ const [innerIndex, setInnerIndex] = react.useState(0);
10242
+ const idx = index ?? innerIndex;
10243
+ const safeIdx = Math.min(Math.max(idx, 0), Math.max(0, gallery.length - 1));
10244
+ const current = gallery[safeIdx];
10245
+ const [scale, setScale] = react.useState(1);
10246
+ const [rot, setRot] = react.useState(0);
10247
+ const [pan, setPan] = react.useState({ x: 0, y: 0 });
10248
+ const drag = react.useRef(null);
10249
+ const reset = react.useCallback(() => {
10250
+ setScale(1);
10251
+ setRot(0);
10252
+ setPan({ x: 0, y: 0 });
10253
+ }, []);
10254
+ const goTo = react.useCallback(
10255
+ (next) => {
10256
+ if (gallery.length < 2) return;
10257
+ const n = (next + gallery.length) % gallery.length;
10258
+ if (index === void 0) setInnerIndex(n);
10259
+ onIndexChange?.(n);
10260
+ reset();
10261
+ },
10262
+ [gallery.length, index, onIndexChange, reset]
10263
+ );
10264
+ react.useEffect(() => {
10265
+ reset();
10266
+ }, [safeIdx, open, reset]);
10267
+ react.useEffect(() => {
10268
+ if (!open) return;
10269
+ const prev = document.body.style.overflow;
10270
+ document.body.style.overflow = "hidden";
10271
+ const onKey = (e) => {
10272
+ if (e.key === "Escape") onClose();
10273
+ else if (e.key === "ArrowLeft") goTo(safeIdx - 1);
10274
+ else if (e.key === "ArrowRight") goTo(safeIdx + 1);
10275
+ else if (zoomable && (e.key === "+" || e.key === "=")) setScale((s) => clampScale(s + 0.25));
10276
+ else if (zoomable && (e.key === "-" || e.key === "_")) setScale((s) => clampScale(s - 0.25));
10277
+ else if (e.key === "0") reset();
10278
+ };
10279
+ window.addEventListener("keydown", onKey);
10280
+ return () => {
10281
+ document.body.style.overflow = prev;
10282
+ window.removeEventListener("keydown", onKey);
10283
+ };
10284
+ }, [open, safeIdx, zoomable, onClose, goTo, reset]);
10285
+ if (!open || !current || typeof document === "undefined") return null;
10286
+ const onWheel = (e) => {
10287
+ if (!zoomable) return;
10288
+ e.preventDefault();
10289
+ setScale((s) => clampScale(s + (e.deltaY < 0 ? 0.2 : -0.2)));
10290
+ };
10291
+ const onImgDown = (e) => {
10292
+ if (scale <= 1) return;
10293
+ e.preventDefault();
10294
+ drag.current = { x: e.clientX, y: e.clientY, px: pan.x, py: pan.y };
10295
+ };
10296
+ const onMove = (e) => {
10297
+ if (!drag.current) return;
10298
+ setPan({ x: drag.current.px + (e.clientX - drag.current.x), y: drag.current.py + (e.clientY - drag.current.y) });
10299
+ };
10300
+ const endDrag = () => {
10301
+ drag.current = null;
10302
+ };
10303
+ const toggleZoom = () => scale > 1 ? reset() : setScale(2);
10304
+ const heading = title ?? current.title ?? current.alt;
10305
+ return reactDom.createPortal(
10306
+ /* @__PURE__ */ jsxRuntime.jsxs(
10307
+ "div",
10308
+ {
10309
+ className: chunkTPGAXYFU_cjs.cx("klun-image-viewer", className),
10310
+ onMouseDown: (e) => {
10311
+ if (closeOnBackdrop && e.target === e.currentTarget) onClose();
10312
+ },
10313
+ onMouseMove: onMove,
10314
+ onMouseUp: endDrag,
10315
+ onMouseLeave: endDrag,
10316
+ children: [
10317
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-image-viewer__bar", onMouseDown: (e) => e.stopPropagation(), children: [
10318
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-image-viewer__title", children: [
10319
+ heading,
10320
+ gallery.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "klun-image-viewer__counter", children: [
10321
+ safeIdx + 1,
10322
+ " / ",
10323
+ gallery.length
10324
+ ] }) : null
10325
+ ] }),
10326
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "klun-image-viewer__tools", children: [
10327
+ zoomable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10328
+ /* @__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" }) }),
10329
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "klun-image-viewer__zoom", children: [
10330
+ Math.round(scale * 100),
10331
+ "%"
10332
+ ] }),
10333
+ /* @__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" }) })
10334
+ ] }) : null,
10335
+ 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,
10336
+ /* @__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" }) }),
10337
+ 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,
10338
+ /* @__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" }) })
10339
+ ] })
10340
+ ] }),
10341
+ 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,
10342
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "klun-image-viewer__stage", onMouseDown: (e) => e.stopPropagation(), onWheel, children: /* @__PURE__ */ jsxRuntime.jsx(
10343
+ "img",
10344
+ {
10345
+ className: chunkTPGAXYFU_cjs.cx("klun-image-viewer__img", scale > 1 && "is-zoomed"),
10346
+ src: current.src,
10347
+ alt: current.alt ?? "",
10348
+ draggable: false,
10349
+ onDoubleClick: toggleZoom,
10350
+ onMouseDown: onImgDown,
10351
+ style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale}) rotate(${rot}deg)` }
10352
+ }
10353
+ ) }),
10354
+ 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
10355
+ ]
10356
+ }
10357
+ ),
10358
+ document.body
10359
+ );
10360
+ }
10361
+ ImageViewer.displayName = "ImageViewer";
9975
10362
  var Modal = react.forwardRef(function Modal2({
9976
10363
  open,
9977
10364
  onClose,
@@ -10101,7 +10488,7 @@ var Modal = react.forwardRef(function Modal2({
10101
10488
  );
10102
10489
  });
10103
10490
  Modal.displayName = "Modal";
10104
- var GAP2 = 8;
10491
+ var GAP3 = 8;
10105
10492
  var MARGIN = 8;
10106
10493
  var Popconfirm = react.forwardRef(function Popconfirm2({
10107
10494
  title: titleProp,
@@ -10150,7 +10537,7 @@ var Popconfirm = react.forwardRef(function Popconfirm2({
10150
10537
  setPos(null);
10151
10538
  return;
10152
10539
  }
10153
- const place = () => {
10540
+ const place2 = () => {
10154
10541
  const trigger = innerRef.current;
10155
10542
  const bubble = bubbleRef.current;
10156
10543
  if (!trigger || !bubble) return;
@@ -10163,31 +10550,31 @@ var Popconfirm = react.forwardRef(function Popconfirm2({
10163
10550
  let left;
10164
10551
  switch (placement) {
10165
10552
  case "bottom":
10166
- top = tr.bottom + GAP2;
10553
+ top = tr.bottom + GAP3;
10167
10554
  left = tr.left + tr.width / 2 - bw / 2;
10168
10555
  break;
10169
10556
  case "left":
10170
- left = tr.left - bw - GAP2;
10557
+ left = tr.left - bw - GAP3;
10171
10558
  top = tr.top + tr.height / 2 - bh / 2;
10172
10559
  break;
10173
10560
  case "right":
10174
- left = tr.right + GAP2;
10561
+ left = tr.right + GAP3;
10175
10562
  top = tr.top + tr.height / 2 - bh / 2;
10176
10563
  break;
10177
10564
  default:
10178
- top = tr.top - bh - GAP2;
10565
+ top = tr.top - bh - GAP3;
10179
10566
  left = tr.left + tr.width / 2 - bw / 2;
10180
10567
  }
10181
10568
  left = Math.max(MARGIN, Math.min(left, vw - bw - MARGIN));
10182
10569
  top = Math.max(MARGIN, Math.min(top, vh - bh - MARGIN));
10183
10570
  setPos({ top, left });
10184
10571
  };
10185
- place();
10186
- window.addEventListener("scroll", place, true);
10187
- window.addEventListener("resize", place);
10572
+ place2();
10573
+ window.addEventListener("scroll", place2, true);
10574
+ window.addEventListener("resize", place2);
10188
10575
  return () => {
10189
- window.removeEventListener("scroll", place, true);
10190
- window.removeEventListener("resize", place);
10576
+ window.removeEventListener("scroll", place2, true);
10577
+ window.removeEventListener("resize", place2);
10191
10578
  };
10192
10579
  }, [open, placement, title, description]);
10193
10580
  const setRefs = (node) => {
@@ -10299,6 +10686,7 @@ exports.Chip = Chip;
10299
10686
  exports.CircularProgress = CircularProgress;
10300
10687
  exports.CodeViewer = CodeViewer;
10301
10688
  exports.Col = Col;
10689
+ exports.Collapse = Collapse;
10302
10690
  exports.ColorDot = ColorDot;
10303
10691
  exports.ColorPicker = ColorPicker;
10304
10692
  exports.ColorSlider = ColorSlider;
@@ -10308,6 +10696,7 @@ exports.CompactSelectForInput = CompactSelectForInput;
10308
10696
  exports.ConfigProvider = ConfigProvider;
10309
10697
  exports.Confirm = Confirm;
10310
10698
  exports.ContentLabel = ContentLabel;
10699
+ exports.ContextMenu = ContextMenu2;
10311
10700
  exports.CopyButton = CopyButton;
10312
10701
  exports.CounterInput = CounterInput;
10313
10702
  exports.DatePicker = DatePicker;
@@ -10330,6 +10719,7 @@ exports.Grid = Grid;
10330
10719
  exports.Hint = Hint;
10331
10720
  exports.HorizontalFilter = HorizontalFilter;
10332
10721
  exports.ImageUpload = ImageUpload;
10722
+ exports.ImageViewer = ImageViewer;
10333
10723
  exports.InlineInput = InlineInput;
10334
10724
  exports.InlineSelect = InlineSelect;
10335
10725
  exports.Input = Input;
@@ -10409,6 +10799,7 @@ exports.toast = toast;
10409
10799
  exports.trTR = trTR;
10410
10800
  exports.useBreakpoint = useBreakpoint;
10411
10801
  exports.useConfig = useConfig;
10802
+ exports.useContextMenu = useContextMenu;
10412
10803
  exports.useForm = useForm;
10413
10804
  exports.useLocale = useLocale;
10414
10805
  exports.useMappedSize = useMappedSize;
@@ -10418,5 +10809,5 @@ exports.useSize = useSize;
10418
10809
  exports.viVN = viVN;
10419
10810
  exports.zhCN = zhCN;
10420
10811
  exports.zhTW = zhTW;
10421
- //# sourceMappingURL=chunk-PRDLMM4A.cjs.map
10422
- //# sourceMappingURL=chunk-PRDLMM4A.cjs.map
10812
+ //# sourceMappingURL=chunk-F7D5VPDL.cjs.map
10813
+ //# sourceMappingURL=chunk-F7D5VPDL.cjs.map