@sia.soul/sia-react-ui 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.cjs CHANGED
@@ -983,6 +983,17 @@ var coreIconPaths = {
983
983
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "10.8", cy: "10.8", r: "6.8" }),
984
984
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m15.8 15.8 5.2 5.2M7.8 10.8h6" })
985
985
  ] }),
986
+ // 图片变换工具使用独立图标,便于在预览及其他编辑界面复用。
987
+ "flip-horizontal": /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 3v18M3 6h5v12H3zM16 6h5v12h-5z" }) }),
988
+ "flip-vertical": /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 12h18M6 3h12v5H6zM6 16h12v5H6z" }) }),
989
+ "rotate-left": /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
990
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M4 9a8 8 0 1 1 0 6M4 3v6h6" }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "9", width: "6", height: "6", rx: "1" })
992
+ ] }),
993
+ "rotate-right": /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
994
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M20 9a8 8 0 1 0 0 6M20 3v6h-6" }),
995
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "9", width: "6", height: "6", rx: "1" })
996
+ ] }),
986
997
  download: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 3v12M7 10l5 5 5-5M4 20h16" }) }),
987
998
  upload: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 16V4M7 9l5-5 5 5M4 20h16" }) }),
988
999
  refresh: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
@@ -2523,11 +2534,98 @@ function Collapse({ items, activeKey, defaultActiveKey = [], accordion = false,
2523
2534
  }
2524
2535
  function ImagePreview({ open, src, alt = "", onOpenChange }) {
2525
2536
  const previewRef = (0, import_react15.useRef)(null);
2537
+ const stageRef = (0, import_react15.useRef)(null);
2538
+ const [natural, setNatural] = (0, import_react15.useState)({ src: "", width: 0, height: 0 });
2539
+ const [area, setArea] = (0, import_react15.useState)({ width: 0, height: 0 });
2540
+ const [zoom, setZoom] = (0, import_react15.useState)(1);
2541
+ const [rotation, setRotation] = (0, import_react15.useState)(0);
2542
+ const [flip, setFlip] = (0, import_react15.useState)({ x: 1, y: 1 });
2543
+ const [pan, setPan] = (0, import_react15.useState)({ x: 0, y: 0 });
2544
+ const [failed, setFailed] = (0, import_react15.useState)(false);
2545
+ const drag = (0, import_react15.useRef)();
2526
2546
  useOverlayLifecycle(open, previewRef, () => onOpenChange(false));
2547
+ const reset = () => {
2548
+ setZoom(1);
2549
+ setRotation(0);
2550
+ setFlip({ x: 1, y: 1 });
2551
+ setPan({ x: 0, y: 0 });
2552
+ drag.current = void 0;
2553
+ };
2554
+ (0, import_react15.useEffect)(() => {
2555
+ reset();
2556
+ setFailed(false);
2557
+ }, [open, src]);
2558
+ (0, import_react15.useEffect)(() => {
2559
+ const stage = stageRef.current;
2560
+ if (!open || !stage) return;
2561
+ const measure = () => {
2562
+ setArea({ width: stage.clientWidth, height: stage.clientHeight });
2563
+ setPan({ x: 0, y: 0 });
2564
+ };
2565
+ measure();
2566
+ const observer = new ResizeObserver(measure);
2567
+ observer.observe(stage);
2568
+ return () => observer.disconnect();
2569
+ }, [open, src]);
2570
+ const ready = natural.src === src && natural.width > 0 && area.width > 0 && area.height > 0 && !failed;
2571
+ const sideways = Math.abs(rotation % 180) === 90;
2572
+ const fit = ready ? Math.min(1, area.width / (sideways ? natural.height : natural.width), area.height / (sideways ? natural.width : natural.height)) : 0;
2573
+ const changeZoom = (factor) => setZoom((value) => Math.max(0.25, Math.min(8, value * factor)));
2527
2574
  if (!open || !src || typeof document === "undefined") return null;
2528
2575
  return (0, import_react_dom5.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { ref: previewRef, className: "sia-image-preview", role: "dialog", "aria-modal": "true", "aria-label": "\u56FE\u7247\u9884\u89C8", tabIndex: -1, onClick: () => onOpenChange(false), children: [
2529
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", "aria-label": "\u5173\u95ED\u9884\u89C8", onClick: () => onOpenChange(false), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "close" }) }),
2530
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("img", { src, alt, onClick: (event) => event.stopPropagation() })
2576
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", className: "sia-image-preview__close", "aria-label": "\u5173\u95ED\u9884\u89C8", onClick: (event) => {
2577
+ event.stopPropagation();
2578
+ onOpenChange(false);
2579
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "close" }) }),
2580
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { ref: stageRef, className: "sia-image-preview__stage", children: failed ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "sia-image-preview__error", children: "\u56FE\u7247\u52A0\u8F7D\u5931\u8D25" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2581
+ "img",
2582
+ {
2583
+ src,
2584
+ alt,
2585
+ draggable: false,
2586
+ style: { width: natural.width * fit, height: natural.height * fit, visibility: ready ? "visible" : "hidden", transform: `translate(${pan.x}px, ${pan.y}px) rotate(${rotation}deg) scale(${zoom * flip.x}, ${zoom * flip.y})` },
2587
+ onLoad: (event) => setNatural({ src, width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight }),
2588
+ onError: () => setFailed(true),
2589
+ onClick: (event) => event.stopPropagation(),
2590
+ onPointerDown: (event) => {
2591
+ if (event.button !== 0) return;
2592
+ event.preventDefault();
2593
+ event.stopPropagation();
2594
+ event.currentTarget.setPointerCapture(event.pointerId);
2595
+ drag.current = { id: event.pointerId, x: event.clientX, y: event.clientY, left: pan.x, top: pan.y };
2596
+ },
2597
+ onPointerMove: (event) => {
2598
+ const start = drag.current;
2599
+ if (start?.id === event.pointerId) setPan({ x: start.left + event.clientX - start.x, y: start.top + event.clientY - start.y });
2600
+ },
2601
+ onPointerUp: () => {
2602
+ drag.current = void 0;
2603
+ },
2604
+ onPointerCancel: () => {
2605
+ drag.current = void 0;
2606
+ },
2607
+ onLostPointerCapture: () => {
2608
+ drag.current = void 0;
2609
+ }
2610
+ },
2611
+ src
2612
+ ) }),
2613
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "sia-image-preview__toolbar", role: "toolbar", "aria-label": "\u56FE\u7247\u64CD\u4F5C", onClick: (event) => event.stopPropagation(), children: [
2614
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u6C34\u5E73\u7FFB\u8F6C", "aria-label": "\u6C34\u5E73\u7FFB\u8F6C", disabled: !ready, onClick: () => setFlip((value) => ({ ...value, x: -value.x })), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "flip-horizontal", size: 20 }) }),
2615
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u5782\u76F4\u7FFB\u8F6C", "aria-label": "\u5782\u76F4\u7FFB\u8F6C", disabled: !ready, onClick: () => setFlip((value) => ({ ...value, y: -value.y })), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "flip-vertical", size: 20 }) }),
2616
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u5411\u5DE6\u65CB\u8F6C", "aria-label": "\u5411\u5DE6\u65CB\u8F6C", disabled: !ready, onClick: () => {
2617
+ setRotation((value) => value - 90);
2618
+ setPan({ x: 0, y: 0 });
2619
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "rotate-left", size: 20 }) }),
2620
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u5411\u53F3\u65CB\u8F6C", "aria-label": "\u5411\u53F3\u65CB\u8F6C", disabled: !ready, onClick: () => {
2621
+ setRotation((value) => value + 90);
2622
+ setPan({ x: 0, y: 0 });
2623
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "rotate-right", size: 20 }) }),
2624
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u7F29\u5C0F", "aria-label": "\u7F29\u5C0F", disabled: !ready || zoom <= 0.25, onClick: () => changeZoom(1 / 1.25), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "zoom-out", size: 20 }) }),
2625
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("output", { className: "sia-image-preview__scale", "aria-label": "\u5F53\u524D\u7F29\u653E\u6BD4\u4F8B", "aria-live": "polite", children: ready ? `${Math.round(fit * zoom * 100)}%` : "\u2014" }),
2626
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u653E\u5927", "aria-label": "\u653E\u5927", disabled: !ready || zoom >= 8, onClick: () => changeZoom(1.25), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "zoom-in", size: 20 }) }),
2627
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "text", title: "\u9002\u5E94\u7A97\u53E3", "aria-label": "\u9002\u5E94\u7A97\u53E3", disabled: !ready, onClick: reset, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "refresh", size: 20 }) })
2628
+ ] })
2531
2629
  ] }), document.body);
