@texion/react 0.0.0-stage → 0.1.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.js ADDED
@@ -0,0 +1,1304 @@
1
+ import {
2
+ HighlightOverlay
3
+ } from "./chunk-T56S7VW3.js";
4
+
5
+ // src/hooks/useTexion.ts
6
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
7
+ import {
8
+ createContent,
9
+ isTextFormat as isTextFormat2,
10
+ isTexionContent,
11
+ createRegistry
12
+ } from "@texion/core";
13
+
14
+ // src/controllers/attachments.ts
15
+ import {
16
+ useEffect,
17
+ useMemo,
18
+ useRef,
19
+ useState,
20
+ useSyncExternalStore
21
+ } from "react";
22
+ import { createController } from "@texion/core";
23
+
24
+ // src/dom/drop.ts
25
+ function isFileDrag(transfer) {
26
+ return Array.from(transfer.types).includes("Files");
27
+ }
28
+ function filesFromDrop(transfer) {
29
+ const items = Array.from(transfer.items ?? []);
30
+ const hasFolders = items.some(
31
+ (item) => item.webkitGetAsEntry?.()?.isDirectory
32
+ );
33
+ const files = items.length ? items.flatMap((item) => {
34
+ if (item.webkitGetAsEntry?.()?.isDirectory) {
35
+ return [];
36
+ }
37
+ const file = item.getAsFile();
38
+ return file ? [file] : [];
39
+ }) : Array.from(transfer.files);
40
+ return { files, hasFolders };
41
+ }
42
+
43
+ // src/dom/paste.ts
44
+ import { detectMarkdown } from "@texion/core";
45
+ function readClipboard(clipboard, threshold) {
46
+ const files = Array.from(clipboard.files ?? []);
47
+ if (files.length) {
48
+ return { files };
49
+ }
50
+ if (threshold === void 0) {
51
+ return { files };
52
+ }
53
+ const text = clipboard.getData("text/plain");
54
+ return threshold !== void 0 && text.length >= threshold && detectMarkdown(text) ? { files, attachmentText: text } : { files };
55
+ }
56
+
57
+ // src/dom/picker.ts
58
+ var markdownAccept = ".md,.markdown,.mdown,.mkd,text/markdown,text/x-markdown";
59
+ var accepted = {
60
+ markdown: markdownAccept,
61
+ text: ".txt,text/plain",
62
+ pdf: ".pdf,application/pdf",
63
+ docx: ".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
64
+ json: ".json,application/json,text/json",
65
+ csv: ".csv,text/csv,application/csv",
66
+ image: ".png,.jpg,.jpeg,.gif,.webp,image/png,image/jpeg,image/gif,image/webp"
67
+ };
68
+ function acceptForFormats(formats = Object.keys(accepted), adapters) {
69
+ if (adapters)
70
+ return adapters.filter((adapter) => formats.includes(adapter.format)).flatMap((adapter) => [
71
+ ...adapter.extensions.map((extension) => `.${extension}`),
72
+ ...adapter.mimeTypes
73
+ ]).join(",");
74
+ return formats.flatMap((format) => accepted[format] ?? []).join(",");
75
+ }
76
+ function takeFiles(input) {
77
+ const files = Array.from(input.files ?? []);
78
+ input.value = "";
79
+ return files;
80
+ }
81
+
82
+ // src/controllers/presentation.ts
83
+ var defaultPasteThreshold = 1e4;
84
+
85
+ // src/controllers/attachments.ts
86
+ function useAttachmentController(options) {
87
+ const [controller] = useState(() => createController(options));
88
+ const snapshot = useSyncExternalStore(
89
+ controller.subscribe,
90
+ controller.getSnapshot,
91
+ controller.getSnapshot
92
+ );
93
+ const documents = useMemo(
94
+ () => Object.freeze(
95
+ snapshot.documents.filter((document) => document.id !== "text")
96
+ ),
97
+ [snapshot]
98
+ );
99
+ const inputRef = useRef(null);
100
+ const dragDepth = useRef(0);
101
+ const [isDragging, setDragging] = useState(false);
102
+ const [inputError, setInputError] = useState();
103
+ const initialAttachments = useRef([...options.initialAttachments ?? []]);
104
+ useEffect(() => {
105
+ initialAttachments.current.forEach(addDocument);
106
+ return () => controller.clear();
107
+ }, [controller]);
108
+ const enabled = options.attachments !== false;
109
+ function addDocument(input) {
110
+ return controller.add(
111
+ typeof input === "string" ? { text: input, source: "paste", name: "Pasted Markdown" } : input
112
+ );
113
+ }
114
+ function getRootProps(props = {}) {
115
+ return {
116
+ ...props,
117
+ "data-dragging": isDragging || void 0,
118
+ onDragEnter(event) {
119
+ props.onDragEnter?.(event);
120
+ if (!event.defaultPrevented && enabled && isFileDrag(event.dataTransfer)) {
121
+ event.preventDefault();
122
+ dragDepth.current++;
123
+ setDragging(true);
124
+ }
125
+ },
126
+ onDragOver(event) {
127
+ props.onDragOver?.(event);
128
+ if (!event.defaultPrevented && enabled && isFileDrag(event.dataTransfer)) {
129
+ event.preventDefault();
130
+ }
131
+ },
132
+ onDragLeave(event) {
133
+ props.onDragLeave?.(event);
134
+ if (enabled && isFileDrag(event.dataTransfer)) {
135
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
136
+ if (!dragDepth.current) {
137
+ setDragging(false);
138
+ }
139
+ }
140
+ },
141
+ onDrop(event) {
142
+ props.onDrop?.(event);
143
+ dragDepth.current = 0;
144
+ setDragging(false);
145
+ if (event.defaultPrevented || !enabled || !isFileDrag(event.dataTransfer)) {
146
+ return;
147
+ }
148
+ event.preventDefault();
149
+ const result = filesFromDrop(event.dataTransfer);
150
+ setInputError(
151
+ result.hasFolders ? "Folders are not supported. Select individual supported files." : void 0
152
+ );
153
+ result.files.forEach(addDocument);
154
+ }
155
+ };
156
+ }
157
+ function getInputProps(props = {}) {
158
+ return {
159
+ ...props,
160
+ ref: inputRef,
161
+ type: "file",
162
+ multiple: true,
163
+ accept: props.accept ?? acceptForFormats(
164
+ options.acceptedFormats ?? options.adapters?.map((adapter) => adapter.format),
165
+ options.adapters
166
+ ),
167
+ disabled: !enabled || props.disabled,
168
+ onChange(event) {
169
+ props.onChange?.(event);
170
+ const files = takeFiles(event.currentTarget);
171
+ if (enabled && !event.defaultPrevented) {
172
+ files.forEach(addDocument);
173
+ }
174
+ }
175
+ };
176
+ }
177
+ function handlePaste(event) {
178
+ if (event.defaultPrevented || !enabled) {
179
+ return;
180
+ }
181
+ const clipboard = readClipboard(
182
+ event.clipboardData,
183
+ options.pasteAsAttachment ? options.pasteAttachmentThreshold ?? defaultPasteThreshold : void 0
184
+ );
185
+ if (clipboard.files.length || clipboard.attachmentText !== void 0) {
186
+ event.preventDefault();
187
+ clipboard.files.forEach(addDocument);
188
+ if (clipboard.attachmentText !== void 0) {
189
+ addDocument(clipboard.attachmentText);
190
+ }
191
+ }
192
+ }
193
+ return {
194
+ controller,
195
+ snapshot,
196
+ documents,
197
+ isDragging,
198
+ inputError,
199
+ addDocument,
200
+ getRootProps,
201
+ getInputProps,
202
+ handlePaste,
203
+ openPicker: () => inputRef.current?.click()
204
+ };
205
+ }
206
+
207
+ // src/hooks/useSourceInput.ts
208
+ import { useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
209
+ import {
210
+ createHighlightModel,
211
+ defaultHighlightLimits,
212
+ sourceWarnings,
213
+ detectTextFormat,
214
+ isTextFormat
215
+ } from "@texion/core";
216
+
217
+ // src/controllers/text.ts
218
+ function shouldSubmit(event, composing) {
219
+ return event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey && !composing && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229;
220
+ }
221
+
222
+ // src/hooks/useSourceInput.ts
223
+ var nativeValue = (value) => value.replace(/\r\n?/g, "\n");
224
+ function useSourceInput(options = {}) {
225
+ const [internalValue, setInternalValue] = useState2(
226
+ nativeValue(options.defaultValue ?? "")
227
+ );
228
+ const value = nativeValue(options.value ?? internalValue);
229
+ const valueRef = useRef2(value);
230
+ valueRef.current = value;
231
+ const composing = useRef2(false);
232
+ const [isComposing, setComposing] = useState2(false);
233
+ const [selection, setSelection] = useState2({
234
+ start: 0,
235
+ end: 0,
236
+ direction: "none"
237
+ });
238
+ const maxCharacters = options.maxHighlightCharacters ?? defaultHighlightLimits.maxCharacters;
239
+ const highlight = options.highlight !== false && !isComposing;
240
+ const language = options.language ?? "markdown";
241
+ const getFormat = () => language === "auto" ? detectTextFormat(
242
+ valueRef.current,
243
+ options.allowedFormats,
244
+ options.adapters ?? []
245
+ ) : language;
246
+ const format = getFormat();
247
+ const warnings = useMemo2(
248
+ () => isComposing ? [] : sourceWarnings(value, format, maxCharacters),
249
+ [value, format, maxCharacters, isComposing]
250
+ );
251
+ const highlightModel = useMemo2(
252
+ () => createHighlightModel(value, {
253
+ enabled: highlight,
254
+ maxCharacters,
255
+ format: isTextFormat(format) ? format : "text"
256
+ }),
257
+ [value, highlight, maxCharacters, format]
258
+ );
259
+ function setValue(next) {
260
+ next = nativeValue(next);
261
+ if (next === valueRef.current) {
262
+ return;
263
+ }
264
+ valueRef.current = next;
265
+ if (options.value === void 0) {
266
+ setInternalValue(next);
267
+ }
268
+ options.onChange?.(next);
269
+ }
270
+ function readSelection(element) {
271
+ const next = {
272
+ start: element.selectionStart,
273
+ end: element.selectionEnd,
274
+ direction: element.selectionDirection
275
+ };
276
+ setSelection(
277
+ (previous) => previous.start === next.start && previous.end === next.end && previous.direction === next.direction ? previous : next
278
+ );
279
+ }
280
+ function getTextareaProps(props = {}) {
281
+ return {
282
+ ...props,
283
+ value,
284
+ onChange(event) {
285
+ props.onChange?.(event);
286
+ if (!event.defaultPrevented) {
287
+ setValue(event.currentTarget.value);
288
+ }
289
+ readSelection(event.currentTarget);
290
+ },
291
+ onSelect(event) {
292
+ props.onSelect?.(event);
293
+ readSelection(event.currentTarget);
294
+ },
295
+ onCompositionStart(event) {
296
+ composing.current = true;
297
+ setComposing(true);
298
+ props.onCompositionStart?.(event);
299
+ },
300
+ onCompositionEnd(event) {
301
+ composing.current = false;
302
+ setComposing(false);
303
+ if (event.currentTarget.value !== valueRef.current) {
304
+ setValue(event.currentTarget.value);
305
+ }
306
+ props.onCompositionEnd?.(event);
307
+ },
308
+ onKeyDown(event) {
309
+ props.onKeyDown?.(event);
310
+ if (!event.defaultPrevented && options.onSubmit && options.submitOnEnter && shouldSubmit(event, composing.current)) {
311
+ event.preventDefault();
312
+ options.onSubmit();
313
+ }
314
+ }
315
+ };
316
+ }
317
+ return {
318
+ value,
319
+ format,
320
+ getFormat,
321
+ warnings,
322
+ setValue,
323
+ selection,
324
+ isComposing,
325
+ isComposingNow: () => composing.current,
326
+ getValue: () => valueRef.current,
327
+ highlightModel,
328
+ getTextareaProps
329
+ };
330
+ }
331
+
332
+ // src/hooks/useTexion.ts
333
+ function useTexion(options = {}) {
334
+ const [adapters] = useState3(() => createRegistry(options.adapters));
335
+ if (options.formats && (!options.formats.length || options.formats.some(
336
+ (format) => !adapters.some((adapter) => adapter.format === format)
337
+ ))) {
338
+ throw new RangeError("Choose at least one registered format.");
339
+ }
340
+ if (options.defaultContent !== void 0 && !isTexionContent(options.defaultContent)) {
341
+ throw new TypeError("Invalid initial Texion content.");
342
+ }
343
+ const attachments = useAttachmentController(
344
+ options.formats && !options.acceptedFormats ? { ...options, acceptedFormats: options.formats } : options
345
+ );
346
+ const { controller, snapshot } = attachments;
347
+ const submitting = useRef3(false);
348
+ const richComposing = useRef3(false);
349
+ const originalSource = useRef3(
350
+ options.defaultContent ? options.defaultContent.originalSource : options.defaultValue
351
+ );
352
+ const [isSubmitting, setSubmitting] = useState3(false);
353
+ const [submitError, setSubmitError] = useState3();
354
+ const preferredFormats = options.formats ?? [
355
+ "markdown",
356
+ ...adapters.map((adapter) => adapter.format)
357
+ ];
358
+ const allowedSourceFormats = [...new Set(preferredFormats)].filter(
359
+ (format) => adapters.some(
360
+ (adapter) => adapter.kind !== "binary" && adapter.format === format
361
+ )
362
+ );
363
+ const allowedTextFormats = allowedSourceFormats.filter(isTextFormat2);
364
+ const defaultValue = options.defaultContent?.source ?? options.defaultValue;
365
+ const input = useSourceInput({
366
+ language: options.format ?? options.defaultContent?.format ?? "auto",
367
+ allowedFormats: allowedSourceFormats,
368
+ adapters,
369
+ ...defaultValue === void 0 ? {} : { defaultValue },
370
+ highlight: options.highlight ?? false,
371
+ ...options.maxHighlightCharacters === void 0 ? {} : { maxHighlightCharacters: options.maxHighlightCharacters },
372
+ onChange: () => controller.remove("text"),
373
+ submitOnEnter: options.submitOnEnter !== false,
374
+ onSubmit: () => {
375
+ void submit();
376
+ }
377
+ });
378
+ useEffect2(() => {
379
+ const document = controller.getSnapshot().documents.find((candidate) => candidate.id === "text");
380
+ if (document && document.format !== input.format) {
381
+ controller.remove("text");
382
+ }
383
+ }, [controller, input.format]);
384
+ const textDocument = snapshot.documents.find(
385
+ (document) => document.id === "text"
386
+ );
387
+ async function submit() {
388
+ if (input.isComposingNow() || richComposing.current || submitting.current) {
389
+ return false;
390
+ }
391
+ const text = input.getValue();
392
+ const format = input.getFormat();
393
+ if (!text.trim() && !attachments.documents.length) {
394
+ return false;
395
+ }
396
+ submitting.current = true;
397
+ setSubmitting(true);
398
+ setSubmitError(void 0);
399
+ try {
400
+ controller.setText(text.trim() ? text : "", format);
401
+ const captured = controller.getSnapshot().documents;
402
+ const settled = await Promise.all(
403
+ captured.map((document) => controller.whenSettled(document.id))
404
+ );
405
+ const current = controller.getSnapshot().documents;
406
+ const textChanged = input.getValue() !== text || input.getFormat() !== format;
407
+ const hasUnreadyDocuments = settled.some(
408
+ (document) => !document || document.status !== "ready"
409
+ );
410
+ const documentsChanged = settled.length !== current.length || settled.some((document, index) => document !== current[index]);
411
+ if (textChanged || hasUnreadyDocuments || documentsChanged) {
412
+ return false;
413
+ }
414
+ const primary = current.find((document) => document.id === "text");
415
+ const documents = Object.freeze(
416
+ current.filter((document) => document.id !== "text")
417
+ );
418
+ await options.onSubmit?.({
419
+ text,
420
+ documents,
421
+ attachments: documents,
422
+ content: createContent(
423
+ text,
424
+ originalSource.current === void 0 ? { format } : { format, originalSource: originalSource.current }
425
+ ),
426
+ ...primary ? { textDocument: primary } : {}
427
+ });
428
+ return true;
429
+ } catch (error) {
430
+ setSubmitError(
431
+ error instanceof Error ? error.message : "Submission failed."
432
+ );
433
+ return false;
434
+ } finally {
435
+ submitting.current = false;
436
+ setSubmitting(false);
437
+ }
438
+ }
439
+ function getTextareaProps(props = {}) {
440
+ return input.getTextareaProps({
441
+ ...props,
442
+ onPaste(event) {
443
+ props.onPaste?.(event);
444
+ if (!input.isComposingNow()) {
445
+ attachments.handlePaste(event);
446
+ }
447
+ }
448
+ });
449
+ }
450
+ function getRichEditorProps(props = {}) {
451
+ if (input.format !== "markdown") {
452
+ throw new Error(
453
+ "Plain text, JSON and CSV use source editing; only Markdown supports rich conversion."
454
+ );
455
+ }
456
+ return {
457
+ formats: options.format && options.format !== "auto" ? [options.format].filter(isTextFormat2) : allowedTextFormats ?? ["markdown", "json", "csv"],
458
+ ...props,
459
+ value: input.value,
460
+ onChange(source) {
461
+ props.onChange?.(source);
462
+ input.setValue(source);
463
+ },
464
+ onImport(source) {
465
+ originalSource.current ??= source;
466
+ props.onImport?.(source);
467
+ },
468
+ onCompositionChange(composing) {
469
+ richComposing.current = composing;
470
+ props.onCompositionChange?.(composing);
471
+ },
472
+ onSubmit() {
473
+ props.onSubmit?.();
474
+ void submit();
475
+ },
476
+ submitOnEnter: options.submitOnEnter ?? false,
477
+ ...options.attachments === false ? {} : {
478
+ onFiles(files) {
479
+ files.forEach(attachments.addDocument);
480
+ props.onFiles?.(files);
481
+ }
482
+ }
483
+ };
484
+ }
485
+ return {
486
+ content: createContent(
487
+ input.value,
488
+ originalSource.current === void 0 ? { format: input.format } : { format: input.format, originalSource: originalSource.current }
489
+ ),
490
+ format: input.format,
491
+ warnings: input.warnings,
492
+ getRichEditorProps,
493
+ text: input.value,
494
+ textDocument,
495
+ selection: input.selection,
496
+ isComposing: input.isComposing,
497
+ highlightModel: input.highlightModel,
498
+ documents: attachments.documents,
499
+ isDragging: attachments.isDragging,
500
+ isSubmitting,
501
+ inputError: attachments.inputError,
502
+ submitError,
503
+ setText: input.setValue,
504
+ addDocument: attachments.addDocument,
505
+ removeDocument: controller.remove,
506
+ cancelDocument: controller.cancel,
507
+ clear() {
508
+ input.setValue("");
509
+ originalSource.current = void 0;
510
+ richComposing.current = false;
511
+ controller.clear();
512
+ },
513
+ submit,
514
+ openPicker: attachments.openPicker,
515
+ getRootProps: attachments.getRootProps,
516
+ getTextareaProps,
517
+ getInputProps: attachments.getInputProps
518
+ };
519
+ }
520
+
521
+ // src/hooks/useDocument.ts
522
+ import { useEffect as useEffect3, useState as useState4, useSyncExternalStore as useSyncExternalStore2 } from "react";
523
+ import { createController as createController2 } from "@texion/core";
524
+ function useDocument(input, options = {}) {
525
+ const [controller] = useState4(() => createController2(options));
526
+ const [current, setCurrent] = useState4();
527
+ const snapshot = useSyncExternalStore2(
528
+ controller.subscribe,
529
+ controller.getSnapshot,
530
+ controller.getSnapshot
531
+ );
532
+ useEffect3(() => {
533
+ if (input === void 0) {
534
+ setCurrent(void 0);
535
+ return;
536
+ }
537
+ const id = controller.add(input);
538
+ setCurrent({ input, id });
539
+ return () => {
540
+ controller.remove(id);
541
+ };
542
+ }, [controller, input]);
543
+ return current?.input === input ? snapshot.documents.find((document) => document.id === current?.id) : void 0;
544
+ }
545
+
546
+ // src/components/TexionInput.tsx
547
+ import { isTextFormat as isTextFormat3 } from "@texion/core";
548
+ import { Suspense as Suspense2, lazy as lazy2, useEffect as useEffect5, useId, useState as useState6 } from "react";
549
+
550
+ // src/components/AttachmentList.tsx
551
+ import { jsx, jsxs } from "react/jsx-runtime";
552
+ function AttachmentList({
553
+ documents,
554
+ onRemove,
555
+ onCancel,
556
+ onPreview
557
+ }) {
558
+ if (!documents.length) {
559
+ return null;
560
+ }
561
+ return /* @__PURE__ */ jsx("ul", { className: "texion-attachments", "aria-label": "Attachments", children: documents.map((document) => /* @__PURE__ */ jsxs("li", { children: [
562
+ /* @__PURE__ */ jsx("span", { children: document.name }),
563
+ " ",
564
+ /* @__PURE__ */ jsx("span", { role: "status", children: document.status }),
565
+ document.status === "processing" && document.progress && /* @__PURE__ */ jsxs("span", { className: "texion-document-progress", role: "status", children: [
566
+ document.progress.phase === "pending-consent" ? "Waiting for OCR approval" : document.progress.phase === "initializing-ocr" ? "Loading OCR engine" : document.progress.phase === "ocr" ? `Recognizing page ${document.progress.pageNumber ?? document.progress.done + 1} \xB7 ${document.progress.done}/${document.progress.total} complete` : `Extracting page ${document.progress.done} of ${document.progress.total}`,
567
+ /* @__PURE__ */ jsx(
568
+ "progress",
569
+ {
570
+ "aria-label": `Processing ${document.name}`,
571
+ value: document.progress.done,
572
+ max: Math.max(1, document.progress.total)
573
+ }
574
+ )
575
+ ] }),
576
+ document.error && /* @__PURE__ */ jsx("span", { role: "alert", children: document.error.message }),
577
+ document.warnings.map((warning, i) => /* @__PURE__ */ jsx("span", { role: "status", children: warning.message }, i)),
578
+ onPreview && document.status === "ready" && /* @__PURE__ */ jsx(
579
+ "button",
580
+ {
581
+ type: "button",
582
+ onClick: () => onPreview(document.id),
583
+ "aria-label": `Preview ${document.name}`,
584
+ children: "Preview"
585
+ }
586
+ ),
587
+ onCancel && (document.status === "pending" || document.status === "processing") && /* @__PURE__ */ jsx(
588
+ "button",
589
+ {
590
+ type: "button",
591
+ onClick: () => onCancel(document.id),
592
+ "aria-label": `Cancel ${document.name}`,
593
+ children: "Cancel"
594
+ }
595
+ ),
596
+ /* @__PURE__ */ jsx(
597
+ "button",
598
+ {
599
+ type: "button",
600
+ onClick: () => onRemove(document.id),
601
+ "aria-label": `Remove ${document.name}`,
602
+ children: "Remove"
603
+ }
604
+ )
605
+ ] }, document.id)) });
606
+ }
607
+
608
+ // src/components/DocumentViewer.tsx
609
+ import { Suspense, useMemo as useMemo3 } from "react";
610
+
611
+ // src/renderers/registry.ts
612
+ import { lazy } from "react";
613
+ var DefaultMarkdownRenderer = lazy(() => import("./markdown-QTRYMUXO.js"));
614
+ var JsonRenderer = lazy(() => import("./json-BEWG6XTV.js"));
615
+ var CsvRenderer = lazy(() => import("./csv-COGONMFA.js"));
616
+ var ImageRenderer = lazy(() => import("./image-TKOV52M6.js"));
617
+ function defaultRenderer(format) {
618
+ switch (format) {
619
+ case "json":
620
+ return JsonRenderer;
621
+ case "csv":
622
+ return CsvRenderer;
623
+ case "image":
624
+ return ImageRenderer;
625
+ default:
626
+ return DefaultMarkdownRenderer;
627
+ }
628
+ }
629
+
630
+ // src/components/DocumentViewer.tsx
631
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
632
+ function DocumentViewer({
633
+ document: supplied,
634
+ input,
635
+ content,
636
+ format,
637
+ options,
638
+ className,
639
+ renderer,
640
+ renderers
641
+ }) {
642
+ const documentInput = useMemo3(() => {
643
+ const value = input ?? content;
644
+ if (typeof value === "string" && format) {
645
+ return { text: value, format };
646
+ }
647
+ if (value && typeof value === "object" && "text" in value && typeof value.text === "string" && format) {
648
+ return { ...value, format };
649
+ }
650
+ return value;
651
+ }, [input, content, format]);
652
+ const processed = useDocument(supplied ? void 0 : documentInput, options);
653
+ const document = supplied ?? processed;
654
+ const Renderer = renderer ?? (document ? renderers?.[document.format] : void 0) ?? defaultRenderer(document?.format);
655
+ const preview = useMemo3(
656
+ () => document?.status === "ready" ? document.getPreview?.() : void 0,
657
+ [document]
658
+ );
659
+ if (document?.status === "error") {
660
+ return /* @__PURE__ */ jsx2("p", { role: "alert", children: document.error?.message });
661
+ }
662
+ if (!document || document.status !== "ready") {
663
+ return /* @__PURE__ */ jsx2("p", { role: "status", children: "Preparing document\u2026" });
664
+ }
665
+ return /* @__PURE__ */ jsxs2(
666
+ "section",
667
+ {
668
+ className: className ?? "texion-viewer",
669
+ "aria-label": `Preview of ${document.name}`,
670
+ children: [
671
+ document.warnings.map((warning, i) => /* @__PURE__ */ jsx2("p", { role: "status", children: warning.message }, i)),
672
+ preview?.warnings.filter(
673
+ (warning) => !document.warnings.some(
674
+ (existing) => existing.code === warning.code && existing.message === warning.message
675
+ )
676
+ ).map((warning, i) => /* @__PURE__ */ jsx2("p", { role: "status", children: warning.message }, i)),
677
+ preview && /* @__PURE__ */ jsx2(Suspense, { fallback: /* @__PURE__ */ jsx2("p", { role: "status", children: "Loading preview\u2026" }), children: /* @__PURE__ */ jsx2(Renderer, { preview, document }) })
678
+ ]
679
+ }
680
+ );
681
+ }
682
+
683
+ // src/components/HighlightedTextarea.tsx
684
+ import {
685
+ forwardRef,
686
+ useCallback,
687
+ useEffect as useEffect4,
688
+ useLayoutEffect,
689
+ useRef as useRef4,
690
+ useState as useState5
691
+ } from "react";
692
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
693
+ var useClientLayoutEffect = typeof window === "undefined" ? useEffect4 : useLayoutEffect;
694
+ var mirrorProperties = [
695
+ "fontFamily",
696
+ "fontSize",
697
+ "fontWeight",
698
+ "fontStyle",
699
+ "fontVariant",
700
+ "fontStretch",
701
+ "fontKerning",
702
+ "fontFeatureSettings",
703
+ "fontVariationSettings",
704
+ "fontVariantLigatures",
705
+ "lineHeight",
706
+ "letterSpacing",
707
+ "wordSpacing",
708
+ "textAlign",
709
+ "textIndent",
710
+ "textTransform",
711
+ "textRendering",
712
+ "tabSize",
713
+ "direction",
714
+ "unicodeBidi",
715
+ "writingMode",
716
+ "paddingTop",
717
+ "paddingRight",
718
+ "paddingBottom",
719
+ "paddingLeft",
720
+ "color",
721
+ "wordBreak",
722
+ "overflowWrap",
723
+ "lineBreak"
724
+ ];
725
+ var HighlightedTextareaSurface = forwardRef(function HighlightedTextareaSurface2({ textareaProps, model, wrapperClassName }, forwardedRef) {
726
+ const textareaRef = useRef4(null);
727
+ const viewportRef = useRef4(null);
728
+ const [layout, setLayout] = useState5();
729
+ const [forcedColors, setForcedColors] = useState5(false);
730
+ const value = String(textareaProps.value ?? "");
731
+ const requested = model.enabled && !!value && model.text === value && !forcedColors;
732
+ const syncScroll = useCallback(() => {
733
+ const textarea = textareaRef.current;
734
+ if (textarea && viewportRef.current) {
735
+ viewportRef.current.scrollTop = textarea.scrollTop;
736
+ viewportRef.current.scrollLeft = textarea.scrollLeft;
737
+ }
738
+ }, []);
739
+ const synchronize = useCallback(() => {
740
+ const textarea = textareaRef.current;
741
+ const view = textarea?.ownerDocument.defaultView;
742
+ if (!textarea || !view) {
743
+ return;
744
+ }
745
+ const computed = view.getComputedStyle(textarea);
746
+ const content = {
747
+ ...Object.fromEntries(
748
+ mirrorProperties.map((property) => [property, computed[property]])
749
+ ),
750
+ boxSizing: "border-box",
751
+ margin: 0,
752
+ border: 0,
753
+ width: textarea.clientWidth,
754
+ minHeight: textarea.clientHeight,
755
+ whiteSpace: textarea.wrap === "off" ? "pre" : "pre-wrap",
756
+ WebkitTextFillColor: "currentColor"
757
+ };
758
+ const next = {
759
+ left: textarea.offsetLeft + textarea.clientLeft,
760
+ top: textarea.offsetTop + textarea.clientTop,
761
+ width: textarea.clientWidth,
762
+ height: textarea.clientHeight,
763
+ content
764
+ };
765
+ setLayout(
766
+ (previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next
767
+ );
768
+ syncScroll();
769
+ }, [syncScroll]);
770
+ useClientLayoutEffect(() => {
771
+ if (requested) {
772
+ synchronize();
773
+ }
774
+ });
775
+ useClientLayoutEffect(() => {
776
+ if (!requested) {
777
+ return;
778
+ }
779
+ const textarea = textareaRef.current;
780
+ const view = textarea?.ownerDocument.defaultView;
781
+ if (!textarea || !view) {
782
+ return;
783
+ }
784
+ let mounted = true;
785
+ const refresh = () => {
786
+ if (mounted) {
787
+ synchronize();
788
+ }
789
+ };
790
+ const resize = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(refresh);
791
+ resize?.observe(textarea);
792
+ if (textarea.parentElement) {
793
+ resize?.observe(textarea.parentElement);
794
+ }
795
+ const mutation = new MutationObserver(refresh);
796
+ for (let element = textarea; element; element = element.parentElement) {
797
+ mutation.observe(element, {
798
+ attributes: true,
799
+ attributeFilter: ["style", "class", "dir", "wrap"]
800
+ });
801
+ }
802
+ view.addEventListener("resize", refresh);
803
+ const fonts = textarea.ownerDocument.fonts;
804
+ void fonts?.ready.then(refresh);
805
+ fonts?.addEventListener("loadingdone", refresh);
806
+ return () => {
807
+ mounted = false;
808
+ resize?.disconnect();
809
+ mutation.disconnect();
810
+ view.removeEventListener("resize", refresh);
811
+ fonts?.removeEventListener("loadingdone", refresh);
812
+ };
813
+ }, [requested, synchronize]);
814
+ useEffect4(() => {
815
+ const media = textareaRef.current?.ownerDocument.defaultView?.matchMedia?.(
816
+ "(forced-colors: active)"
817
+ );
818
+ if (!media) {
819
+ return;
820
+ }
821
+ const update = () => setForcedColors(media.matches);
822
+ update();
823
+ media.addEventListener("change", update);
824
+ return () => media.removeEventListener("change", update);
825
+ }, []);
826
+ const active = requested && !!layout;
827
+ return /* @__PURE__ */ jsxs3(
828
+ "div",
829
+ {
830
+ className: ["texion-highlighted-textarea", wrapperClassName].filter(Boolean).join(" "),
831
+ "data-highlighted": active || void 0,
832
+ "data-highlight-reason": model.reason,
833
+ style: { position: "relative", display: "grid", minWidth: 0 },
834
+ children: [
835
+ /* @__PURE__ */ jsx3(
836
+ "textarea",
837
+ {
838
+ ...textareaProps,
839
+ ref: (node) => {
840
+ textareaRef.current = node;
841
+ if (typeof forwardedRef === "function") {
842
+ forwardedRef(node);
843
+ } else if (forwardedRef) {
844
+ forwardedRef.current = node;
845
+ }
846
+ },
847
+ style: {
848
+ display: "block",
849
+ boxSizing: "border-box",
850
+ minWidth: 0,
851
+ width: "100%",
852
+ ...textareaProps.style,
853
+ ...active ? { WebkitTextFillColor: "transparent", caretColor: "currentColor" } : {}
854
+ },
855
+ onScroll: (event) => {
856
+ syncScroll();
857
+ textareaProps.onScroll?.(event);
858
+ }
859
+ }
860
+ ),
861
+ active && /* @__PURE__ */ jsx3(
862
+ "div",
863
+ {
864
+ ref: viewportRef,
865
+ "data-texion-overlay-viewport": "",
866
+ "aria-hidden": "true",
867
+ style: {
868
+ position: "absolute",
869
+ pointerEvents: "none",
870
+ userSelect: "none",
871
+ overflow: "hidden",
872
+ direction: layout.content.direction,
873
+ left: layout.left,
874
+ top: layout.top,
875
+ width: layout.width,
876
+ height: layout.height
877
+ },
878
+ children: /* @__PURE__ */ jsx3(HighlightOverlay, { model, style: layout.content })
879
+ }
880
+ )
881
+ ]
882
+ }
883
+ );
884
+ });
885
+ var HighlightedTextarea = forwardRef(function HighlightedTextarea2({
886
+ value,
887
+ defaultValue,
888
+ onChange,
889
+ language = "markdown",
890
+ highlight = true,
891
+ maxHighlightCharacters,
892
+ wrapperClassName,
893
+ ...props
894
+ }, ref) {
895
+ const input = useSourceInput({
896
+ language,
897
+ highlight,
898
+ ...value === void 0 ? {} : { value },
899
+ ...defaultValue === void 0 ? {} : { defaultValue },
900
+ ...onChange === void 0 ? {} : { onChange },
901
+ ...maxHighlightCharacters === void 0 ? {} : { maxHighlightCharacters }
902
+ });
903
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
904
+ /* @__PURE__ */ jsx3(
905
+ HighlightedTextareaSurface,
906
+ {
907
+ ref,
908
+ textareaProps: input.getTextareaProps(props),
909
+ model: input.highlightModel,
910
+ wrapperClassName
911
+ }
912
+ ),
913
+ input.warnings.map((warning, index) => /* @__PURE__ */ jsx3("p", { role: "status", children: warning.message }, index))
914
+ ] });
915
+ });
916
+
917
+ // src/components/TexionInput.tsx
918
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
919
+ var RichEditor = lazy2(() => import("./RichMarkdownEditor-OAHLD4XC.js"));
920
+ function TexionInput({
921
+ placeholder = "Drop a file, paste, or write\u2026",
922
+ renderers,
923
+ label = "Message",
924
+ preview = false,
925
+ previewImages = false,
926
+ previewPosition = "above",
927
+ className = "texion-input",
928
+ mode = "source",
929
+ live = true,
930
+ modeSwitch = mode === "rich",
931
+ submitLabel = "Submit",
932
+ maxRichCharacters = 5e4,
933
+ ...options
934
+ }) {
935
+ const [position, setPosition] = useState6(previewPosition);
936
+ useEffect5(() => setPosition(previewPosition), [previewPosition]);
937
+ const [editorMode, setEditorMode] = useState6(mode);
938
+ useEffect5(() => setEditorMode(mode), [mode]);
939
+ const texion = useTexion({
940
+ ...options,
941
+ format: options.format ?? options.defaultContent?.format ?? (editorMode === "rich" ? "markdown" : "auto"),
942
+ highlight: editorMode === "source" && !!options.highlight
943
+ });
944
+ const canUseRich = texion.format === "markdown";
945
+ const id = useId();
946
+ const [previewId, setPreviewId] = useState6();
947
+ const [showTextPreview, setShowTextPreview] = useState6(false);
948
+ const [showImagePreviews, setShowImagePreviews] = useState6(previewImages);
949
+ useEffect5(() => setShowImagePreviews(previewImages), [previewImages]);
950
+ const images = texion.documents.filter(
951
+ (document) => document.format === "image" && document.status === "ready"
952
+ );
953
+ const selected = texion.documents.find(
954
+ (document) => document.id === previewId
955
+ );
956
+ const previews = /* @__PURE__ */ jsxs4("div", { className: "texion-input-previews", children: [
957
+ texion.warnings.map((warning, index) => /* @__PURE__ */ jsx4("p", { role: "status", children: warning.message }, index)),
958
+ texion.textDocument?.error && /* @__PURE__ */ jsx4("p", { role: "alert", children: texion.textDocument.error.message }),
959
+ preview && !!texion.text.trim() && /* @__PURE__ */ jsx4(
960
+ "button",
961
+ {
962
+ type: "button",
963
+ "aria-expanded": showTextPreview,
964
+ onClick: () => setShowTextPreview((value) => !value),
965
+ children: showTextPreview ? "Hide message preview" : "Preview message"
966
+ }
967
+ ),
968
+ preview && showTextPreview && !!texion.text.trim() && /* @__PURE__ */ jsx4(
969
+ DocumentViewer,
970
+ {
971
+ format: texion.format,
972
+ content: texion.text,
973
+ options: options.formats && !options.acceptedFormats ? { ...options, acceptedFormats: options.formats } : options,
974
+ ...renderers ? { renderers } : {}
975
+ }
976
+ ),
977
+ texion.isDragging && /* @__PURE__ */ jsx4("p", { role: "status", children: "Drop supported files here" }),
978
+ texion.inputError && /* @__PURE__ */ jsx4("p", { role: "alert", children: texion.inputError }),
979
+ images.length > 0 && /* @__PURE__ */ jsxs4("label", { children: [
980
+ /* @__PURE__ */ jsx4(
981
+ "input",
982
+ {
983
+ type: "checkbox",
984
+ checked: showImagePreviews,
985
+ onChange: (event) => {
986
+ setShowImagePreviews(event.currentTarget.checked);
987
+ setPreviewId(void 0);
988
+ }
989
+ }
990
+ ),
991
+ "Show image previews"
992
+ ] }),
993
+ /* @__PURE__ */ jsx4(
994
+ AttachmentList,
995
+ {
996
+ documents: texion.documents,
997
+ onRemove: texion.removeDocument,
998
+ onCancel: texion.cancelDocument,
999
+ onPreview: preview ? setPreviewId : void 0
1000
+ }
1001
+ ),
1002
+ showImagePreviews && images.map((document) => /* @__PURE__ */ jsx4(
1003
+ DocumentViewer,
1004
+ {
1005
+ document,
1006
+ ...renderers ? { renderers } : {}
1007
+ },
1008
+ document.id
1009
+ )),
1010
+ preview && selected && !(showImagePreviews && selected.format === "image") && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1011
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => setPreviewId(void 0), children: "Close preview" }),
1012
+ /* @__PURE__ */ jsx4(
1013
+ DocumentViewer,
1014
+ {
1015
+ document: selected,
1016
+ ...renderers ? { renderers } : {}
1017
+ }
1018
+ )
1019
+ ] })
1020
+ ] });
1021
+ return /* @__PURE__ */ jsxs4("div", { ...texion.getRootProps({ className }), children: [
1022
+ /* @__PURE__ */ jsxs4("label", { children: [
1023
+ "Preview position",
1024
+ /* @__PURE__ */ jsxs4(
1025
+ "select",
1026
+ {
1027
+ value: position,
1028
+ onChange: (event) => setPosition(event.currentTarget.value),
1029
+ children: [
1030
+ /* @__PURE__ */ jsx4("option", { value: "above", children: "Above input" }),
1031
+ /* @__PURE__ */ jsx4("option", { value: "below", children: "Below input" })
1032
+ ]
1033
+ }
1034
+ )
1035
+ ] }),
1036
+ position === "above" && previews,
1037
+ /* @__PURE__ */ jsx4("label", { htmlFor: id, children: label }),
1038
+ modeSwitch && canUseRich && /* @__PURE__ */ jsx4(
1039
+ "button",
1040
+ {
1041
+ type: "button",
1042
+ onClick: () => setEditorMode((value) => value === "rich" ? "source" : "rich"),
1043
+ children: editorMode === "rich" ? "Edit Markdown source" : "Edit rich content"
1044
+ }
1045
+ ),
1046
+ editorMode === "rich" && canUseRich ? /* @__PURE__ */ jsx4(Suspense2, { fallback: /* @__PURE__ */ jsx4("p", { role: "status", children: "Loading rich editor\u2026" }), children: /* @__PURE__ */ jsx4(
1047
+ RichEditor,
1048
+ {
1049
+ ...texion.getRichEditorProps({
1050
+ formats: options.format && options.format !== "auto" ? [options.format].filter(isTextFormat3) : options.formats?.filter(isTextFormat3) ?? [
1051
+ "markdown",
1052
+ "json",
1053
+ "csv"
1054
+ ],
1055
+ id,
1056
+ label,
1057
+ placeholder,
1058
+ live,
1059
+ maxCharacters: maxRichCharacters,
1060
+ onRequestSource: () => setEditorMode("source")
1061
+ })
1062
+ }
1063
+ ) }) : /* @__PURE__ */ jsx4(
1064
+ HighlightedTextareaSurface,
1065
+ {
1066
+ textareaProps: texion.getTextareaProps({ id, placeholder }),
1067
+ model: texion.highlightModel
1068
+ }
1069
+ ),
1070
+ position === "below" && previews,
1071
+ options.attachments !== false && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1072
+ /* @__PURE__ */ jsx4(
1073
+ "input",
1074
+ {
1075
+ ...texion.getInputProps({
1076
+ hidden: true,
1077
+ "aria-label": "Choose files"
1078
+ })
1079
+ }
1080
+ ),
1081
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: texion.openPicker, children: "Attach files" })
1082
+ ] }),
1083
+ texion.submitError && /* @__PURE__ */ jsx4("p", { role: "alert", children: texion.submitError }),
1084
+ /* @__PURE__ */ jsx4(
1085
+ "button",
1086
+ {
1087
+ type: "button",
1088
+ onClick: () => {
1089
+ void texion.submit();
1090
+ },
1091
+ disabled: texion.isSubmitting,
1092
+ children: texion.isSubmitting ? "Submitting\u2026" : submitLabel
1093
+ }
1094
+ )
1095
+ ] });
1096
+ }
1097
+
1098
+ // src/hooks/useMarkdownInput.ts
1099
+ function useMarkdownInput(options = {}) {
1100
+ return useSourceInput({ ...options, language: "markdown" });
1101
+ }
1102
+
1103
+ // src/components/TexionViewer.tsx
1104
+ import { isTexionContent as isTexionContent2 } from "@texion/core";
1105
+ import { jsx as jsx5 } from "react/jsx-runtime";
1106
+ function TexionViewer({ content, ...props }) {
1107
+ if (!isTexionContent2(content)) {
1108
+ return /* @__PURE__ */ jsx5("p", { role: "alert", children: "Unsupported Texion content." });
1109
+ }
1110
+ return /* @__PURE__ */ jsx5(
1111
+ DocumentViewer,
1112
+ {
1113
+ ...props,
1114
+ content: content.source,
1115
+ format: content.format
1116
+ }
1117
+ );
1118
+ }
1119
+
1120
+ // src/hooks/useTexionAttachments.ts
1121
+ function useTexionAttachments(options = {}) {
1122
+ const attachments = useAttachmentController(options);
1123
+ function getTextareaProps(props = {}) {
1124
+ return {
1125
+ ...props,
1126
+ onPaste(event) {
1127
+ props.onPaste?.(event);
1128
+ attachments.handlePaste(event);
1129
+ }
1130
+ };
1131
+ }
1132
+ return {
1133
+ documents: attachments.documents,
1134
+ isDragging: attachments.isDragging,
1135
+ inputError: attachments.inputError,
1136
+ addDocument: attachments.addDocument,
1137
+ removeDocument: attachments.controller.remove,
1138
+ cancelDocument: attachments.controller.cancel,
1139
+ clear: attachments.controller.clear,
1140
+ whenSettled: attachments.controller.whenSettled,
1141
+ waitForIdle: attachments.controller.waitForIdle,
1142
+ openPicker: attachments.openPicker,
1143
+ getRootProps: attachments.getRootProps,
1144
+ getInputProps: attachments.getInputProps,
1145
+ getTextareaProps,
1146
+ handlePaste: attachments.handlePaste
1147
+ };
1148
+ }
1149
+
1150
+ // src/components/TexionMessageViewer.tsx
1151
+ import { useEffect as useEffect6, useState as useState7 } from "react";
1152
+ import { isTexionMessage } from "@texion/core";
1153
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1154
+ function SavedAttachment({
1155
+ attachment,
1156
+ resolve,
1157
+ options,
1158
+ renderers
1159
+ }) {
1160
+ const [open, setOpen] = useState7(false);
1161
+ const [result, setResult] = useState7();
1162
+ useEffect6(() => {
1163
+ if (!open || !resolve) return;
1164
+ const operation = new AbortController();
1165
+ setResult(void 0);
1166
+ void Promise.resolve().then(() => resolve(attachment, operation.signal)).then((input) => {
1167
+ if (operation.signal.aborted) return;
1168
+ const value = input && typeof input === "object" && "arrayBuffer" in input ? new File([input], attachment.name, {
1169
+ type: attachment.mimeType || input.type
1170
+ }) : input;
1171
+ setResult({
1172
+ attachment,
1173
+ ...value === void 0 ? {} : { input: value }
1174
+ });
1175
+ }).catch((error) => {
1176
+ if (!operation.signal.aborted)
1177
+ setResult({
1178
+ attachment,
1179
+ error: error instanceof Error ? error.message : "The attachment could not be restored."
1180
+ });
1181
+ });
1182
+ return () => operation.abort();
1183
+ }, [open, resolve, attachment]);
1184
+ const current = result?.attachment === attachment ? result : void 0;
1185
+ return /* @__PURE__ */ jsxs5("li", { className: "texion-saved-attachment", children: [
1186
+ /* @__PURE__ */ jsxs5("div", { children: [
1187
+ /* @__PURE__ */ jsx6("strong", { children: attachment.name }),
1188
+ /* @__PURE__ */ jsxs5("span", { children: [
1189
+ attachment.format,
1190
+ " \xB7 ",
1191
+ attachment.size.toLocaleString(),
1192
+ " bytes"
1193
+ ] })
1194
+ ] }),
1195
+ resolve ? /* @__PURE__ */ jsx6(
1196
+ "button",
1197
+ {
1198
+ type: "button",
1199
+ "aria-expanded": open,
1200
+ onClick: () => setOpen((value) => !value),
1201
+ children: open ? `Close ${attachment.name}` : `Preview ${attachment.name}`
1202
+ }
1203
+ ) : /* @__PURE__ */ jsx6("p", { children: "Original attachment requires a host resolver." }),
1204
+ open && resolve && /* @__PURE__ */ jsx6("div", { className: "texion-saved-attachment-preview", children: !current ? /* @__PURE__ */ jsx6("p", { role: "status", children: "Restoring attachment\u2026" }) : current.error ? /* @__PURE__ */ jsx6("p", { role: "alert", children: current.error }) : current.input === void 0 ? /* @__PURE__ */ jsx6("p", { role: "status", children: "The original attachment is not available." }) : /* @__PURE__ */ jsx6(
1205
+ DocumentViewer,
1206
+ {
1207
+ input: current.input,
1208
+ format: attachment.format,
1209
+ ...options ? { options } : {},
1210
+ ...renderers ? { renderers } : {}
1211
+ }
1212
+ ) })
1213
+ ] });
1214
+ }
1215
+ function TexionMessageViewer({
1216
+ message,
1217
+ resolveAttachment,
1218
+ options,
1219
+ renderers,
1220
+ className = "texion-message"
1221
+ }) {
1222
+ if (!isTexionMessage(message))
1223
+ return /* @__PURE__ */ jsx6("p", { role: "alert", children: "Invalid Texion message." });
1224
+ return /* @__PURE__ */ jsxs5("section", { className, "aria-label": "Saved message", children: [
1225
+ message.content.source.trim() && /* @__PURE__ */ jsx6(
1226
+ TexionViewer,
1227
+ {
1228
+ content: message.content,
1229
+ ...options ? { options } : {},
1230
+ ...renderers ? { renderers } : {}
1231
+ }
1232
+ ),
1233
+ message.attachments.length > 0 && /* @__PURE__ */ jsx6("ul", { "aria-label": "Saved attachments", children: message.attachments.map((attachment) => /* @__PURE__ */ jsx6(
1234
+ SavedAttachment,
1235
+ {
1236
+ attachment,
1237
+ resolve: resolveAttachment,
1238
+ options,
1239
+ renderers
1240
+ },
1241
+ attachment.id
1242
+ )) })
1243
+ ] });
1244
+ }
1245
+
1246
+ // src/hooks/useOcrApproval.ts
1247
+ import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef5, useState as useState8 } from "react";
1248
+ function useOcrApproval() {
1249
+ const [pending, setPending] = useState8([]);
1250
+ const requests = useRef5(
1251
+ /* @__PURE__ */ new Map()
1252
+ );
1253
+ const next = useRef5(0);
1254
+ const requestApproval = useCallback2(
1255
+ (request) => {
1256
+ if (request.signal.aborted) return Promise.resolve(false);
1257
+ const id = `ocr-approval-${++next.current}`;
1258
+ return new Promise((resolve) => {
1259
+ const abort = () => finish(false);
1260
+ function finish(approved) {
1261
+ if (!requests.current.delete(id)) return;
1262
+ request.signal.removeEventListener("abort", abort);
1263
+ setPending((values) => values.filter((value) => value.id !== id));
1264
+ resolve(approved);
1265
+ }
1266
+ requests.current.set(id, { finish });
1267
+ request.signal.addEventListener("abort", abort, { once: true });
1268
+ setPending((values) => [...values, { id, request }]);
1269
+ });
1270
+ },
1271
+ []
1272
+ );
1273
+ const approve = useCallback2(
1274
+ (id) => requests.current.get(id)?.finish(true),
1275
+ []
1276
+ );
1277
+ const deny = useCallback2(
1278
+ (id) => requests.current.get(id)?.finish(false),
1279
+ []
1280
+ );
1281
+ useEffect7(
1282
+ () => () => {
1283
+ for (const request of [...requests.current.values()])
1284
+ request.finish(false);
1285
+ },
1286
+ []
1287
+ );
1288
+ return { pending, requestApproval, approve, deny };
1289
+ }
1290
+ export {
1291
+ AttachmentList,
1292
+ DocumentViewer,
1293
+ HighlightedTextarea,
1294
+ TexionInput,
1295
+ TexionMessageViewer,
1296
+ TexionViewer,
1297
+ useDocument,
1298
+ useMarkdownInput,
1299
+ useOcrApproval,
1300
+ useSourceInput,
1301
+ useTexion,
1302
+ useTexionAttachments
1303
+ };
1304
+ //# sourceMappingURL=index.js.map