haiku-react-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1550 @@
1
+ 'use strict';
2
+
3
+ var classVarianceAuthority = require('class-variance-authority');
4
+ var clsx = require('clsx');
5
+ var tailwindMerge = require('tailwind-merge');
6
+ var react = require('@iconify/react');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+ var React2 = require('react');
9
+
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
29
+
30
+ // src/components/Button/button.tsx
31
+ function cn(...inputs) {
32
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
33
+ }
34
+ var buttonVariants = classVarianceAuthority.cva(
35
+ "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-primary-700 dark:focus-visible:ring-offset-zinc-950",
36
+ {
37
+ variants: {
38
+ variant: {
39
+ primary: "shadow-sm",
40
+ default: "border shadow-sm",
41
+ dashed: "border-2 border-dashed shadow-sm",
42
+ filled: "",
43
+ text: "",
44
+ link: "underline-offset-4 hover:underline"
45
+ },
46
+ color: {
47
+ default: "",
48
+ primary: "",
49
+ danger: "",
50
+ info: "",
51
+ success: "",
52
+ warning: ""
53
+ },
54
+ size: {
55
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
56
+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
57
+ lg: "h-10 px-6 has-[>svg]:px-4"
58
+ },
59
+ shape: {
60
+ default: "rounded-md",
61
+ rounded: "rounded-full",
62
+ square: "rounded-none"
63
+ }
64
+ },
65
+ compoundVariants: [
66
+ // primary (solid)
67
+ {
68
+ variant: "primary",
69
+ color: "default",
70
+ className: "bg-default text-white hover:bg-default"
71
+ },
72
+ {
73
+ variant: "primary",
74
+ color: "primary",
75
+ className: "bg-primary-900 text-primary-foreground hover:bg-primary-800"
76
+ },
77
+ {
78
+ variant: "primary",
79
+ color: "danger",
80
+ className: "bg-danger text-white hover:bg-danger"
81
+ },
82
+ {
83
+ variant: "primary",
84
+ color: "info",
85
+ className: "bg-info text-white hover:bg-info"
86
+ },
87
+ {
88
+ variant: "primary",
89
+ color: "success",
90
+ className: "bg-success text-white hover:bg-success"
91
+ },
92
+ {
93
+ variant: "primary",
94
+ color: "warning",
95
+ className: "bg-warning text-white hover:bg-warning"
96
+ },
97
+ // default (outline)
98
+ {
99
+ variant: "default",
100
+ color: "default",
101
+ className: "bg-white text-default border-default hover:bg-default-foreground hover:border-default"
102
+ },
103
+ {
104
+ variant: "default",
105
+ color: "primary",
106
+ className: "bg-white text-primary-900 border-primary-300 hover:bg-primary-100/60 "
107
+ },
108
+ {
109
+ variant: "default",
110
+ color: "danger",
111
+ className: "bg-white text-danger border-danger-foreground hover:bg-danger-50 hover:bg-danger-foreground/6"
112
+ },
113
+ {
114
+ variant: "default",
115
+ color: "info",
116
+ className: "bg-white text-info border-info-300 hover:bg-info-foreground/6 border-info-foreground"
117
+ },
118
+ {
119
+ variant: "default",
120
+ color: "success",
121
+ className: "bg-white text-success border-success-300 hover:bg-success-foreground/6 border-success-foreground"
122
+ },
123
+ {
124
+ variant: "default",
125
+ color: "warning",
126
+ className: "bg-white text-warning border-warning-300 hover:bg-warning-foreground/6 border-warning-foreground"
127
+ },
128
+ // dashed (dashed outline)
129
+ {
130
+ variant: "dashed",
131
+ color: "default",
132
+ className: "bg-white text-default border-default hover:bg-default-foreground hover:border-default"
133
+ },
134
+ {
135
+ variant: "dashed",
136
+ color: "primary",
137
+ className: "bg-white text-primary-900 border-primary-300 hover:bg-primary-foreground hover:border-primary-400"
138
+ },
139
+ {
140
+ variant: "dashed",
141
+ color: "danger",
142
+ className: "bg-white text-danger border-danger-foreground hover:bg-danger-50 hover:border-danger-foreground"
143
+ },
144
+ {
145
+ variant: "dashed",
146
+ color: "info",
147
+ className: "bg-white text-blue-700 border-blue-300 hover:bg-blue-50 hover:border-blue-400"
148
+ },
149
+ {
150
+ variant: "dashed",
151
+ color: "success",
152
+ className: "bg-white text-emerald-700 border-emerald-300 hover:bg-emerald-50 hover:border-emerald-400"
153
+ },
154
+ {
155
+ variant: "dashed",
156
+ color: "warning",
157
+ className: "bg-white text-amber-800 border-amber-300 hover:bg-amber-50 hover:border-amber-400"
158
+ },
159
+ // filled (tinted)
160
+ {
161
+ variant: "filled",
162
+ color: "default",
163
+ className: "bg-zinc-100 text-zinc-900 hover:bg-zinc-200"
164
+ },
165
+ {
166
+ variant: "filled",
167
+ color: "primary",
168
+ className: "bg-primary-100 text-primary-900 hover:bg-primary-200"
169
+ },
170
+ {
171
+ variant: "filled",
172
+ color: "danger",
173
+ className: "bg-red-50 text-red-700 hover:bg-red-100"
174
+ },
175
+ {
176
+ variant: "filled",
177
+ color: "info",
178
+ className: "bg-blue-50 text-blue-700 hover:bg-blue-100"
179
+ },
180
+ {
181
+ variant: "filled",
182
+ color: "success",
183
+ className: "bg-emerald-50 text-emerald-700 hover:bg-emerald-100"
184
+ },
185
+ {
186
+ variant: "filled",
187
+ color: "warning",
188
+ className: "bg-amber-50 text-amber-800 hover:bg-amber-100"
189
+ },
190
+ // text (no border/bg)
191
+ {
192
+ variant: "text",
193
+ color: "default",
194
+ className: "text-zinc-900 hover:bg-zinc-100"
195
+ },
196
+ {
197
+ variant: "text",
198
+ color: "primary",
199
+ className: "text-primary-900 hover:bg-primary-100"
200
+ },
201
+ {
202
+ variant: "text",
203
+ color: "danger",
204
+ className: "text-red-700 hover:bg-red-50"
205
+ },
206
+ {
207
+ variant: "text",
208
+ color: "info",
209
+ className: "text-blue-700 hover:bg-blue-50"
210
+ },
211
+ {
212
+ variant: "text",
213
+ color: "success",
214
+ className: "text-emerald-700 hover:bg-emerald-50"
215
+ },
216
+ {
217
+ variant: "text",
218
+ color: "warning",
219
+ className: "text-amber-800 hover:bg-amber-50"
220
+ },
221
+ // link (no border/bg)
222
+ {
223
+ variant: "link",
224
+ color: "default",
225
+ className: "text-zinc-900 hover:text-zinc-700"
226
+ },
227
+ {
228
+ variant: "link",
229
+ color: "primary",
230
+ className: "text-primary-900 hover:text-primary-800"
231
+ },
232
+ {
233
+ variant: "link",
234
+ color: "danger",
235
+ className: "text-red-700 hover:text-red-600"
236
+ },
237
+ {
238
+ variant: "link",
239
+ color: "info",
240
+ className: "text-blue-700 hover:text-blue-600"
241
+ },
242
+ {
243
+ variant: "link",
244
+ color: "success",
245
+ className: "text-emerald-700 hover:text-emerald-600"
246
+ },
247
+ {
248
+ variant: "link",
249
+ color: "warning",
250
+ className: "text-amber-800 hover:text-amber-700"
251
+ }
252
+ ],
253
+ defaultVariants: {
254
+ variant: "default",
255
+ color: "default",
256
+ size: "default",
257
+ shape: "default"
258
+ }
259
+ }
260
+ );
261
+ function Button({
262
+ className,
263
+ variant = "default",
264
+ size = "default",
265
+ color,
266
+ shape = "default",
267
+ loading = false,
268
+ disabled,
269
+ children,
270
+ icon,
271
+ as = "button",
272
+ ...props
273
+ }) {
274
+ const resolvedColor = color != null ? color : variant === "primary" ? "primary" : "default";
275
+ const isDisabled = disabled || loading;
276
+ const isLabel = as === "label" || "htmlFor" in props;
277
+ const baseClassName = cn(
278
+ buttonVariants({ color: resolvedColor, variant, size, shape, className }),
279
+ loading && "disabled:cursor-wait opacity-70",
280
+ isDisabled && "cursor-not-allowed opacity-50"
281
+ );
282
+ if (isLabel) {
283
+ const { disabled: _disabled, ...labelProps } = props;
284
+ return /* @__PURE__ */ jsxRuntime.jsxs(
285
+ "label",
286
+ {
287
+ "data-slot": "button",
288
+ "data-variant": variant,
289
+ "data-size": size,
290
+ "data-shape": shape,
291
+ "data-color": resolvedColor,
292
+ "data-loading": loading ? "" : void 0,
293
+ "aria-busy": loading || void 0,
294
+ "aria-disabled": isDisabled || void 0,
295
+ className: baseClassName,
296
+ ...labelProps,
297
+ children: [
298
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(
299
+ "span",
300
+ {
301
+ "aria-hidden": "true",
302
+ className: "size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
303
+ }
304
+ ) : icon ? typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon, width: "16", height: "16" }) : icon : null,
305
+ children
306
+ ]
307
+ }
308
+ );
309
+ }
310
+ return /* @__PURE__ */ jsxRuntime.jsxs(
311
+ "button",
312
+ {
313
+ "data-slot": "button",
314
+ "data-variant": variant,
315
+ "data-size": size,
316
+ "data-shape": shape,
317
+ "data-color": resolvedColor,
318
+ "data-loading": loading ? "" : void 0,
319
+ "aria-busy": loading || void 0,
320
+ disabled: isDisabled,
321
+ className: baseClassName,
322
+ ...props,
323
+ children: [
324
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(
325
+ "span",
326
+ {
327
+ "aria-hidden": "true",
328
+ className: "size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
329
+ }
330
+ ) : icon ? typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon, width: "16", height: "16" }) : icon : null,
331
+ children
332
+ ]
333
+ }
334
+ );
335
+ }
336
+ var avatarVariants = classVarianceAuthority.cva(
337
+ "inline-flex items-center justify-center overflow-hidden align-middle text-primary-foreground",
338
+ {
339
+ variants: {
340
+ variant: {
341
+ default: "bg-muted-foreground ",
342
+ ghost: "text-white",
343
+ primary: "bg-primary-900 text-primary-foreground",
344
+ info: "bg-info text-info-foreground",
345
+ success: "bg-success text-success-foreground",
346
+ warning: "bg-warning text-warning-foreground",
347
+ danger: "bg-danger text-danger-foreground"
348
+ },
349
+ shape: {
350
+ rounded: "rounded-full",
351
+ square: "rounded-md"
352
+ },
353
+ size: {
354
+ sm: "h-8 w-8",
355
+ md: "h-10 w-10",
356
+ lg: "h-12 w-12"
357
+ }
358
+ },
359
+ defaultVariants: {
360
+ shape: "rounded",
361
+ size: "md",
362
+ variant: "default"
363
+ }
364
+ }
365
+ );
366
+ function Avatar({
367
+ alt,
368
+ icon,
369
+ shape,
370
+ size,
371
+ variant,
372
+ className,
373
+ url,
374
+ ...props
375
+ }) {
376
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(avatarVariants({ shape, size, variant }), className), ...props, children: icon ? typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon, className: "w-[80%] h-[80%]" }) : icon : url ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: url, alt, className: "w-full h-full object-cover" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: alt ? alt.charAt(0).toUpperCase() : "?" }) });
377
+ }
378
+ function PlayBoard({
379
+ icon,
380
+ heading,
381
+ description,
382
+ actions,
383
+ code,
384
+ defaultExpanded = false,
385
+ previewClassName,
386
+ className,
387
+ children,
388
+ ...props
389
+ }) {
390
+ const [expanded, setExpanded] = React2__namespace.useState(defaultExpanded);
391
+ const codeId = React2__namespace.useId();
392
+ const hasCode = typeof code === "string" && code.trim().length > 0;
393
+ return /* @__PURE__ */ jsxRuntime.jsxs(
394
+ "section",
395
+ {
396
+ className: cn(
397
+ "overflow-hidden rounded-xl border border-zinc-200/70 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950",
398
+ className
399
+ ),
400
+ ...props,
401
+ children: [
402
+ (heading || description || actions) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-zinc-200/70 px-5 py-4 dark:border-zinc-800", children: [
403
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
404
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
405
+ icon && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-9 items-center justify-center rounded-full bg-zinc-100 text-zinc-600 dark:bg-zinc-900 dark:text-zinc-300", children: icon }),
406
+ heading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-zinc-900 dark:text-zinc-50", children: heading })
407
+ ] }),
408
+ description && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: description })
409
+ ] }),
410
+ actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: actions })
411
+ ] }),
412
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("px-5 py-6", previewClassName), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg bg-zinc-50 p-5 dark:bg-zinc-900/40", children }) }),
413
+ hasCode && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-zinc-200/70 dark:border-zinc-800", children: [
414
+ /* @__PURE__ */ jsxRuntime.jsx(
415
+ "button",
416
+ {
417
+ type: "button",
418
+ className: "flex w-full items-center justify-center gap-2 px-5 py-3 text-sm text-zinc-600 hover:bg-zinc-50 dark:text-zinc-300 dark:hover:bg-zinc-900/30",
419
+ "aria-expanded": expanded,
420
+ "aria-controls": codeId,
421
+ onClick: () => setExpanded((v) => !v),
422
+ children: expanded ? "\u6536\u8D77\u4EE3\u7801" : "\u5C55\u5F00\u4EE3\u7801"
423
+ }
424
+ ),
425
+ expanded && /* @__PURE__ */ jsxRuntime.jsx(
426
+ "pre",
427
+ {
428
+ id: codeId,
429
+ className: "overflow-x-auto border-t border-zinc-200/70 bg-zinc-950 px-5 py-4 text-xs leading-relaxed text-zinc-50 dark:border-zinc-800",
430
+ children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: code })
431
+ }
432
+ )
433
+ ] })
434
+ ]
435
+ }
436
+ );
437
+ }
438
+ var playBoard_default = PlayBoard;
439
+ function getLongestCommonPrefix(strings) {
440
+ if (!strings.length) return "";
441
+ return strings.reduce((prefix, current) => {
442
+ let index = 0;
443
+ while (index < prefix.length && index < current.length && prefix[index] === current[index]) {
444
+ index += 1;
445
+ }
446
+ return prefix.slice(0, index);
447
+ });
448
+ }
449
+ function isNumber(value) {
450
+ return typeof value === "number" && Number.isFinite(value);
451
+ }
452
+ function getUid() {
453
+ return Math.random().toString(16).slice(2);
454
+ }
455
+ function requestFrame(callback) {
456
+ if (typeof requestAnimationFrame !== "undefined") {
457
+ return requestAnimationFrame(callback);
458
+ }
459
+ return setTimeout(() => callback(Date.now()), 16);
460
+ }
461
+ function cancelFrame(id) {
462
+ if (typeof cancelAnimationFrame !== "undefined") {
463
+ cancelAnimationFrame(id);
464
+ return;
465
+ }
466
+ clearTimeout(id);
467
+ }
468
+ function useTyping({
469
+ streaming,
470
+ content,
471
+ typing,
472
+ onTyping,
473
+ onTypingComplete
474
+ }) {
475
+ const [chunks, setChunks] = React2__namespace.useState([]);
476
+ const rafId = React2__namespace.useRef(-1);
477
+ const currentTaskId = React2__namespace.useRef(1);
478
+ const animatingRef = React2__namespace.useRef(false);
479
+ const renderedRef = React2__namespace.useRef("");
480
+ const streamingRef = React2__namespace.useRef(streaming);
481
+ streamingRef.current = streaming;
482
+ const mergedConfig = React2__namespace.useMemo(() => {
483
+ var _a, _b, _c, _d;
484
+ const base = {
485
+ effect: "fade-in",
486
+ interval: 100,
487
+ step: 6,
488
+ keepPrefix: true
489
+ };
490
+ if (typing === false) return base;
491
+ if (typing === true) return base;
492
+ const interval = (_a = typing.interval) != null ? _a : base.interval;
493
+ const step = (_b = typing.step) != null ? _b : base.step;
494
+ const effect = (_c = typing.effect) != null ? _c : base.effect;
495
+ const keepPrefix = (_d = typing.keepPrefix) != null ? _d : base.keepPrefix;
496
+ if (!isNumber(interval) || interval <= 0) {
497
+ throw new Error("[Bubble] invalid typing.interval, expect positive number.");
498
+ }
499
+ const stepIsNumber = isNumber(step);
500
+ if (!stepIsNumber && !Array.isArray(step)) {
501
+ throw new Error("[Bubble] invalid typing.step, expect number or [min,max].");
502
+ }
503
+ if (stepIsNumber && step <= 0) {
504
+ throw new Error("[Bubble] invalid typing.step, expect positive number.");
505
+ }
506
+ if (Array.isArray(step)) {
507
+ if (!isNumber(step[0]) || step[0] <= 0) {
508
+ throw new Error("[Bubble] invalid typing.step[0], expect positive number.");
509
+ }
510
+ if (!isNumber(step[1]) || step[1] <= 0) {
511
+ throw new Error("[Bubble] invalid typing.step[1], expect positive number.");
512
+ }
513
+ if (step[0] > step[1]) {
514
+ throw new Error("[Bubble] invalid typing.step, step[0] should <= step[1].");
515
+ }
516
+ }
517
+ return { effect, interval, step, keepPrefix };
518
+ }, [typing]);
519
+ const typingSourceRef = React2__namespace.useRef({
520
+ content,
521
+ interval: mergedConfig.interval,
522
+ step: mergedConfig.step
523
+ });
524
+ typingSourceRef.current = {
525
+ content,
526
+ interval: mergedConfig.interval,
527
+ step: mergedConfig.step
528
+ };
529
+ const reset = React2__namespace.useCallback(() => {
530
+ cancelFrame(rafId.current);
531
+ rafId.current = -1;
532
+ setChunks([]);
533
+ renderedRef.current = "";
534
+ animatingRef.current = false;
535
+ }, []);
536
+ const execute = React2__namespace.useCallback(
537
+ (taskId) => {
538
+ let lastFrameTime = 0;
539
+ renderedRef.current = mergedConfig.keepPrefix ? getLongestCommonPrefix([typingSourceRef.current.content, renderedRef.current]) : "";
540
+ setChunks(
541
+ renderedRef.current ? [{ text: renderedRef.current, id: getUid(), taskId, done: true }] : []
542
+ );
543
+ const tick = () => {
544
+ if (taskId !== currentTaskId.current) return;
545
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
546
+ const { content: fullContent, interval, step } = typingSourceRef.current;
547
+ if (now - lastFrameTime < interval) {
548
+ rafId.current = requestFrame(tick);
549
+ return;
550
+ }
551
+ const currentLen = renderedRef.current.length;
552
+ const resolvedStep = isNumber(step) ? step : Math.floor(Math.random() * (step[1] - step[0] + 1)) + step[0];
553
+ const nextText = fullContent.slice(currentLen, currentLen + resolvedStep);
554
+ if (!nextText) {
555
+ if (streamingRef.current) {
556
+ rafId.current = requestFrame(tick);
557
+ return;
558
+ }
559
+ setChunks([
560
+ {
561
+ text: renderedRef.current,
562
+ id: getUid(),
563
+ taskId,
564
+ done: true
565
+ }
566
+ ]);
567
+ onTypingComplete == null ? void 0 : onTypingComplete(fullContent);
568
+ animatingRef.current = false;
569
+ currentTaskId.current += 1;
570
+ return;
571
+ }
572
+ renderedRef.current += nextText;
573
+ setChunks(
574
+ (prev) => (
575
+ //把这段 nextText 作为一个新 chunk 追加到数组尾部(触发 UI 更新)。
576
+ prev.concat({
577
+ id: getUid(),
578
+ text: nextText,
579
+ taskId,
580
+ done: false
581
+ })
582
+ )
583
+ );
584
+ animatingRef.current = true;
585
+ lastFrameTime = now;
586
+ rafId.current = requestFrame(tick);
587
+ onTyping == null ? void 0 : onTyping(renderedRef.current, fullContent);
588
+ };
589
+ tick();
590
+ },
591
+ [mergedConfig.keepPrefix, onTyping, onTypingComplete]
592
+ );
593
+ React2__namespace.useEffect(() => {
594
+ if (!content) return reset();
595
+ if (content === renderedRef.current) return;
596
+ if (animatingRef.current && !content.startsWith(renderedRef.current)) {
597
+ cancelFrame(rafId.current);
598
+ animatingRef.current = false;
599
+ rafId.current = requestFrame(() => execute(currentTaskId.current += 1));
600
+ return;
601
+ }
602
+ if (!animatingRef.current) {
603
+ execute(currentTaskId.current);
604
+ }
605
+ }, [content, execute, reset]);
606
+ React2__namespace.useEffect(() => () => cancelFrame(rafId.current), []);
607
+ return {
608
+ chunks,
609
+ animating: animatingRef.current,
610
+ config: mergedConfig,
611
+ renderedText: renderedRef.current
612
+ };
613
+ }
614
+ function TypingContent({
615
+ content,
616
+ typing,
617
+ streaming,
618
+ className,
619
+ cursor = "|",
620
+ onTyping,
621
+ onTypingComplete
622
+ }) {
623
+ const { chunks, animating, config } = useTyping({
624
+ streaming,
625
+ content,
626
+ typing,
627
+ onTyping,
628
+ onTypingComplete
629
+ });
630
+ const effect = config.effect;
631
+ const isTypingEffect = typing !== true && effect === "typing";
632
+ return /* @__PURE__ */ jsxRuntime.jsxs(
633
+ "div",
634
+ {
635
+ className: cn(
636
+ "whitespace-pre-wrap break-words",
637
+ className
638
+ ),
639
+ children: [
640
+ chunks.map(
641
+ (chunk) => config.effect === "fade-in" && !chunk.done ? /* @__PURE__ */ jsxRuntime.jsx(
642
+ "span",
643
+ {
644
+ className: "inline animate-[haiku-bubble-fade-in_0.1s_linear]",
645
+ children: chunk.text
646
+ },
647
+ chunk.id
648
+ ) : /* @__PURE__ */ jsxRuntime.jsx(React2__namespace.Fragment, { children: chunk.text }, chunk.id)
649
+ ),
650
+ isTypingEffect && animating ? /* @__PURE__ */ jsxRuntime.jsx(
651
+ "span",
652
+ {
653
+ "aria-hidden": "true",
654
+ className: cn("ml-1 select-none font-black", "animate-[haiku-bubble-cursor-blink_0.8s_linear_infinite]"),
655
+ children: cursor
656
+ }
657
+ ) : null
658
+ ]
659
+ }
660
+ );
661
+ }
662
+ var bubbleRootVariants = classVarianceAuthority.cva("flex gap-3", {
663
+ variants: {
664
+ placement: {
665
+ start: "flex-row",
666
+ end: "flex-row-reverse"
667
+ }
668
+ },
669
+ defaultVariants: {
670
+ placement: "start"
671
+ }
672
+ });
673
+ var bubbleContentVariants = classVarianceAuthority.cva(
674
+ "max-w-full min-w-0 px-4 py-3 text-sm leading-6 text-default break-words",
675
+ {
676
+ variants: {
677
+ variant: {
678
+ filled: "bg-zinc-100 text-zinc-900 dark:bg-zinc-900/60 dark:text-zinc-50",
679
+ outlined: "border border-zinc-200/70 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
680
+ shadow: "bg-white text-zinc-900 shadow-sm dark:bg-zinc-950 dark:text-zinc-50",
681
+ borderless: "bg-transparent p-0"
682
+ },
683
+ shape: {
684
+ default: "rounded-xl",
685
+ round: "rounded-full",
686
+ corner: "rounded-xl"
687
+ },
688
+ placement: {
689
+ start: "",
690
+ end: ""
691
+ }
692
+ },
693
+ compoundVariants: [
694
+ { shape: "corner", placement: "start", className: "rounded-tl-sm" },
695
+ { shape: "corner", placement: "end", className: "rounded-tr-sm" }
696
+ ],
697
+ defaultVariants: {
698
+ variant: "filled",
699
+ shape: "default",
700
+ placement: "start"
701
+ }
702
+ }
703
+ );
704
+ function renderSlot(slot, content, info) {
705
+ if (!slot) return null;
706
+ return typeof slot === "function" ? slot(content, info) : slot;
707
+ }
708
+ function DefaultLoading() {
709
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
710
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-zinc-400 [animation-delay:-0.2s] dark:bg-zinc-500" }),
711
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-zinc-400 [animation-delay:-0.1s] dark:bg-zinc-500" }),
712
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-zinc-400 dark:bg-zinc-500" })
713
+ ] });
714
+ }
715
+ function Bubble({
716
+ content,
717
+ info,
718
+ placement = "start",
719
+ variant = "filled",
720
+ shape = "default",
721
+ header,
722
+ footer,
723
+ avatar,
724
+ extra,
725
+ contentRender,
726
+ typing,
727
+ typingCursor,
728
+ streaming = false,
729
+ loading = false,
730
+ loadingRender,
731
+ onTyping,
732
+ onTypingComplete,
733
+ className,
734
+ ...props
735
+ }) {
736
+ const mergedInfo = info != null ? info : {};
737
+ const rendered = contentRender ? contentRender(content, mergedInfo) : content;
738
+ const mergedTyping = typeof typing === "function" ? typing(content, mergedInfo) : typing;
739
+ const useInnerAnimation = Boolean(mergedTyping) && typeof rendered === "string";
740
+ React2__namespace.useEffect(() => {
741
+ if (useInnerAnimation) return;
742
+ if (streaming) return;
743
+ if (typeof rendered === "string" && rendered) {
744
+ onTypingComplete == null ? void 0 : onTypingComplete(rendered);
745
+ }
746
+ }, [rendered, streaming, useInnerAnimation, onTypingComplete]);
747
+ const contentNode = (() => {
748
+ if (loading) {
749
+ return loadingRender ? loadingRender() : /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading, {});
750
+ }
751
+ if (useInnerAnimation) {
752
+ return /* @__PURE__ */ jsxRuntime.jsx(
753
+ TypingContent,
754
+ {
755
+ content: rendered,
756
+ streaming,
757
+ typing: mergedTyping,
758
+ cursor: typingCursor,
759
+ onTyping,
760
+ onTypingComplete
761
+ }
762
+ );
763
+ }
764
+ return typeof rendered === "string" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap break-words", children: rendered }) : rendered;
765
+ })();
766
+ const avatarNode = renderSlot(avatar, content, mergedInfo);
767
+ const headerNode = renderSlot(header, content, mergedInfo);
768
+ const footerNode = renderSlot(footer, content, mergedInfo);
769
+ const extraNode = renderSlot(extra, content, mergedInfo);
770
+ return /* @__PURE__ */ jsxRuntime.jsxs(
771
+ "div",
772
+ {
773
+ className: cn(bubbleRootVariants({ placement }), className),
774
+ "data-slot": "bubble",
775
+ "data-placement": placement,
776
+ "data-variant": variant,
777
+ "data-shape": shape,
778
+ "data-loading": loading ? "" : void 0,
779
+ ...props,
780
+ children: [
781
+ avatarNode ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: avatarNode }) : null,
782
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 max-w-full flex-col", children: [
783
+ headerNode ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("mb-1 text-xs text-zinc-500 dark:text-zinc-400"), children: headerNode }) : null,
784
+ /* @__PURE__ */ jsxRuntime.jsx(
785
+ "div",
786
+ {
787
+ className: cn(
788
+ bubbleContentVariants({ variant, shape, placement }),
789
+ typeof rendered === "string" && "whitespace-pre-wrap"
790
+ ),
791
+ children: contentNode
792
+ }
793
+ ),
794
+ footerNode ? /* @__PURE__ */ jsxRuntime.jsx(
795
+ "div",
796
+ {
797
+ className: cn(
798
+ "mt-2 text-xs text-zinc-500 dark:text-zinc-400",
799
+ placement === "end" && "text-right"
800
+ ),
801
+ children: footerNode
802
+ }
803
+ ) : null
804
+ ] }),
805
+ extraNode && !loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: extraNode }) : null
806
+ ]
807
+ }
808
+ );
809
+ }
810
+
811
+ // src/components/Upload/utils.ts
812
+ var uploadFile = ({
813
+ action,
814
+ file,
815
+ method = "post",
816
+ header,
817
+ onProgress,
818
+ signal,
819
+ formData: providedFormData
820
+ }) => new Promise((resolve, reject) => {
821
+ const xhr = new XMLHttpRequest();
822
+ xhr.open(method.toUpperCase(), action, true);
823
+ const handleAbort = () => {
824
+ xhr.abort();
825
+ };
826
+ if (header) {
827
+ Object.entries(header).forEach(([key, value]) => {
828
+ xhr.setRequestHeader(key, value);
829
+ });
830
+ }
831
+ xhr.upload.onprogress = (event) => {
832
+ onProgress == null ? void 0 : onProgress(event);
833
+ };
834
+ xhr.onload = () => {
835
+ signal == null ? void 0 : signal.removeEventListener("abort", handleAbort);
836
+ resolve(xhr.responseText);
837
+ };
838
+ xhr.onerror = (event) => {
839
+ signal == null ? void 0 : signal.removeEventListener("abort", handleAbort);
840
+ reject(event);
841
+ };
842
+ xhr.onabort = () => {
843
+ signal == null ? void 0 : signal.removeEventListener("abort", handleAbort);
844
+ reject(new DOMException("Upload aborted", "AbortError"));
845
+ };
846
+ if (signal == null ? void 0 : signal.aborted) {
847
+ reject(new DOMException("Upload aborted", "AbortError"));
848
+ return;
849
+ }
850
+ signal == null ? void 0 : signal.addEventListener("abort", handleAbort);
851
+ const formData = providedFormData || new FormData();
852
+ if (!providedFormData && file) {
853
+ formData.append("file", file);
854
+ }
855
+ xhr.send(formData);
856
+ });
857
+ var uploadTriggerVariants = classVarianceAuthority.cva(
858
+ "flex cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white",
859
+ {
860
+ variants: {
861
+ type: {
862
+ button: "",
863
+ dragger: "flex-col border-2 border-dashed text-center",
864
+ image: "relative overflow-hidden p-0"
865
+ },
866
+ variant: {
867
+ default: "border border-default bg-white text-default hover:bg-default-foreground/10",
868
+ dashed: "border-2 border-dashed border-default bg-white text-default hover:bg-default-foreground/10"
869
+ },
870
+ size: {
871
+ sm: "",
872
+ md: "",
873
+ lg: ""
874
+ },
875
+ shape: {
876
+ rounded: "rounded-md",
877
+ square: "rounded-none"
878
+ }
879
+ },
880
+ compoundVariants: [
881
+ { type: "button", size: "sm", className: "h-8 px-3 text-xs" },
882
+ { type: "button", size: "md", className: "h-9 px-4 text-sm" },
883
+ { type: "button", size: "lg", className: "h-10 px-6 text-sm" },
884
+ { type: "dragger", size: "sm", className: "min-h-[96px] px-4 py-6 text-xs" },
885
+ { type: "dragger", size: "md", className: "min-h-[120px] px-6 py-8 text-sm" },
886
+ { type: "dragger", size: "lg", className: "min-h-[152px] px-8 py-10 text-sm" },
887
+ { type: "image", size: "sm", className: "h-15 w-15" },
888
+ { type: "image", size: "md", className: "h-23 w-23" },
889
+ { type: "image", size: "lg", className: "h-35 w-35" },
890
+ { type: "image", shape: "rounded", className: "rounded-full" }
891
+ ],
892
+ defaultVariants: {
893
+ type: "button",
894
+ variant: "default",
895
+ size: "md",
896
+ shape: "rounded"
897
+ }
898
+ }
899
+ );
900
+ function UploadTrigger({
901
+ inputId,
902
+ type = "button",
903
+ variant,
904
+ size,
905
+ shape,
906
+ className,
907
+ isDragging,
908
+ children,
909
+ ...props
910
+ }) {
911
+ return /* @__PURE__ */ jsxRuntime.jsx(
912
+ "label",
913
+ {
914
+ htmlFor: inputId,
915
+ className: cn(
916
+ uploadTriggerVariants({ type, variant, size, shape }),
917
+ isDragging && "border-primary-300 text-primary-900",
918
+ className
919
+ ),
920
+ ...props,
921
+ children
922
+ }
923
+ );
924
+ }
925
+ function ButtonUpload({
926
+ inputId,
927
+ variant,
928
+ size,
929
+ shape,
930
+ className,
931
+ status,
932
+ progress,
933
+ onCancel,
934
+ onReset
935
+ }) {
936
+ const isUploading = status === "uploading";
937
+ const widthClass = isUploading ? "w-[154px]" : "w-[130px]";
938
+ const mergedClassName = cn(
939
+ widthClass,
940
+ "justify-center transition-[width] duration-200 ease-out",
941
+ isUploading && "relative overflow-hidden",
942
+ className
943
+ );
944
+ const labelContent = status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 tabular-nums", children: [
945
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u4E0A\u4F20\u4E2D" }),
946
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-[3ch] text-right", children: progress }),
947
+ "%"
948
+ ] }) : status === "success" ? "\u4E0A\u4F20\u5B8C\u6210" : status === "error" ? "\u4E0A\u4F20\u5931\u8D25" : "\u70B9\u51FB\u4E0A\u4F20\u6587\u4EF6";
949
+ const reloadButton = status === "success" && onReset ? /* @__PURE__ */ jsxRuntime.jsx(
950
+ "button",
951
+ {
952
+ type: "button",
953
+ "aria-label": "Reset upload",
954
+ className: "ml-1 inline-flex items-center",
955
+ onClick: (event) => {
956
+ event.preventDefault();
957
+ event.stopPropagation();
958
+ onReset();
959
+ },
960
+ children: /* @__PURE__ */ jsxRuntime.jsx(
961
+ react.Icon,
962
+ {
963
+ icon: "bx:reset",
964
+ width: "18",
965
+ height: "18",
966
+ className: "cursor-pointer"
967
+ }
968
+ )
969
+ }
970
+ ) : null;
971
+ const cancelButton = status === "uploading" && onCancel ? /* @__PURE__ */ jsxRuntime.jsx(
972
+ "button",
973
+ {
974
+ type: "button",
975
+ "aria-label": "Cancel upload",
976
+ className: "ml-1 inline-flex items-center",
977
+ onClick: (event) => {
978
+ event.preventDefault();
979
+ event.stopPropagation();
980
+ onCancel();
981
+ },
982
+ children: /* @__PURE__ */ jsxRuntime.jsx(
983
+ react.Icon,
984
+ {
985
+ icon: "fluent-emoji-flat:stop-sign",
986
+ width: "18",
987
+ height: "18",
988
+ className: "cursor-pointer"
989
+ }
990
+ )
991
+ }
992
+ ) : null;
993
+ const statusIcon = status === "success" ? /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: "icon-park:success", width: "16", height: "16" }) : status === "error" ? /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: "material-icon-theme:folder-error", width: "16", height: "16" }) : status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: "line-md:loading-loop", width: "16", height: "16" }) : /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: "line-md:upload", width: "16", height: "16" });
994
+ return /* @__PURE__ */ jsxRuntime.jsxs(
995
+ UploadTrigger,
996
+ {
997
+ inputId,
998
+ type: "button",
999
+ variant,
1000
+ size,
1001
+ shape,
1002
+ className: mergedClassName,
1003
+ children: [
1004
+ isUploading && /* @__PURE__ */ jsxRuntime.jsx(
1005
+ "span",
1006
+ {
1007
+ "aria-hidden": "true",
1008
+ className: "pointer-events-none absolute inset-0 opacity-80 haiku-upload-shine"
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative z-10 inline-flex items-center gap-2", children: [
1012
+ statusIcon,
1013
+ labelContent,
1014
+ cancelButton,
1015
+ reloadButton
1016
+ ] })
1017
+ ]
1018
+ }
1019
+ );
1020
+ }
1021
+ function DraggerUpload({
1022
+ inputId,
1023
+ isDragging,
1024
+ onDragOver,
1025
+ onDragLeave,
1026
+ onDrop,
1027
+ variant,
1028
+ size,
1029
+ shape,
1030
+ className,
1031
+ status,
1032
+ progress
1033
+ }) {
1034
+ return /* @__PURE__ */ jsxRuntime.jsx(
1035
+ UploadTrigger,
1036
+ {
1037
+ inputId,
1038
+ type: "dragger",
1039
+ variant,
1040
+ size,
1041
+ shape,
1042
+ className,
1043
+ isDragging,
1044
+ onDragOver,
1045
+ onDragEnter: onDragOver,
1046
+ onDragLeave,
1047
+ onDrop,
1048
+ children: status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1049
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium inline-flex tabular-nums", children: [
1050
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u4E0A\u4F20\u4E2D" }),
1051
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "w-[4ch] text-right", children: [
1052
+ progress,
1053
+ "%"
1054
+ ] })
1055
+ ] }),
1056
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1 w-full rounded-full bg-default-foreground/20", children: /* @__PURE__ */ jsxRuntime.jsx(
1057
+ "div",
1058
+ {
1059
+ className: "h-full rounded-full bg-primary-900",
1060
+ style: { width: `${progress}%` }
1061
+ }
1062
+ ) })
1063
+ ] }) : status === "error" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u4E0A\u4F20\u5931\u8D25\uFF0C\u70B9\u51FB\u91CD\u8BD5" }) : status === "success" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u4E0A\u4F20\u5B8C\u6210" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1064
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u70B9\u51FB\u6216\u62D6\u62FD\u4E0A\u4F20" }),
1065
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-default opacity-70", children: "\u652F\u6301\u591A\u6587\u4EF6" })
1066
+ ] })
1067
+ }
1068
+ );
1069
+ }
1070
+ function ImageUpload({
1071
+ inputId,
1072
+ previewUrl,
1073
+ variant,
1074
+ size,
1075
+ shape,
1076
+ className,
1077
+ status,
1078
+ progress
1079
+ }) {
1080
+ const isUploading = status === "uploading";
1081
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1082
+ UploadTrigger,
1083
+ {
1084
+ inputId,
1085
+ type: "image",
1086
+ variant,
1087
+ size,
1088
+ shape,
1089
+ className,
1090
+ children: [
1091
+ previewUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: previewUrl, alt: "\u4E0A\u4F20\u9884\u89C8", className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs inline-flex font-medium", children: "\u4E0A\u4F20" }),
1092
+ isUploading && /* @__PURE__ */ jsxRuntime.jsx(
1093
+ "span",
1094
+ {
1095
+ "aria-hidden": "true",
1096
+ className: "pointer-events-none absolute inset-0 z-30 opacity-70 haiku-upload-shine"
1097
+ }
1098
+ ),
1099
+ isUploading && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute inset-0 z-20 tabular-nums text-right flex items-center justify-center bg-black/35 text-xs font-medium text-white", children: [
1100
+ progress,
1101
+ "%"
1102
+ ] }),
1103
+ status === "error" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inset-0 z-20 flex items-center justify-center bg-black/60 text-xs font-medium text-white", children: "\u4E0A\u4F20\u5931\u8D25" })
1104
+ ]
1105
+ }
1106
+ );
1107
+ }
1108
+ function FileItem({ file, onRemove, onCancel }) {
1109
+ const { id, file: fileData, status, progress, error } = file;
1110
+ const iconMap = {
1111
+ idle: { icon: "lucide:file", className: "text-gray-400" },
1112
+ uploading: { icon: "lucide:loader-2", className: "text-blue-500 animate-spin" },
1113
+ success: { icon: "lucide:check-circle", className: "text-green-500" },
1114
+ error: { icon: "lucide:x-circle", className: "text-red-500" }
1115
+ };
1116
+ const iconConfig = iconMap[status] || iconMap.idle;
1117
+ const formatSize = (bytes) => {
1118
+ if (bytes < 1024) return `${bytes} B`;
1119
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1120
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1121
+ };
1122
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 p-3 border rounded-md bg-white dark:bg-zinc-800", children: [
1123
+ /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: iconConfig.icon, className: iconConfig.className, width: 20 }),
1124
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
1125
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1126
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium text-md", children: fileData.name }),
1127
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 ml-2", children: formatSize(fileData.size) })
1128
+ ] }),
1129
+ (status === "uploading" || status === "success") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1", children: [
1130
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1.5 w-full bg-gray-200 rounded-full overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
1131
+ "div",
1132
+ {
1133
+ className: "h-full bg-blue-500 transition-all duration-300",
1134
+ style: { width: `${progress}%` }
1135
+ }
1136
+ ) }),
1137
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: [
1138
+ progress,
1139
+ "%"
1140
+ ] })
1141
+ ] }),
1142
+ status === "error" && error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 text-xs text-red-500", children: error.message })
1143
+ ] }),
1144
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1145
+ status === "uploading" && onCancel && /* @__PURE__ */ jsxRuntime.jsx(
1146
+ "button",
1147
+ {
1148
+ type: "button",
1149
+ onClick: () => onCancel(id),
1150
+ className: "p-1 hover:bg-gray-100 rounded",
1151
+ title: "\u53D6\u6D88",
1152
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: "lucide:x", className: "w-4 h-4 text-gray-500" })
1153
+ }
1154
+ ),
1155
+ status !== "uploading" && onRemove && /* @__PURE__ */ jsxRuntime.jsx(
1156
+ "button",
1157
+ {
1158
+ type: "button",
1159
+ onClick: () => onRemove(id),
1160
+ className: "p-1 hover:bg-gray-100 rounded",
1161
+ title: "\u5220\u9664",
1162
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { icon: "lucide:trash-2", className: "w-4 h-4 text-gray-500" })
1163
+ }
1164
+ )
1165
+ ] })
1166
+ ] });
1167
+ }
1168
+ function FileList({ files, onRemove, onCancel, show = true }) {
1169
+ if (!show || files.length === 0) {
1170
+ return null;
1171
+ }
1172
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 mt-4", children: files.map((file) => /* @__PURE__ */ jsxRuntime.jsx(
1173
+ FileItem,
1174
+ {
1175
+ file,
1176
+ onRemove,
1177
+ onCancel
1178
+ },
1179
+ file.id
1180
+ )) });
1181
+ }
1182
+ var DEFAULT_OPTIONS = {
1183
+ mode: "button",
1184
+ autoUpload: true,
1185
+ showFileList: true,
1186
+ variant: "default",
1187
+ size: "md",
1188
+ shape: "rounded",
1189
+ method: "post",
1190
+ name: "file"
1191
+ };
1192
+ var createUploadError = (type, message, file, originalError) => ({
1193
+ type,
1194
+ message,
1195
+ file,
1196
+ originalError
1197
+ });
1198
+ var generateFileId = () => Math.random().toString(36).substring(2, 11);
1199
+ function Upload(config) {
1200
+ const options = {
1201
+ ...DEFAULT_OPTIONS,
1202
+ ...config
1203
+ };
1204
+ const {
1205
+ mode,
1206
+ autoUpload,
1207
+ showFileList,
1208
+ directory,
1209
+ accept,
1210
+ maxSize,
1211
+ minSize,
1212
+ maxCount,
1213
+ variant,
1214
+ size,
1215
+ shape,
1216
+ action,
1217
+ method,
1218
+ headers,
1219
+ data,
1220
+ name,
1221
+ onChange,
1222
+ onProgress,
1223
+ onSuccess,
1224
+ onError,
1225
+ onComplete
1226
+ } = options;
1227
+ const [files, setFiles] = React2.useState([]);
1228
+ const [isDragging, setIsDragging] = React2.useState(false);
1229
+ const [previewUrl, setPreviewUrl] = React2.useState(null);
1230
+ const [status, setStatus] = React2.useState("idle");
1231
+ const [progress, setProgress] = React2.useState(0);
1232
+ const inputId = React2.useId();
1233
+ const abortRef = React2.useRef(null);
1234
+ const cancelRef = React2.useRef(false);
1235
+ const inputRef = React2.useRef(null);
1236
+ const isDisabled = status === "uploading";
1237
+ const notifyChange = React2.useCallback((newFiles) => {
1238
+ onChange == null ? void 0 : onChange(newFiles);
1239
+ }, [onChange]);
1240
+ const validateFile = React2.useCallback((file) => {
1241
+ if (maxSize && file.size > maxSize) {
1242
+ return createUploadError(
1243
+ "FILE_SIZE_EXCEEDED",
1244
+ `\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC7 ${Math.round(maxSize / 1024 / 1024)}MB`,
1245
+ file
1246
+ );
1247
+ }
1248
+ if (minSize && file.size < minSize) {
1249
+ return createUploadError(
1250
+ "FILE_SIZE_EXCEEDED",
1251
+ `\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u5C0F\u4E8E ${Math.round(minSize / 1024)}KB`,
1252
+ file
1253
+ );
1254
+ }
1255
+ return null;
1256
+ }, [maxSize, minSize]);
1257
+ const validateFiles = React2.useCallback((newFiles) => {
1258
+ const valid = [];
1259
+ const errors = [];
1260
+ if (maxCount && newFiles.length > maxCount) {
1261
+ const error = createUploadError(
1262
+ "FILE_COUNT_EXCEEDED",
1263
+ `\u6700\u591A\u53EA\u80FD\u4E0A\u4F20 ${maxCount} \u4E2A\u6587\u4EF6`
1264
+ );
1265
+ errors.push(error);
1266
+ }
1267
+ for (const file of newFiles) {
1268
+ const error = validateFile(file);
1269
+ if (error) {
1270
+ errors.push(error);
1271
+ } else {
1272
+ valid.push(file);
1273
+ }
1274
+ }
1275
+ return { valid, errors };
1276
+ }, [maxCount, validateFile]);
1277
+ const uploadSingleFile = async (fileItem, totalBytes, uploadedBytes) => {
1278
+ const { file } = fileItem;
1279
+ const controller = new AbortController();
1280
+ abortRef.current = controller;
1281
+ try {
1282
+ const formData = new FormData();
1283
+ formData.append(name, file);
1284
+ if (data) {
1285
+ Object.entries(data).forEach(([key, value]) => {
1286
+ formData.append(key, String(value));
1287
+ });
1288
+ }
1289
+ const responseText = await uploadFile({
1290
+ action,
1291
+ method,
1292
+ header: headers,
1293
+ signal: controller.signal,
1294
+ formData,
1295
+ onProgress: (event) => {
1296
+ if (!event.lengthComputable || file.size === 0) {
1297
+ return;
1298
+ }
1299
+ const percent = Math.min(
1300
+ 100,
1301
+ Math.round(event.loaded / file.size * 100)
1302
+ );
1303
+ const currentTotalBytes = uploadedBytes + event.loaded;
1304
+ setProgress(
1305
+ Math.round(currentTotalBytes / totalBytes * 100)
1306
+ );
1307
+ onProgress == null ? void 0 : onProgress(percent, file);
1308
+ setFiles(
1309
+ (prev) => prev.map(
1310
+ (f) => f.id === fileItem.id ? { ...f, progress: percent } : f
1311
+ )
1312
+ );
1313
+ }
1314
+ });
1315
+ const updatedFile = {
1316
+ ...fileItem,
1317
+ status: "success",
1318
+ progress: 100,
1319
+ response: responseText
1320
+ };
1321
+ onSuccess == null ? void 0 : onSuccess(responseText, file);
1322
+ return updatedFile;
1323
+ } catch (event) {
1324
+ const isAbort = cancelRef.current || (event == null ? void 0 : event.name) === "AbortError";
1325
+ if (isAbort) {
1326
+ return { ...fileItem, status: "idle", progress: 0 };
1327
+ }
1328
+ const error = createUploadError(
1329
+ "NETWORK_ERROR",
1330
+ event instanceof Error ? event.message : "\u4E0A\u4F20\u5931\u8D25",
1331
+ file,
1332
+ event instanceof Error ? event : void 0
1333
+ );
1334
+ onError == null ? void 0 : onError(error, file);
1335
+ return { ...fileItem, status: "error", error };
1336
+ }
1337
+ };
1338
+ const uploadFiles = React2.useCallback(async (filesToUpload) => {
1339
+ var _a;
1340
+ if (!action || filesToUpload.length === 0) {
1341
+ setStatus("idle");
1342
+ setProgress(0);
1343
+ return;
1344
+ }
1345
+ if (!autoUpload) {
1346
+ return;
1347
+ }
1348
+ cancelRef.current = false;
1349
+ (_a = abortRef.current) == null ? void 0 : _a.abort();
1350
+ abortRef.current = null;
1351
+ setStatus("uploading");
1352
+ setProgress(0);
1353
+ const totalBytes = filesToUpload.reduce((sum, f) => sum + f.file.size, 0);
1354
+ let uploadedBytes = 0;
1355
+ const uploadedFiles = [];
1356
+ for (const fileItem of filesToUpload) {
1357
+ if (cancelRef.current) {
1358
+ setStatus("idle");
1359
+ setProgress(0);
1360
+ return;
1361
+ }
1362
+ setFiles(
1363
+ (prev) => prev.map(
1364
+ (f) => f.id === fileItem.id ? { ...f, status: "uploading" } : f
1365
+ )
1366
+ );
1367
+ const result = await uploadSingleFile(fileItem, totalBytes, uploadedBytes);
1368
+ uploadedFiles.push(result);
1369
+ setFiles(
1370
+ (prev) => prev.map(
1371
+ (f) => f.id === fileItem.id ? { ...f, status: result.status, response: result.response } : f
1372
+ )
1373
+ );
1374
+ if (result.status === "success") {
1375
+ uploadedBytes += fileItem.file.size;
1376
+ setProgress(
1377
+ totalBytes === 0 ? 100 : Math.round(uploadedBytes / totalBytes * 100)
1378
+ );
1379
+ } else if (result.status === "error") {
1380
+ setStatus("error");
1381
+ return;
1382
+ }
1383
+ }
1384
+ setStatus("success");
1385
+ setProgress(100);
1386
+ onComplete == null ? void 0 : onComplete(uploadedFiles);
1387
+ }, [action, autoUpload, headers, method, name, data, onComplete, onError, onProgress, onSuccess]);
1388
+ const handleFiles = React2.useCallback((fileList) => {
1389
+ const newFiles = Array.from(fileList);
1390
+ const { valid, errors } = validateFiles(newFiles);
1391
+ errors.forEach((error) => {
1392
+ onError == null ? void 0 : onError(error, error.file);
1393
+ });
1394
+ if (valid.length === 0) {
1395
+ return;
1396
+ }
1397
+ const fileItems = valid.map((file) => ({
1398
+ id: generateFileId(),
1399
+ file,
1400
+ status: "idle",
1401
+ progress: 0
1402
+ }));
1403
+ setFiles((prev) => {
1404
+ const newFileList = [...prev, ...fileItems];
1405
+ notifyChange(newFileList);
1406
+ return newFileList;
1407
+ });
1408
+ void uploadFiles(fileItems);
1409
+ }, [validateFiles, notifyChange, uploadFiles, onError]);
1410
+ const handleInputChange = (event) => {
1411
+ if (event.target.files) {
1412
+ handleFiles(event.target.files);
1413
+ event.target.value = "";
1414
+ }
1415
+ };
1416
+ const handleDragOver = (event) => {
1417
+ event.preventDefault();
1418
+ setIsDragging(true);
1419
+ };
1420
+ const handleDragLeave = (event) => {
1421
+ event.preventDefault();
1422
+ setIsDragging(false);
1423
+ };
1424
+ const handleDrop = (event) => {
1425
+ event.preventDefault();
1426
+ setIsDragging(false);
1427
+ if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
1428
+ handleFiles(event.dataTransfer.files);
1429
+ event.dataTransfer.clearData();
1430
+ }
1431
+ };
1432
+ const handleCancel = React2.useCallback(() => {
1433
+ var _a;
1434
+ if (status !== "uploading") {
1435
+ return;
1436
+ }
1437
+ cancelRef.current = true;
1438
+ (_a = abortRef.current) == null ? void 0 : _a.abort();
1439
+ abortRef.current = null;
1440
+ setStatus("idle");
1441
+ setProgress(0);
1442
+ setFiles([]);
1443
+ }, [status]);
1444
+ const handleReset = React2.useCallback(() => {
1445
+ setFiles([]);
1446
+ setStatus("idle");
1447
+ setProgress(0);
1448
+ setPreviewUrl(null);
1449
+ if (inputRef.current) {
1450
+ inputRef.current.value = "";
1451
+ }
1452
+ notifyChange([]);
1453
+ }, [notifyChange]);
1454
+ const handleRemoveFile = React2.useCallback((id) => {
1455
+ setFiles((prev) => {
1456
+ const newFiles = prev.filter((f) => f.id !== id);
1457
+ notifyChange(newFiles);
1458
+ return newFiles;
1459
+ });
1460
+ }, [notifyChange]);
1461
+ const handleCancelFile = React2.useCallback((id) => {
1462
+ setFiles(
1463
+ (prev) => prev.map(
1464
+ (f) => f.id === id ? { ...f, status: "idle", progress: 0 } : f
1465
+ )
1466
+ );
1467
+ }, []);
1468
+ React2.useCallback(() => {
1469
+ if (files.length > 0 && autoUpload === false) {
1470
+ const pendingFiles = files.filter((f) => f.status === "idle");
1471
+ if (pendingFiles.length > 0) {
1472
+ uploadFiles(pendingFiles);
1473
+ }
1474
+ }
1475
+ }, [files, autoUpload, uploadFiles]);
1476
+ React2.useEffect(() => {
1477
+ var _a;
1478
+ if (mode !== "image") {
1479
+ setPreviewUrl(null);
1480
+ return;
1481
+ }
1482
+ const file = (_a = files[0]) == null ? void 0 : _a.file;
1483
+ if (!file || !file.type.startsWith("image/")) {
1484
+ setPreviewUrl(null);
1485
+ return;
1486
+ }
1487
+ const url = URL.createObjectURL(file);
1488
+ setPreviewUrl(url);
1489
+ return () => {
1490
+ URL.revokeObjectURL(url);
1491
+ };
1492
+ }, [files, mode]);
1493
+ const triggerProps = {
1494
+ inputId,
1495
+ variant,
1496
+ size,
1497
+ shape,
1498
+ className: cn(isDisabled && "cursor-not-allowed opacity-80"),
1499
+ status,
1500
+ progress,
1501
+ onCancel: handleCancel,
1502
+ onReset: handleReset
1503
+ };
1504
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1505
+ mode === "button" && /* @__PURE__ */ jsxRuntime.jsx(ButtonUpload, { ...triggerProps }),
1506
+ mode === "image" && /* @__PURE__ */ jsxRuntime.jsx(ImageUpload, { ...triggerProps, previewUrl }),
1507
+ mode === "dragger" && /* @__PURE__ */ jsxRuntime.jsx(
1508
+ DraggerUpload,
1509
+ {
1510
+ ...triggerProps,
1511
+ isDragging,
1512
+ onDragOver: handleDragOver,
1513
+ onDragLeave: handleDragLeave,
1514
+ onDrop: handleDrop
1515
+ }
1516
+ ),
1517
+ /* @__PURE__ */ jsxRuntime.jsx(
1518
+ "input",
1519
+ {
1520
+ className: "opacity-0 absolute",
1521
+ disabled: isDisabled,
1522
+ id: inputId,
1523
+ ref: inputRef,
1524
+ type: "file",
1525
+ multiple: maxCount !== 1,
1526
+ accept,
1527
+ webkitdirectory: directory ? "" : void 0,
1528
+ onChange: handleInputChange
1529
+ }
1530
+ ),
1531
+ /* @__PURE__ */ jsxRuntime.jsx(
1532
+ FileList,
1533
+ {
1534
+ files,
1535
+ show: showFileList,
1536
+ onRemove: handleRemoveFile,
1537
+ onCancel: handleCancelFile
1538
+ }
1539
+ )
1540
+ ] });
1541
+ }
1542
+ var Upload_default = Upload;
1543
+
1544
+ exports.Avatar = Avatar;
1545
+ exports.Bubble = Bubble;
1546
+ exports.Button = Button;
1547
+ exports.PlayBoard = playBoard_default;
1548
+ exports.Upload = Upload_default;
1549
+ //# sourceMappingURL=index.cjs.map
1550
+ //# sourceMappingURL=index.cjs.map