@particle-academy/react-fancy 5.36.2 → 5.38.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/autocomplete.cjs +3 -3
  3. package/dist/autocomplete.js +2 -2
  4. package/dist/{chunk-FBXYZC4Z.cjs → chunk-AE53RKWR.cjs} +2 -2
  5. package/dist/{chunk-FBXYZC4Z.cjs.map → chunk-AE53RKWR.cjs.map} +1 -1
  6. package/dist/{chunk-FXP4JLGU.cjs → chunk-AR3JOPGT.cjs} +3 -3
  7. package/dist/{chunk-FXP4JLGU.cjs.map → chunk-AR3JOPGT.cjs.map} +1 -1
  8. package/dist/{chunk-35EN5AYE.js → chunk-CF5KRDN4.js} +6 -3
  9. package/dist/chunk-CF5KRDN4.js.map +1 -0
  10. package/dist/{chunk-6ASZUE4T.js → chunk-FQLL2JI7.js} +3 -3
  11. package/dist/{chunk-6ASZUE4T.js.map → chunk-FQLL2JI7.js.map} +1 -1
  12. package/dist/{chunk-ZH3SPBVB.js → chunk-IECPLOSZ.js} +3 -3
  13. package/dist/{chunk-ZH3SPBVB.js.map → chunk-IECPLOSZ.js.map} +1 -1
  14. package/dist/{chunk-E2SZBIAV.cjs → chunk-IWQ3ZFLZ.cjs} +5 -5
  15. package/dist/{chunk-E2SZBIAV.cjs.map → chunk-IWQ3ZFLZ.cjs.map} +1 -1
  16. package/dist/{chunk-QBNR5IVM.cjs → chunk-MFBUK4RV.cjs} +3 -3
  17. package/dist/{chunk-QBNR5IVM.cjs.map → chunk-MFBUK4RV.cjs.map} +1 -1
  18. package/dist/{chunk-DNV7MUEE.cjs → chunk-QLL7MWAU.cjs} +4 -4
  19. package/dist/{chunk-DNV7MUEE.cjs.map → chunk-QLL7MWAU.cjs.map} +1 -1
  20. package/dist/{chunk-ZDDWAVVZ.js → chunk-RLE5WYY6.js} +2 -2
  21. package/dist/{chunk-ZDDWAVVZ.js.map → chunk-RLE5WYY6.js.map} +1 -1
  22. package/dist/chunk-RYKPYILS.cjs +699 -0
  23. package/dist/chunk-RYKPYILS.cjs.map +1 -0
  24. package/dist/{chunk-O3WEV562.js → chunk-WUV3XDJH.js} +3 -3
  25. package/dist/{chunk-O3WEV562.js.map → chunk-WUV3XDJH.js.map} +1 -1
  26. package/dist/{chunk-ISPCXIJZ.cjs → chunk-X7H75WVP.cjs} +6 -3
  27. package/dist/chunk-X7H75WVP.cjs.map +1 -0
  28. package/dist/chunk-Z4GO6N6N.js +695 -0
  29. package/dist/chunk-Z4GO6N6N.js.map +1 -0
  30. package/dist/{chunk-O2ZHORSC.js → chunk-Z6RVBZQ4.js} +3 -3
  31. package/dist/{chunk-O2ZHORSC.js.map → chunk-Z6RVBZQ4.js.map} +1 -1
  32. package/dist/composer.cjs +9 -2
  33. package/dist/composer.d.cts +1 -1
  34. package/dist/composer.d.ts +1 -1
  35. package/dist/composer.js +4 -1
  36. package/dist/editor.cjs +1 -1
  37. package/dist/editor.js +1 -1
  38. package/dist/{index-C8PKWA_5.d.ts → index-CazpqSsk.d.cts} +103 -4
  39. package/dist/{index-C8PKWA_5.d.cts → index-CazpqSsk.d.ts} +103 -4
  40. package/dist/index.cjs +43 -39
  41. package/dist/index.d.cts +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +8 -8
  44. package/dist/inputs.cjs +3 -3
  45. package/dist/inputs.js +2 -2
  46. package/dist/json-editor.cjs +14 -14
  47. package/dist/json-editor.js +3 -3
  48. package/dist/prompt-input.cjs +3 -3
  49. package/dist/prompt-input.js +2 -2
  50. package/dist/qr-code.cjs +4 -4
  51. package/dist/qr-code.d.cts +38 -16
  52. package/dist/qr-code.d.ts +38 -16
  53. package/dist/qr-code.js +1 -1
  54. package/dist/select.cjs +3 -3
  55. package/dist/select.js +2 -2
  56. package/dist/tooltip.cjs +2 -2
  57. package/dist/tooltip.js +1 -1
  58. package/docs/QrCode.md +68 -29
  59. package/package.json +1 -1
  60. package/dist/chunk-23FTDEGR.cjs +0 -218
  61. package/dist/chunk-23FTDEGR.cjs.map +0 -1
  62. package/dist/chunk-2XAVAPDA.js +0 -215
  63. package/dist/chunk-2XAVAPDA.js.map +0 -1
  64. package/dist/chunk-35EN5AYE.js.map +0 -1
  65. package/dist/chunk-ISPCXIJZ.cjs.map +0 -1