2532
2630
  }
2533
2631
 
@@ -4058,10 +4156,158 @@ function sendUploadRequest(config, options) {
4058
4156
  return { abort };
4059
4157
  }
4060
4158
 
4061
- // src/components/Upload.tsx
4159
+ // src/components/uploadDownload.ts
4160
+ async function downloadUploadFile(file, request) {
4161
+ let blob;
4162
+ if (request) blob = await request(file);
4163
+ else if (file.url) {
4164
+ const response = await fetch(file.url);
4165
+ if (!response.ok) throw new Error(`\u4E0B\u8F7D\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
4166
+ blob = await response.blob();
4167
+ } else if (file.originFileObj) blob = file.originFileObj;
4168
+ else throw new Error("\u6587\u4EF6\u7F3A\u5C11\u4E0B\u8F7D\u5730\u5740\u6216\u539F\u59CB\u5185\u5BB9");
4169
+ if (!(blob instanceof Blob)) throw new Error("\u4E0B\u8F7D\u8BF7\u6C42\u5FC5\u987B\u8FD4\u56DE Blob \u6587\u4EF6\u5185\u5BB9");
4170
+ const url = URL.createObjectURL(blob);
4171
+ const anchor = document.createElement("a");
4172
+ anchor.href = url;
4173
+ anchor.download = file.name;
4174
+ anchor.hidden = true;
4175
+ document.body.append(anchor);
4176
+ try {
4177
+ anchor.click();
4178
+ } finally {
4179
+ anchor.remove();
4180
+ window.setTimeout(() => URL.revokeObjectURL(url), 6e4);
4181
+ }
4182
+ }
4183
+
4184
+ // src/components/uploadImage.ts
4185
+ var imageExtension = /\.(png|jpe?g|gif|webp|avif|svg|bmp|ico|apng)$/i;
4186
+ function isUploadImage(file, accept) {
4187
+ if (!file) return false;
4188
+ if (file.thumbUrl) return true;
4189
+ const type = (file.type || file.originFileObj?.type || "").toLowerCase();
4190
+ if (type && type !== "application/octet-stream") return type.startsWith("image/");
4191
+ if (/^data:image\//i.test(file.url || "")) return true;
4192
+ let pathname = "";
4193
+ try {
4194
+ pathname = new URL(file.url || "", "https://upload.invalid").pathname;
4195
+ } catch {
4196
+ }
4197
+ if (imageExtension.test(file.name) || imageExtension.test(pathname)) return true;
4198
+ const rules = (accept || "").split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
4199
+ return Boolean(file.url) && rules.length > 0 && rules.every((rule) => rule.startsWith("image/") || imageExtension.test(rule));
4200
+ }
4201
+
4202
+ // src/components/Message.tsx
4203
+ var import_client4 = require("react-dom/client");
4062
4204
  var import_jsx_runtime22 = require("react/jsx-runtime");
4205
+ var messageSeed = 0;
4206
+ var messageRoot = null;
4207
+ var messageContainer = null;
4208
+ var messageItems = [];
4209
+ var defaultDuration = 3;
4210
+ var maxCount = 5;
4211
+ var messageTimers = /* @__PURE__ */ new Map();
4212
+ var messageIcons = {
4213
+ info: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "info", size: 18 }),
4214
+ success: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "circle-check", size: 18 }),
4215
+ warning: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "warning", size: 18 }),
4216
+ error: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "circle-close", size: 18 }),
4217
+ loading: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "loader", size: 18, spin: true })
4218
+ };
4219
+ function ensureMessageRoot() {
4220
+ if (typeof document === "undefined") return false;
4221
+ if (!messageContainer) {
4222
+ messageContainer = document.createElement("div");
4223
+ messageContainer.className = "sia-message-root";
4224
+ document.body.appendChild(messageContainer);
4225
+ messageRoot = (0, import_client4.createRoot)(messageContainer);
4226
+ }
4227
+ return true;
4228
+ }
4229
+ function renderMessages() {
4230
+ if (!ensureMessageRoot()) return;
4231
+ messageRoot.render(/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "sia-message-list", "aria-live": "polite", children: messageItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: `sia-message sia-message--${item.type}`, role: item.type === "loading" ? "status" : "alert", children: [
4232
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sia-message__icon", "aria-hidden": "true", children: messageIcons[item.type] }),
4233
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sia-message__content", children: item.content }),
4234
+ item.closable ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", className: "sia-message__close", "aria-label": "\u5173\u95ED\u63D0\u793A", onClick: () => closeMessage(item.key), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "close", size: 14 }) }) : null
4235
+ ] }, item.key)) }));
4236
+ }
4237
+ function closeMessage(key) {
4238
+ const item = messageItems.find((candidate) => candidate.key === key);
4239
+ const timer = messageTimers.get(key);
4240
+ if (timer !== void 0) window.clearTimeout(timer);
4241
+ messageTimers.delete(key);
4242
+ messageItems = messageItems.filter((candidate) => candidate.key !== key);
4243
+ item?.onClose?.();
4244
+ renderMessages();
4245
+ }
4246
+ function scheduleMessage(item) {
4247
+ const existingTimer = messageTimers.get(item.key);
4248
+ if (existingTimer !== void 0) window.clearTimeout(existingTimer);
4249
+ const duration = item.duration ?? (item.type === "loading" ? 0 : defaultDuration);
4250
+ if (duration > 0) {
4251
+ messageTimers.set(item.key, window.setTimeout(() => closeMessage(item.key), duration * 1e3));
4252
+ }
4253
+ }
4254
+ function openMessage(input) {
4255
+ const config = typeof input === "object" && input !== null && "content" in input ? input : { content: input };
4256
+ const key = config.key ?? `sia-message-${++messageSeed}`;
4257
+ const item = { ...config, key, type: config.type ?? "info" };
4258
+ const existingIndex = messageItems.findIndex((candidate) => candidate.key === key);
4259
+ if (existingIndex >= 0) messageItems = messageItems.map((candidate, index) => index === existingIndex ? item : candidate);
4260
+ else messageItems = [...messageItems, item];
4261
+ while (messageItems.length > maxCount) closeMessage(messageItems[0].key);
4262
+ renderMessages();
4263
+ scheduleMessage(item);
4264
+ return {
4265
+ close: () => closeMessage(key),
4266
+ update(next) {
4267
+ const current = messageItems.find((candidate) => candidate.key === key);
4268
+ if (!current) return;
4269
+ const updated = { ...current, ...next, key, type: next.type ?? current.type };
4270
+ messageItems = messageItems.map((candidate) => candidate.key === key ? updated : candidate);
4271
+ renderMessages();
4272
+ scheduleMessage(updated);
4273
+ }
4274
+ };
4275
+ }
4276
+ function shortcut(type) {
4277
+ return (content, duration) => openMessage({ content, duration, type });
4278
+ }
4279
+ var message = {
4280
+ /** 直接打开实例,返回可用于更新或关闭的操作句柄。 */
4281
+ open: openMessage,
4282
+ /** 显示普通信息提示。 */
4283
+ info: shortcut("info"),
4284
+ /** 显示成功提示。 */
4285
+ success: shortcut("success"),
4286
+ /** 显示警告提示。 */
4287
+ warning: shortcut("warning"),
4288
+ /** 显示错误提示。 */
4289
+ error: shortcut("error"),
4290
+ /** 显示持续加载提示。 */
4291
+ loading: shortcut("loading"),
4292
+ /** 关闭指定实例;未指定标识时关闭所有实例。 */
4293
+ destroy(key) {
4294
+ if (key !== void 0) {
4295
+ closeMessage(key);
4296
+ return;
4297
+ }
4298
+ [...messageItems].forEach((item) => closeMessage(item.key));
4299
+ },
4300
+ /** 配置消息默认持续时间和最大同时显示数量。 */
4301
+ config(config) {
4302
+ if (config.duration !== void 0) defaultDuration = config.duration;
4303
+ if (config.maxCount !== void 0) maxCount = Math.max(1, config.maxCount);
4304
+ }
4305
+ };
4306
+
4307
+ // src/components/Upload.tsx
4308
+ var import_jsx_runtime23 = require("react/jsx-runtime");
4063
4309
  var LIST_IGNORE = /* @__PURE__ */ Symbol("SIA_UPLOAD_LIST_IGNORE");
4064
- function useThumbnail(file) {
4310
+ function useThumbnail(file, accept) {
4065
4311
  const [local, setLocal] = (0, import_react23.useState)();
4066
4312
  (0, import_react23.useEffect)(() => {
4067
4313
  if (!file?.thumbUrl && file?.originFileObj?.type.startsWith("image/")) {
@@ -4071,27 +4317,48 @@ function useThumbnail(file) {
4071
4317
  }
4072
4318
  setLocal(void 0);
4073
4319
  }, [file?.originFileObj, file?.thumbUrl]);
4074
- return file?.thumbUrl || local || (file?.type?.startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg)(\?|$)/i.test(file?.url ?? "") ? file?.url : void 0);
4320
+ return file?.thumbUrl || local || (isUploadImage(file, accept) ? file?.url : void 0);
4075
4321
  }
4076
4322
  function UploadItem({ file, files, props, actions }) {
4077
- const source = useThumbnail(file);
4323
+ const source = useThumbnail(file, props.accept);
4078
4324
  const [failed, setFailed] = (0, import_react23.useState)(false);
4325
+ const [downloading, setDownloading] = (0, import_react23.useState)(false);
4326
+ const downloadingRef = (0, import_react23.useRef)(false);
4327
+ const canPreview = !!(source || file.url || file.originFileObj || props.onPreview);
4328
+ const canDownload = !!(file.url || file.originFileObj || props.downloadRequest);
4329
+ async function download() {
4330
+ if (downloadingRef.current) return;
4331
+ downloadingRef.current = true;
4332
+ setDownloading(true);
4333
+ try {
4334
+ await actions.download();
4335
+ } catch {
4336
+ message.error("\u4E0B\u8F7D\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u5730\u5740\u3001\u8BBF\u95EE\u6743\u9650\u6216\u8DE8\u57DF\u914D\u7F6E");
4337
+ } finally {
4338
+ downloadingRef.current = false;
4339
+ setDownloading(false);
4340
+ }
4341
+ }
4079
4342
  (0, import_react23.useEffect)(() => setFailed(false), [source]);
4080
- const node = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: `sia-upload__item sia-upload__item--${file.status ?? "ready"}`, children: [
4081
- props.listType !== "text" ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Button, { variant: "text", className: "sia-upload__thumbnail", "aria-label": `\u9884\u89C8 ${file.name}`, onClick: actions.preview, disabled: !source && !props.onPreview, children: source && !failed ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("img", { src: source, alt: file.name, onError: () => setFailed(true) }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: file.type?.startsWith("image/") ? "image" : "file", size: 28 }) }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: file.status === "done" ? "circle-check" : "file", size: 16 }),
4082
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sia-upload__name", ...withTitleTooltip({ title: file.relativePath || file.name }), children: file.url ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("a", { href: file.url, target: "_blank", rel: "noreferrer", children: file.name }) : file.name }),
4083
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Button, { variant: "text", className: "sia-upload__remove", disabled: props.disabled, "aria-label": `\u79FB\u9664 ${file.name}`, onClick: () => void actions.remove(), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "close", size: 14 }) }),
4084
- file.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "sia-upload__uploading", children: [
4085
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { children: [
4343
+ const node = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: `sia-upload__item sia-upload__item--${file.status ?? "ready"}`, children: [
4344
+ props.listType !== "text" ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Button, { variant: "text", className: "sia-upload__thumbnail", "aria-label": `\u9884\u89C8\u7F29\u7565\u56FE ${file.name}`, onClick: actions.preview, disabled: !canPreview, children: source && !failed ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("img", { src: source, alt: file.name, onError: () => setFailed(true) }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: file.type?.startsWith("image/") ? "image" : "file", size: 28 }) }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: file.status === "done" ? "circle-check" : "file", size: 16 }),
4345
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sia-upload__name", ...withTitleTooltip({ title: file.relativePath || file.name }), children: file.url ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("a", { href: file.url, target: "_blank", rel: "noreferrer", children: file.name }) : file.name }),
4346
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "sia-upload__actions", children: [
4347
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Button, { variant: "text", title: "\u9884\u89C8", "aria-label": `\u9884\u89C8 ${file.name}`, disabled: !canPreview, onClick: actions.preview, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "eye", size: 14 }) }),
4348
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Button, { variant: "text", title: "\u4E0B\u8F7D", "aria-label": `\u4E0B\u8F7D ${file.name}`, disabled: !canDownload, loading: downloading, onClick: () => void download(), children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "download", size: 14 }) }),
4349
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Button, { variant: "text", title: "\u79FB\u9664", className: "sia-upload__remove", disabled: props.disabled, "aria-label": `\u79FB\u9664 ${file.name}`, onClick: () => void actions.remove(), children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "close", size: 14 }) })
4350
+ ] }),
4351
+ file.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "sia-upload__uploading", children: [
4352
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { children: [
4086
4353
  "\u4E0A\u4F20\u4E2D ",
4087
4354
  Math.round(file.percent ?? 0),
4088
4355
  "%"
4089
4356
  ] }),
4090
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sia-upload__progress", role: "progressbar", "aria-label": `${file.name} \u4E0A\u4F20\u8FDB\u5EA6`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": file.percent ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { style: { width: `${file.percent ?? 0}%` } }) })
4357
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sia-upload__progress", role: "progressbar", "aria-label": `${file.name} \u4E0A\u4F20\u8FDB\u5EA6`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": file.percent ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { style: { width: `${file.percent ?? 0}%` } }) })
4091
4358
  ] }),
4092
- file.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sia-upload__error", children: "\u4E0A\u4F20\u5931\u8D25" })
4359
+ file.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sia-upload__error", children: "\u4E0A\u4F20\u5931\u8D25" })
4093
4360
  ] });
4094
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_jsx_runtime22.Fragment, { children: props.itemRender ? props.itemRender(node, file, files, actions) : node });
4361
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: props.itemRender ? props.itemRender(node, file, files, actions) : node });
4095
4362
  }
4096
4363
  var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4097
4364
  accept,
@@ -4105,6 +4372,7 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4105
4372
  fileList,
4106
4373
  defaultFileList = [],
4107
4374
  listType = "text",
4375
+ cardSize,
4108
4376
  showUploadList = true,
4109
4377
  beforeUpload,
4110
4378
  request,
@@ -4114,9 +4382,11 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4114
4382
  onReject,
4115
4383
  onReadError,
4116
4384
  onPreview,
4385
+ downloadRequest,
4117
4386
  itemRender,
4118
4387
  children,
4119
4388
  className = "",
4389
+ style,
4120
4390
  onDragOver,
4121
4391
  onDragLeave,
4122
4392
  onDrop,
@@ -4124,6 +4394,10 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4124
4394
  tabIndex,
4125
4395
  ...props
4126
4396
  }, ref) {
4397
+ const cardDimension = (value) => value !== void 0 && Number.isFinite(value) ? Math.max(48, value) : 112;
4398
+ const cardWidth = cardDimension(typeof cardSize === "number" ? cardSize : cardSize?.width);
4399
+ const cardHeight = cardDimension(typeof cardSize === "number" ? cardSize : cardSize?.height);
4400
+ const compactCard = listType === "picture-card" && (cardWidth < 88 || cardHeight < 88);
4127
4401
  const inputRef = (0, import_react23.useRef)(null);
4128
4402
  const [internalFiles, setInternalFiles] = (0, import_react23.useState)(defaultFileList);
4129
4403
  const files = fileList ?? internalFiles;
@@ -4133,7 +4407,7 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4133
4407
  const mounted = (0, import_react23.useRef)(true);
4134
4408
  const [dragging, setDragging] = (0, import_react23.useState)(false);
4135
4409
  const [preview, setPreview] = (0, import_react23.useState)();
4136
- const previewSource = useThumbnail(preview);
4410
+ const previewSource = useThumbnail(preview, accept);
4137
4411
  const limit = maxCount2 === void 0 || !Number.isFinite(maxCount2) ? Infinity : Math.max(0, Math.floor(maxCount2));
4138
4412
  const full = files.length >= limit;
4139
4413
  const current = (0, import_react23.useRef)({ disabled, directory, recursive });
@@ -4313,18 +4587,19 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4313
4587
  event.preventDefault();
4314
4588
  ingest(selected.map((file) => ({ file, path: file.name })));
4315
4589
  }
4316
- const trigger = !(listType === "picture-card" && full) && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "sia-upload__trigger", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Button, { className: "sia-upload__choose", disabled: disabled || full, onClick: () => inputRef.current?.click(), children: children ?? (listType === "picture-card" ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
4317
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "plus", size: 24 }),
4318
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: "\u4E0A\u4F20\u56FE\u7247" })
4319
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
4320
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "upload", size: 16 }),
4590
+ const trigger = !(listType === "picture-card" && full) && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "sia-upload__trigger", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Button, { className: "sia-upload__choose", "aria-label": compactCard ? directory ? "\u9009\u62E9\u6587\u4EF6\u5939" : "\u9009\u62E9\u6587\u4EF6" : void 0, disabled: disabled || full, onClick: () => inputRef.current?.click(), children: children ?? (listType === "picture-card" ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
4591
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "plus", size: 24 }),
4592
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { children: "\u4E0A\u4F20\u56FE\u7247" })
4593
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
4594
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "upload", size: 16 }),
4321
4595
  directory ? "\u9009\u62E9\u6587\u4EF6\u5939" : "\u9009\u62E9\u6587\u4EF6"
4322
4596
  ] })) }) });
4323
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4597
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
4324
4598
  "div",
4325
4599
  {
4326
4600
  ...withTitleTooltip(props),
4327
- className: `sia-upload sia-upload--${listType}${dragging ? " is-dragging" : ""}${disabled ? " is-disabled" : ""} ${className}`.trim(),
4601
+ className: `sia-upload sia-upload--${listType}${compactCard ? " sia-upload--compact-card" : ""}${dragging ? " is-dragging" : ""}${disabled ? " is-disabled" : ""} ${className}`.trim(),
4602
+ style: listType === "picture-card" ? { "--sia-upload-card-width": `${cardWidth}px`, "--sia-upload-card-height": `${cardHeight}px`, ...style } : style,
4328
4603
  tabIndex: tabIndex ?? (pastable && !disabled ? 0 : void 0),
4329
4604
  onPaste: handlePaste,
4330
4605
  onDragOver: (event) => {
@@ -4340,7 +4615,7 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4340
4615
  },
4341
4616
  onDrop: (event) => void handleDrop(event),
4342
4617
  children: [
4343
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4618
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4344
4619
  "input",
4345
4620
  {
4346
4621
  ref: inputRef,
@@ -4357,19 +4632,25 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4357
4632
  }
4358
4633
  ),
4359
4634
  listType !== "picture-card" && trigger,
4360
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "sia-upload__list", children: [
4361
- showUploadList && files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(UploadItem, { file, files, props: { listType, disabled, itemRender, onPreview }, actions: {
4635
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "sia-upload__list", children: [
4636
+ showUploadList && files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(UploadItem, { file, files, props: { listType, disabled, itemRender, onPreview, downloadRequest, accept }, actions: {
4362
4637
  upload: () => start(file),
4363
4638
  abort: () => abort(file.uid),
4364
4639
  remove: () => remove(file.uid),
4640
+ download: () => downloadUploadFile(file, downloadRequest),
4365
4641
  preview: () => {
4366
4642
  if (onPreview) onPreview(file);
4367
- else setPreview(file);
4643
+ else if (isUploadImage(file, accept)) setPreview(file);
4644
+ else if (file.url || file.originFileObj) {
4645
+ const url = file.url || URL.createObjectURL(file.originFileObj);
4646
+ window.open(url, "_blank", "noopener,noreferrer");
4647
+ if (!file.url) window.setTimeout(() => URL.revokeObjectURL(url), 6e4);
4648
+ } else if (file.thumbUrl) setPreview(file);
4368
4649
  }
4369
4650
  } }, file.uid)),
4370
4651
  listType === "picture-card" && trigger
4371
4652
  ] }),
4372
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ImagePreview, { open: !!preview && !!(preview.url || previewSource), src: preview?.url || previewSource, alt: preview?.name, onOpenChange: (open) => {
4653
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ImagePreview, { open: !!preview && !!(preview.url || previewSource), src: preview?.url || previewSource, alt: preview?.name, onOpenChange: (open) => {
4373
4654
  if (!open) setPreview(void 0);
4374
4655
  } })
4375
4656
  ]
@@ -4377,15 +4658,15 @@ var UploadRoot = (0, import_react23.forwardRef)(function UploadRoot2({
4377
4658
  );
4378
4659
  });
4379
4660
  var UploadDragger = (0, import_react23.forwardRef)(function UploadDragger2({ hint = "\u652F\u6301\u5355\u4E2A\u6216\u6279\u91CF\u4E0A\u4F20", children, className = "", ...props }, ref) {
4380
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(UploadRoot, { ...props, ref, className: `sia-upload-dragger ${className}`.trim(), children: children ?? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { className: "sia-upload-dragger__content", children: [
4381
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Icon, { name: "upload", size: 32 }),
4382
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("strong", { children: [
4661
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(UploadRoot, { ...props, ref, className: `sia-upload-dragger ${className}`.trim(), children: children ?? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "sia-upload-dragger__content", children: [
4662
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "upload", size: 32 }),
4663
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("strong", { children: [
4383
4664
  "\u70B9\u51FB\u6216\u62D6\u62FD",
4384
4665
  props.directory ? "\u6587\u4EF6\u5939" : "\u6587\u4EF6",
4385
4666
  "\u5230\u6B64\u533A\u57DF\u4E0A\u4F20"
4386
4667
  ] }),
4387
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: hint }),
4388
- props.pastable && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: "\u805A\u7126\u6B64\u533A\u57DF\u540E\u6309 Ctrl / \u2318 + V \u7C98\u8D34\u6587\u4EF6\u6216\u622A\u56FE" })
4668
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { children: hint }),
4669
+ props.pastable && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { children: "\u805A\u7126\u6B64\u533A\u57DF\u540E\u6309 Ctrl / \u2318 + V \u7C98\u8D34\u6587\u4EF6\u6216\u622A\u56FE" })
4389
4670
  ] }) });
4390
4671
  });
4391
4672
  var Upload = Object.assign(UploadRoot, {
@@ -4395,111 +4676,6 @@ var Upload = Object.assign(UploadRoot, {
4395
4676
  LIST_IGNORE
4396
4677
  });
4397
4678
 
4398
- // src/components/Message.tsx
4399
- var import_client4 = require("react-dom/client");
4400
- var import_jsx_runtime23 = require("react/jsx-runtime");
4401
- var messageSeed = 0;
4402
- var messageRoot = null;
4403
- var messageContainer = null;
4404
- var messageItems = [];
4405
- var defaultDuration = 3;
4406
- var maxCount = 5;
4407
- var messageTimers = /* @__PURE__ */ new Map();
4408
- var messageIcons = {
4409
- info: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "info", size: 18 }),
4410
- success: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "circle-check", size: 18 }),
4411
- warning: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "warning", size: 18 }),
4412
- error: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "circle-close", size: 18 }),
4413
- loading: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "loader", size: 18, spin: true })
4414
- };
4415
- function ensureMessageRoot() {
4416
- if (typeof document === "undefined") return false;
4417
- if (!messageContainer) {
4418
- messageContainer = document.createElement("div");
4419
- messageContainer.className = "sia-message-root";
4420
- document.body.appendChild(messageContainer);
4421
- messageRoot = (0, import_client4.createRoot)(messageContainer);
4422
- }
4423
- return true;
4424
- }
4425
- function renderMessages() {
4426
- if (!ensureMessageRoot()) return;
4427
- messageRoot.render(/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "sia-message-list", "aria-live": "polite", children: messageItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: `sia-message sia-message--${item.type}`, role: item.type === "loading" ? "status" : "alert", children: [
4428
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sia-message__icon", "aria-hidden": "true", children: messageIcons[item.type] }),
4429
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sia-message__content", children: item.content }),
4430
- item.closable ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", className: "sia-message__close", "aria-label": "\u5173\u95ED\u63D0\u793A", onClick: () => closeMessage(item.key), children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Icon, { name: "close", size: 14 }) }) : null
4431
- ] }, item.key)) }));
4432
- }
4433
- function closeMessage(key) {
4434
- const item = messageItems.find((candidate) => candidate.key === key);
4435
- const timer = messageTimers.get(key);
4436
- if (timer !== void 0) window.clearTimeout(timer);
4437
- messageTimers.delete(key);
4438
- messageItems = messageItems.filter((candidate) => candidate.key !== key);
4439
- item?.onClose?.();
4440
- renderMessages();
4441
- }
4442
- function scheduleMessage(item) {
4443
- const existingTimer = messageTimers.get(item.key);
4444
- if (existingTimer !== void 0) window.clearTimeout(existingTimer);
4445
- const duration = item.duration ?? (item.type === "loading" ? 0 : defaultDuration);
4446
- if (duration > 0) {
4447
- messageTimers.set(item.key, window.setTimeout(() => closeMessage(item.key), duration * 1e3));
4448
- }
4449
- }
4450
- function openMessage(input) {
4451
- const config = typeof input === "object" && input !== null && "content" in input ? input : { content: input };
4452
- const key = config.key ?? `sia-message-${++messageSeed}`;
4453
- const item = { ...config, key, type: config.type ?? "info" };
4454
- const existingIndex = messageItems.findIndex((candidate) => candidate.key === key);
4455
- if (existingIndex >= 0) messageItems = messageItems.map((candidate, index) => index === existingIndex ? item : candidate);
4456
- else messageItems = [...messageItems, item];
4457
- while (messageItems.length > maxCount) closeMessage(messageItems[0].key);
4458
- renderMessages();
4459
- scheduleMessage(item);
4460
- return {
4461
- close: () => closeMessage(key),
4462
- update(next) {
4463
- const current = messageItems.find((candidate) => candidate.key === key);
4464
- if (!current) return;
4465
- const updated = { ...current, ...next, key, type: next.type ?? current.type };
4466
- messageItems = messageItems.map((candidate) => candidate.key === key ? updated : candidate);
4467
- renderMessages();
4468
- scheduleMessage(updated);
4469
- }
4470
- };
4471
- }
4472
- function shortcut(type) {
4473
- return (content, duration) => openMessage({ content, duration, type });
4474
- }
4475
- var message = {
4476
- /** 直接打开实例,返回可用于更新或关闭的操作句柄。 */
4477
- open: openMessage,
4478
- /** 显示普通信息提示。 */
4479
- info: shortcut("info"),
4480
- /** 显示成功提示。 */
4481
- success: shortcut("success"),
4482
- /** 显示警告提示。 */
4483
- warning: shortcut("warning"),
4484
- /** 显示错误提示。 */
4485
- error: shortcut("error"),
4486
- /** 显示持续加载提示。 */
4487
- loading: shortcut("loading"),
4488
- /** 关闭指定实例;未指定标识时关闭所有实例。 */
4489
- destroy(key) {
4490
- if (key !== void 0) {
4491
- closeMessage(key);
4492
- return;
4493
- }
4494
- [...messageItems].forEach((item) => closeMessage(item.key));
4495
- },
4496
- /** 配置消息默认持续时间和最大同时显示数量。 */
4497
- config(config) {
4498
- if (config.duration !== void 0) defaultDuration = config.duration;
4499
- if (config.maxCount !== void 0) maxCount = Math.max(1, config.maxCount);
4500
- }
4501
- };
4502
-
4503
4679
  // src/components/Breadcrumb.tsx
4504
4680
  var import_jsx_runtime24 = require("react/jsx-runtime");
4505
4681
  function Breadcrumb({ items, separator = "/", itemRender, className = "", ...props }) {