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