@@ -0,0 +1,699 @@
1
+ 'use strict';
2
+
3
+ var chunkX7H75WVP_cjs = require('./chunk-X7H75WVP.cjs');
4
+ var chunkSNHU2AI4_cjs = require('./chunk-SNHU2AI4.cjs');
5
+ var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
6
+ var react = require('react');
7
+ var reactDom = require('react-dom');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+
10
+ function usePastePills({
11
+ threshold,
12
+ heldPastes: controlledHeldPastes,
13
+ onHeldPastesChange
14
+ } = {}) {
15
+ const [heldPastes, setHeldPastes] = chunkSNHU2AI4_cjs.useControllableState(
16
+ controlledHeldPastes,
17
+ [],
18
+ onHeldPastesChange
19
+ );
20
+ const nextId = react.useRef(0);
21
+ const hold = react.useCallback(
22
+ (text) => {
23
+ let id;
24
+ do {
25
+ nextId.current += 1;
26
+ id = `paste-${nextId.current}`;
27
+ } while (heldPastes.some((p) => p.id === id));
28
+ setHeldPastes((current) => [...current, { id, text }]);
29
+ return id;
30
+ },
31
+ [heldPastes, setHeldPastes]
32
+ );
33
+ const handlePaste = react.useCallback(
34
+ (event) => {
35
+ if (!threshold || threshold <= 0) return false;
36
+ const text = event.clipboardData?.getData("text/plain") ?? "";
37
+ if (text.length < threshold) return false;
38
+ event.preventDefault();
39
+ hold(text);
40
+ return true;
41
+ },
42
+ [threshold, hold]
43
+ );
44
+ const expand = react.useCallback(
45
+ (id) => {
46
+ const found = heldPastes.find((p) => p.id === id);
47
+ if (found === void 0) return void 0;
48
+ setHeldPastes((current) => current.filter((p) => p.id !== id));
49
+ return found.text;
50
+ },
51
+ [heldPastes, setHeldPastes]
52
+ );
53
+ const remove = react.useCallback(
54
+ (id) => setHeldPastes((current) => current.filter((p) => p.id !== id)),
55
+ [setHeldPastes]
56
+ );
57
+ const clear = react.useCallback(() => setHeldPastes([]), [setHeldPastes]);
58
+ return { heldPastes, handlePaste, hold, expand, remove, clear };
59
+ }
60
+
61
+ // src/components/Composer/paste-markers.ts
62
+ var PILL_MARK = "\uFFFC";
63
+ var MARKER = /\uFFFC([^\uFFFC\n]+)\uFFFC/g;
64
+ function pasteMarker(id) {
65
+ return `${PILL_MARK}${id}${PILL_MARK}`;
66
+ }
67
+ function segmentValue(value, known) {
68
+ const out = [];
69
+ let last = 0;
70
+ for (const match of value.matchAll(MARKER)) {
71
+ const id = match[1];
72
+ if (!known.has(id)) continue;
73
+ const start = match.index ?? 0;
74
+ if (start > last) out.push({ type: "text", text: value.slice(last, start) });
75
+ out.push({ type: "pill", id });
76
+ last = start + match[0].length;
77
+ }
78
+ if (last < value.length) out.push({ type: "text", text: value.slice(last) });
79
+ return out;
80
+ }
81
+ function markedIds(value) {
82
+ return Array.from(value.matchAll(MARKER), (m) => m[1]);
83
+ }
84
+ function expandMarkers(value, pastes, images) {
85
+ const text = new Map(pastes.map((p) => [p.id, p.text]));
86
+ const image = new Map(images.map((i) => [i.id, `[${i.label}]`]));
87
+ return value.replace(MARKER, (whole, id) => text.get(id) ?? image.get(id) ?? whole);
88
+ }
89
+ var HOST_ATTR = "data-react-fancy-composer-paste-host";
90
+ function serialize(parent, keep) {
91
+ let out = "";
92
+ const children = Array.from(parent.childNodes);
93
+ children.forEach((node, i) => {
94
+ if (node.nodeType === Node.TEXT_NODE) {
95
+ out += (node.textContent ?? "").replace(/\u00A0/g, " ");
96
+ return;
97
+ }
98
+ if (!(node instanceof HTMLElement)) return;
99
+ const id = node.getAttribute(HOST_ATTR);
100
+ if (id !== null) {
101
+ if (keep(id)) out += pasteMarker(id);
102
+ return;
103
+ }
104
+ if (node.tagName === "BR") {
105
+ if (i !== children.length - 1) out += "\n";
106
+ return;
107
+ }
108
+ if (/^(DIV|P)$/.test(node.tagName) && out.length > 0 && !out.endsWith("\n")) {
109
+ out += "\n";
110
+ }
111
+ out += serialize(node, keep);
112
+ });
113
+ return out;
114
+ }
115
+ function makeHost(id) {
116
+ const host = document.createElement("span");
117
+ host.setAttribute("contenteditable", "false");
118
+ host.setAttribute(HOST_ATTR, id);
119
+ host.className = "inline-block align-baseline";
120
+ return host;
121
+ }
122
+ function ensureTrailingBreak(root, value) {
123
+ const last = root.lastChild;
124
+ const hasBreak = last instanceof HTMLElement && last.tagName === "BR";
125
+ if (value.endsWith("\n") && !hasBreak) root.append(document.createElement("br"));
126
+ if (!value.endsWith("\n") && hasBreak && value.length > 0) last.remove();
127
+ }
128
+ function placeCaretAfter(node) {
129
+ const selection = window.getSelection();
130
+ if (!selection) return;
131
+ const range = document.createRange();
132
+ range.setStartAfter(node);
133
+ range.collapse(true);
134
+ selection.removeAllRanges();
135
+ selection.addRange(range);
136
+ }
137
+ var InlinePasteEditor = react.forwardRef(
138
+ function InlinePasteEditor2({ value, known, onChange, onSubmit, onPaste, onHostsChange, placeholder, disabled, className }, ref) {
139
+ const rootRef = react.useRef(null);
140
+ const lastValue = react.useRef(null);
141
+ const pending = react.useRef(/* @__PURE__ */ new Set());
142
+ const knownRef = react.useRef(known);
143
+ knownRef.current = known;
144
+ const keep = react.useCallback(
145
+ (id) => knownRef.current.has(id) || pending.current.has(id),
146
+ []
147
+ );
148
+ const reportHosts = react.useCallback(() => {
149
+ const root = rootRef.current;
150
+ if (!root) return;
151
+ const hosts = /* @__PURE__ */ new Map();
152
+ root.querySelectorAll(`[${HOST_ATTR}]`).forEach((el) => {
153
+ const id = el.getAttribute(HOST_ATTR);
154
+ if (keep(id)) hosts.set(id, el);
155
+ });
156
+ onHostsChange(hosts);
157
+ }, [keep, onHostsChange]);
158
+ const emit = react.useCallback(() => {
159
+ const root = rootRef.current;
160
+ if (!root) return;
161
+ const next = serialize(root, keep);
162
+ ensureTrailingBreak(root, next);
163
+ reportHosts();
164
+ if (next !== lastValue.current) {
165
+ lastValue.current = next;
166
+ onChange(next);
167
+ }
168
+ }, [keep, onChange, reportHosts]);
169
+ const caret = react.useCallback(() => {
170
+ const root = rootRef.current;
171
+ const selection = window.getSelection();
172
+ if (selection && selection.rangeCount > 0) {
173
+ const range2 = selection.getRangeAt(0);
174
+ if (root.contains(range2.commonAncestorContainer)) return range2;
175
+ }
176
+ const range = document.createRange();
177
+ range.selectNodeContents(root);
178
+ range.collapse(false);
179
+ const last = root.lastChild;
180
+ if (last instanceof HTMLElement && last.tagName === "BR") {
181
+ range.setStartBefore(last);
182
+ range.collapse(true);
183
+ }
184
+ return range;
185
+ }, []);
186
+ const insert = react.useCallback(
187
+ (build) => {
188
+ const root = rootRef.current;
189
+ const range = caret();
190
+ range.deleteContents();
191
+ const head = document.createRange();
192
+ head.selectNodeContents(root);
193
+ head.setEnd(range.startContainer, range.startOffset);
194
+ const tail = document.createRange();
195
+ tail.selectNodeContents(root);
196
+ tail.setStart(range.endContainer, range.endOffset);
197
+ const nodes = build(
198
+ serialize(head.cloneContents(), () => true),
199
+ serialize(tail.cloneContents(), () => true)
200
+ );
201
+ if (nodes.length === 0) return;
202
+ const fragment = document.createDocumentFragment();
203
+ nodes.forEach((n) => fragment.append(n));
204
+ range.insertNode(fragment);
205
+ placeCaretAfter(nodes[nodes.length - 1]);
206
+ emit();
207
+ },
208
+ [caret, emit]
209
+ );
210
+ const insertText = react.useCallback(
211
+ (text) => insert(() => text ? [document.createTextNode(text)] : []),
212
+ [insert]
213
+ );
214
+ react.useLayoutEffect(() => {
215
+ const root = rootRef.current;
216
+ if (!root) return;
217
+ for (const id of pending.current) if (known.has(id)) pending.current.delete(id);
218
+ const wanted = segmentValue(value, known).flatMap((s) => s.type === "pill" ? [s.id] : []).join(PILL_MARK);
219
+ const onScreen = Array.from(root.querySelectorAll(`[${HOST_ATTR}]`)).map((el) => el.getAttribute(HOST_ATTR)).filter((id) => id !== null && keep(id)).join(PILL_MARK);
220
+ if (value === lastValue.current && wanted === onScreen) return;
221
+ root.replaceChildren();
222
+ for (const segment of segmentValue(value, known)) {
223
+ if (segment.type === "text") {
224
+ root.append(document.createTextNode(segment.text));
225
+ } else {
226
+ root.append(makeHost(segment.id));
227
+ }
228
+ }
229
+ ensureTrailingBreak(root, value);
230
+ lastValue.current = value;
231
+ reportHosts();
232
+ }, [value, known, keep, reportHosts]);
233
+ react.useImperativeHandle(
234
+ ref,
235
+ () => ({
236
+ focus() {
237
+ const root = rootRef.current;
238
+ if (!root) return;
239
+ root.focus();
240
+ const range = caret();
241
+ const selection = window.getSelection();
242
+ selection?.removeAllRanges();
243
+ selection?.addRange(range);
244
+ },
245
+ replacePill(id, text) {
246
+ const root = rootRef.current;
247
+ const host = Array.from(
248
+ root?.querySelectorAll(`[${HOST_ATTR}]`) ?? []
249
+ ).find((el) => el.getAttribute(HOST_ATTR) === id);
250
+ if (!root || !host) return;
251
+ const node = document.createTextNode(text);
252
+ host.replaceWith(node);
253
+ root.focus();
254
+ placeCaretAfter(node);
255
+ emit();
256
+ }
257
+ }),
258
+ [caret, emit]
259
+ );
260
+ const handlePaste = (e) => {
261
+ e.preventDefault();
262
+ if (disabled) return;
263
+ const pills = onPaste(e.clipboardData);
264
+ if (pills === null) {
265
+ insertText((e.clipboardData?.getData("text/plain") ?? "").replace(/\r\n?/g, "\n"));
266
+ return;
267
+ }
268
+ for (const { id } of pills) pending.current.add(id);
269
+ insert((before, after) => {
270
+ const nodes = [];
271
+ for (const { id, block } of pills) {
272
+ if (block) {
273
+ const precededByBreak = nodes.length === 0 ? before === "" || before.endsWith("\n") : nodes[nodes.length - 1].textContent === "\n";
274
+ if (!precededByBreak) nodes.push(document.createTextNode("\n"));
275
+ nodes.push(makeHost(id));
276
+ nodes.push(document.createTextNode("\n"));
277
+ } else {
278
+ nodes.push(makeHost(id));
279
+ }
280
+ }
281
+ const last = nodes[nodes.length - 1];
282
+ if (last.textContent === "\n" && after.startsWith("\n")) nodes.pop();
283
+ return nodes;
284
+ });
285
+ };
286
+ const handleKeyDown = (e) => {
287
+ if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
288
+ e.preventDefault();
289
+ if (e.shiftKey) {
290
+ insertText("\n");
291
+ } else {
292
+ onSubmit();
293
+ }
294
+ };
295
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
296
+ value === "" && /* @__PURE__ */ jsxRuntime.jsx(
297
+ "span",
298
+ {
299
+ "aria-hidden": "true",
300
+ className: "pointer-events-none absolute left-4 top-3 text-sm text-zinc-400",
301
+ children: placeholder
302
+ }
303
+ ),
304
+ /* @__PURE__ */ jsxRuntime.jsx(
305
+ "div",
306
+ {
307
+ ref: rootRef,
308
+ "data-react-fancy-composer-input": "",
309
+ role: "textbox",
310
+ "aria-multiline": "true",
311
+ "aria-placeholder": placeholder,
312
+ "aria-disabled": disabled || void 0,
313
+ contentEditable: !disabled,
314
+ suppressContentEditableWarning: true,
315
+ onInput: emit,
316
+ onPaste: handlePaste,
317
+ onKeyDown: handleKeyDown,
318
+ className: chunkZU5Q77OT_cjs.cn(
319
+ "max-h-72 min-h-[4.5rem] w-full overflow-y-auto whitespace-pre-wrap break-words bg-transparent px-4 pt-3 text-sm outline-none",
320
+ className
321
+ )
322
+ }
323
+ )
324
+ ] });
325
+ }
326
+ );
327
+ var PREVIEW_CHARS = 20;
328
+ function withSeparators(n) {
329
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
330
+ }
331
+ function endsOf(text, chars) {
332
+ if (text.length <= chars * 2) return text;
333
+ return `${text.slice(0, chars)} \u2026 ${text.slice(-chars)}`;
334
+ }
335
+ var PILL_BASE = "inline-flex select-none items-center rounded-md px-2 py-0.5 text-xs leading-5 transition-colors";
336
+ var TEXT_PILL = "cursor-pointer bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700";
337
+ var IMAGE_PILL = "bg-rose-100 font-medium text-rose-600 dark:bg-rose-500/20 dark:text-rose-300";
338
+ var PREVIEW_BASE = "max-w-xs whitespace-pre-wrap break-all text-xs";
339
+ function ImageThumb({ file }) {
340
+ const [url, setUrl] = react.useState(null);
341
+ react.useEffect(() => {
342
+ if (typeof URL.createObjectURL !== "function") return;
343
+ const made = URL.createObjectURL(file);
344
+ setUrl(made);
345
+ return () => URL.revokeObjectURL(made);
346
+ }, [file]);
347
+ return url ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: url, alt: "", className: "max-h-40 max-w-[16rem] rounded" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: file.name });
348
+ }
349
+ function InlinePill({
350
+ preview,
351
+ previewChars,
352
+ renderPastePreview,
353
+ classNames,
354
+ disabled,
355
+ onExpand
356
+ }) {
357
+ const id = preview.kind === "text" ? preview.paste.id : preview.image.id;
358
+ const content = /* @__PURE__ */ jsxRuntime.jsx("span", { "data-react-fancy-composer-paste-pill-preview": id, children: renderPastePreview?.(preview) ?? (preview.kind === "text" ? endsOf(preview.paste.text, previewChars) : /* @__PURE__ */ jsxRuntime.jsx(ImageThumb, { file: preview.image.file })) });
359
+ const tooltipClass = chunkZU5Q77OT_cjs.cn(PREVIEW_BASE, classNames?.preview);
360
+ if (preview.kind === "image") {
361
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkX7H75WVP_cjs.Tooltip, { content, className: tooltipClass, children: /* @__PURE__ */ jsxRuntime.jsx(
362
+ "span",
363
+ {
364
+ "data-react-fancy-composer-image-pill": id,
365
+ role: "img",
366
+ "aria-label": preview.image.label,
367
+ className: chunkZU5Q77OT_cjs.cn(PILL_BASE, IMAGE_PILL, classNames?.pill, classNames?.imagePill),
368
+ children: preview.image.label
369
+ }
370
+ ) });
371
+ }
372
+ const count = withSeparators(preview.paste.text.length);
373
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkX7H75WVP_cjs.Tooltip, { content, className: tooltipClass, children: /* @__PURE__ */ jsxRuntime.jsxs(
374
+ "button",
375
+ {
376
+ type: "button",
377
+ "data-react-fancy-composer-paste-pill": id,
378
+ onClick: () => onExpand(id),
379
+ disabled,
380
+ "aria-label": `Insert this pasted text, ${count} characters`,
381
+ className: chunkZU5Q77OT_cjs.cn(PILL_BASE, TEXT_PILL, classNames?.pill, classNames?.textPill),
382
+ children: [
383
+ "Pasted text \xB7 ",
384
+ count,
385
+ " characters"
386
+ ]
387
+ }
388
+ ) });
389
+ }
390
+ var Composer = react.forwardRef(
391
+ function Composer2({
392
+ value: controlledValue,
393
+ defaultValue = "",
394
+ onChange,
395
+ onSubmit,
396
+ placeholder = "Type a message...",
397
+ actions,
398
+ disabled = false,
399
+ className,
400
+ pasteThreshold,
401
+ pastePlacement = "above",
402
+ pasteImages = false,
403
+ previewChars = PREVIEW_CHARS,
404
+ renderPastePreview,
405
+ classNames,
406
+ heldPastes: controlledHeldPastes,
407
+ onHeldPastesChange,
408
+ heldImages: controlledHeldImages,
409
+ onHeldImagesChange
410
+ }, ref) {
411
+ const inline = pastePlacement === "inline";
412
+ const [value, setValue] = chunkSNHU2AI4_cjs.useControllableState(
413
+ controlledValue,
414
+ defaultValue,
415
+ onChange
416
+ );
417
+ const pills = usePastePills({
418
+ threshold: pasteThreshold,
419
+ heldPastes: controlledHeldPastes,
420
+ onHeldPastesChange
421
+ });
422
+ const heldPastes = pills.heldPastes;
423
+ const [heldImages, setHeldImages] = chunkSNHU2AI4_cjs.useControllableState(
424
+ controlledHeldImages,
425
+ [],
426
+ onHeldImagesChange
427
+ );
428
+ const textareaRef = react.useRef(null);
429
+ const editorRef = react.useRef(null);
430
+ const imageCount = react.useRef(0);
431
+ const known = react.useMemo(
432
+ () => /* @__PURE__ */ new Set([...heldPastes.map((p) => p.id), ...heldImages.map((i) => i.id)]),
433
+ [heldPastes, heldImages]
434
+ );
435
+ const handlePaste = react.useCallback(
436
+ (e) => {
437
+ pills.handlePaste(e);
438
+ },
439
+ [pills]
440
+ );
441
+ const holdImage = react.useCallback(
442
+ (file) => {
443
+ let id;
444
+ do {
445
+ imageCount.current += 1;
446
+ id = `image-${imageCount.current}`;
447
+ } while (heldImages.some((i) => i.id === id));
448
+ const image = { id, file, label: `Pasted image ${imageCount.current}` };
449
+ setHeldImages((current) => [...current, image]);
450
+ return id;
451
+ },
452
+ [heldImages, setHeldImages]
453
+ );
454
+ const decidePaste = react.useCallback(
455
+ (data) => {
456
+ const text = data?.getData("text/plain") ?? "";
457
+ if (pasteImages && text === "") {
458
+ const files = Array.from(data?.files ?? []).filter(
459
+ (f) => f.type.startsWith("image/")
460
+ );
461
+ if (files.length > 0) {
462
+ return files.map((file) => ({ id: holdImage(file), block: false }));
463
+ }
464
+ }
465
+ if (pasteThreshold && pasteThreshold > 0 && text.length >= pasteThreshold) {
466
+ return [{ id: pills.hold(text), block: true }];
467
+ }
468
+ return null;
469
+ },
470
+ [pasteImages, pasteThreshold, holdImage, pills]
471
+ );
472
+ const handleInlineChange = react.useCallback(
473
+ (next) => {
474
+ setValue(next);
475
+ const present = new Set(markedIds(next));
476
+ for (const paste of heldPastes) if (!present.has(paste.id)) pills.remove(paste.id);
477
+ if (heldImages.some((i) => !present.has(i.id))) {
478
+ setHeldImages((current) => current.filter((i) => present.has(i.id)));
479
+ }
480
+ },
481
+ [setValue, heldPastes, heldImages, pills, setHeldImages]
482
+ );
483
+ const [hosts, setHosts] = react.useState(/* @__PURE__ */ new Map());
484
+ const handleHostsChange = react.useCallback((next) => {
485
+ setHosts((prev) => {
486
+ if (prev.size === next.size && [...next].every(([id, el]) => prev.get(id) === el)) {
487
+ return prev;
488
+ }
489
+ return next;
490
+ });
491
+ }, []);
492
+ const composeOutgoing = react.useCallback(() => {
493
+ if (inline) return expandMarkers(value, heldPastes, heldImages);
494
+ return [value, ...heldPastes.map((p) => p.text)].filter(Boolean).join("\n\n");
495
+ }, [inline, value, heldPastes, heldImages]);
496
+ const hasContent = value.trim().length > 0 || !inline && heldPastes.length > 0;
497
+ const focusInput = react.useCallback(() => {
498
+ if (inline) editorRef.current?.focus();
499
+ else textareaRef.current?.focus();
500
+ }, [inline]);
501
+ const handleSubmit = react.useCallback(() => {
502
+ if (!hasContent || disabled) return;
503
+ const order = markedIds(value);
504
+ const images = inline ? order.flatMap((id) => heldImages.filter((i) => i.id === id)) : [];
505
+ onSubmit?.(composeOutgoing(), { images });
506
+ setValue("");
507
+ pills.clear();
508
+ setHeldImages([]);
509
+ focusInput();
510
+ }, [
511
+ hasContent,
512
+ disabled,
513
+ value,
514
+ inline,
515
+ heldImages,
516
+ onSubmit,
517
+ composeOutgoing,
518
+ setValue,
519
+ pills,
520
+ setHeldImages,
521
+ focusInput
522
+ ]);
523
+ const handleKeyDown = (e) => {
524
+ if (e.key === "Enter" && !e.shiftKey) {
525
+ e.preventDefault();
526
+ handleSubmit();
527
+ }
528
+ };
529
+ const insertPaste = react.useCallback(
530
+ (paste) => {
531
+ const text = pills.expand(paste.id);
532
+ if (text === void 0) return;
533
+ setValue((current) => current ? `${current}
534
+
535
+ ${text}` : text);
536
+ textareaRef.current?.focus();
537
+ },
538
+ [pills, setValue]
539
+ );
540
+ const expandInline = react.useCallback(
541
+ (id) => {
542
+ const text = pills.expand(id);
543
+ if (text === void 0) return;
544
+ editorRef.current?.replacePill(id, text);
545
+ },
546
+ [pills]
547
+ );
548
+ return /* @__PURE__ */ jsxRuntime.jsxs(
549
+ "div",
550
+ {
551
+ "data-react-fancy-composer": "",
552
+ ref,
553
+ className: chunkZU5Q77OT_cjs.cn(
554
+ "rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900",
555
+ disabled && "opacity-50",
556
+ className
557
+ ),
558
+ children: [
559
+ !inline && heldPastes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
560
+ "div",
561
+ {
562
+ "data-react-fancy-composer-paste-pills": "",
563
+ className: "flex flex-wrap gap-1.5 px-3 pt-3",
564
+ children: heldPastes.map((paste) => (
565
+ // `group` + `group-hover` rather than hover state: the preview is
566
+ // presentation, and a useState for it would be one more thing an
567
+ // agent could desynchronise.
568
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "group relative inline-flex", children: [
569
+ /* @__PURE__ */ jsxRuntime.jsx(
570
+ "button",
571
+ {
572
+ type: "button",
573
+ "data-react-fancy-composer-paste-pill": paste.id,
574
+ onClick: () => insertPaste(paste),
575
+ disabled,
576
+ "aria-label": `Insert this pasted text, ${withSeparators(paste.text.length)} characters`,
577
+ "aria-describedby": `${paste.id}-preview`,
578
+ className: chunkZU5Q77OT_cjs.cn(
579
+ "inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-zinc-50 py-1 pl-2.5 pr-7 text-xs text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700",
580
+ classNames?.pill,
581
+ classNames?.textPill
582
+ ),
583
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
584
+ "Pasted text \xB7 ",
585
+ withSeparators(paste.text.length),
586
+ " characters"
587
+ ] })
588
+ }
589
+ ),
590
+ /* @__PURE__ */ jsxRuntime.jsx(
591
+ "button",
592
+ {
593
+ type: "button",
594
+ "data-react-fancy-composer-paste-pill-remove": paste.id,
595
+ onClick: () => pills.remove(paste.id),
596
+ disabled,
597
+ "aria-label": "Discard this pasted text",
598
+ className: chunkZU5Q77OT_cjs.cn(
599
+ "absolute right-1 top-1/2 -translate-y-1/2 rounded-full px-1 text-xs text-zinc-400 transition-colors hover:text-zinc-700 dark:hover:text-zinc-100",
600
+ classNames?.remove
601
+ ),
602
+ children: "\xD7"
603
+ }
604
+ ),
605
+ /* @__PURE__ */ jsxRuntime.jsx(
606
+ "span",
607
+ {
608
+ id: `${paste.id}-preview`,
609
+ "data-react-fancy-composer-paste-pill-preview": paste.id,
610
+ role: "tooltip",
611
+ className: chunkZU5Q77OT_cjs.cn(
612
+ "pointer-events-none absolute bottom-full left-0 z-10 mb-1 hidden max-w-xs whitespace-pre-wrap break-all rounded-md bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block dark:bg-zinc-100 dark:text-zinc-900",
613
+ classNames?.preview
614
+ ),
615
+ children: renderPastePreview?.({ kind: "text", paste }) ?? endsOf(paste.text, previewChars)
616
+ }
617
+ )
618
+ ] }, paste.id)
619
+ ))
620
+ }
621
+ ),
622
+ inline ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
623
+ /* @__PURE__ */ jsxRuntime.jsx(
624
+ InlinePasteEditor,
625
+ {
626
+ ref: editorRef,
627
+ value,
628
+ known,
629
+ onChange: handleInlineChange,
630
+ onSubmit: handleSubmit,
631
+ onPaste: decidePaste,
632
+ onHostsChange: handleHostsChange,
633
+ placeholder,
634
+ disabled,
635
+ className: classNames?.input
636
+ }
637
+ ),
638
+ Array.from(hosts, ([id, host]) => {
639
+ const paste = heldPastes.find((p) => p.id === id);
640
+ const image = paste ? void 0 : heldImages.find((i) => i.id === id);
641
+ const preview = paste ? { kind: "text", paste } : image ? { kind: "image", image } : null;
642
+ return preview ? reactDom.createPortal(
643
+ /* @__PURE__ */ jsxRuntime.jsx(
644
+ InlinePill,
645
+ {
646
+ preview,
647
+ previewChars,
648
+ renderPastePreview,
649
+ classNames,
650
+ disabled,
651
+ onExpand: expandInline
652
+ }
653
+ ),
654
+ host,
655
+ id
656
+ ) : null;
657
+ })
658
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
659
+ "textarea",
660
+ {
661
+ ref: textareaRef,
662
+ value,
663
+ onChange: (e) => setValue(e.target.value),
664
+ onKeyDown: handleKeyDown,
665
+ onPaste: handlePaste,
666
+ placeholder,
667
+ disabled,
668
+ rows: 3,
669
+ className: chunkZU5Q77OT_cjs.cn(
670
+ "w-full resize-none bg-transparent px-4 pt-3 text-sm outline-none placeholder:text-zinc-400",
671
+ classNames?.input
672
+ )
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-t border-zinc-100 px-3 py-2 dark:border-zinc-800", children: [
676
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: actions }),
677
+ /* @__PURE__ */ jsxRuntime.jsx(
678
+ "button",
679
+ {
680
+ type: "button",
681
+ onClick: handleSubmit,
682
+ disabled: disabled || !hasContent,
683
+ className: "rounded-lg bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100",
684
+ children: "Send"
685
+ }
686
+ )
687
+ ] })
688
+ ]
689
+ }
690
+ );
691
+ }
692
+ );
693
+ Composer.displayName = "Composer";
694
+
695
+ exports.Composer = Composer;
696
+ exports.pasteMarker = pasteMarker;
697
+ exports.usePastePills = usePastePills;
698
+ //# sourceMappingURL=chunk-RYKPYILS.cjs.map
699
+ //# sourceMappingURL=chunk-RYKPYILS.cjs.map