@tutti-os/ui-rich-text 0.0.1
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/LICENSE +202 -0
- package/README.md +168 -0
- package/dist/chunk-52PIIFZA.js +525 -0
- package/dist/chunk-52PIIFZA.js.map +1 -0
- package/dist/chunk-K5POY2YJ.js +268 -0
- package/dist/chunk-K5POY2YJ.js.map +1 -0
- package/dist/chunk-VQHCWUBH.js +63 -0
- package/dist/chunk-VQHCWUBH.js.map +1 -0
- package/dist/chunk-YLWTSNTT.js +1 -0
- package/dist/chunk-YLWTSNTT.js.map +1 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +38 -0
- package/dist/core/index.js.map +1 -0
- package/dist/editor/index.d.ts +77 -0
- package/dist/editor/index.js +1738 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/mention-QICvf4wE.d.ts +81 -0
- package/dist/plugins/index.d.ts +20 -0
- package/dist/plugins/index.js +30 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/richTextI18n-CUeqHuEQ.d.ts +13 -0
- package/dist/types/index.d.ts +57 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1738 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDefaultRichTextI18nRuntime
|
|
3
|
+
} from "../chunk-VQHCWUBH.js";
|
|
4
|
+
import {
|
|
5
|
+
createRichTextAtRegistry,
|
|
6
|
+
createRichTextMentionAttrs,
|
|
7
|
+
getRichTextMentionDisplayText,
|
|
8
|
+
renderRichTextAtInsertResult,
|
|
9
|
+
resolveRichTextMentionView
|
|
10
|
+
} from "../chunk-K5POY2YJ.js";
|
|
11
|
+
import {
|
|
12
|
+
findRichTextMarkdownLinks,
|
|
13
|
+
isRichTextMentionHref,
|
|
14
|
+
mentionReferenceNodeName,
|
|
15
|
+
normalizeRichTextContent,
|
|
16
|
+
normalizeRichTextLinkHref,
|
|
17
|
+
parseRichTextContentToDocument,
|
|
18
|
+
parseRichTextMentionHref,
|
|
19
|
+
serializeRichTextDocumentToContent,
|
|
20
|
+
workspaceReferenceNodeName
|
|
21
|
+
} from "../chunk-52PIIFZA.js";
|
|
22
|
+
|
|
23
|
+
// src/editor/RichTextAtEditor.tsx
|
|
24
|
+
import {
|
|
25
|
+
useEffect as useEffect2,
|
|
26
|
+
useLayoutEffect,
|
|
27
|
+
useMemo,
|
|
28
|
+
useRef,
|
|
29
|
+
useState as useState2
|
|
30
|
+
} from "react";
|
|
31
|
+
import Document from "@tiptap/extension-document";
|
|
32
|
+
import HardBreak from "@tiptap/extension-hard-break";
|
|
33
|
+
import Paragraph from "@tiptap/extension-paragraph";
|
|
34
|
+
import Text from "@tiptap/extension-text";
|
|
35
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
36
|
+
import { ViewportMenuSurface } from "@tutti-os/ui-system/components";
|
|
37
|
+
import { cn } from "@tutti-os/ui-system/utils";
|
|
38
|
+
|
|
39
|
+
// src/editor/richTextAtQuery.ts
|
|
40
|
+
function isTriggerPrefixBoundary(character) {
|
|
41
|
+
return /[\s,;:!?<>{}|\\'"`~()[\]]/.test(character);
|
|
42
|
+
}
|
|
43
|
+
function findRichTextAtQuery(value, caret) {
|
|
44
|
+
const cursor = Math.max(0, Math.min(caret, value.length));
|
|
45
|
+
let segmentStart = cursor;
|
|
46
|
+
while (segmentStart > 0) {
|
|
47
|
+
const previous = value[segmentStart - 1] ?? "";
|
|
48
|
+
if (/\s/.test(previous)) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
segmentStart -= 1;
|
|
52
|
+
}
|
|
53
|
+
const segment = value.slice(segmentStart, cursor);
|
|
54
|
+
for (let index = segment.lastIndexOf("@"); index >= 0; index = segment.lastIndexOf("@", index - 1)) {
|
|
55
|
+
const previous = segment[index - 1] ?? "";
|
|
56
|
+
if (index > 0 && !isTriggerPrefixBoundary(previous)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const candidate = segment.slice(index);
|
|
60
|
+
if (/[[\]()]/.test(candidate.slice(1))) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
from: segmentStart + index,
|
|
65
|
+
to: cursor,
|
|
66
|
+
keyword: candidate.slice(1)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
async function queryRichTextAtMatches(registry, input) {
|
|
72
|
+
try {
|
|
73
|
+
return await registry.query(input);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/editor/richTextIme.ts
|
|
80
|
+
function isRichTextImeComposing(event) {
|
|
81
|
+
if (event.isComposing || event.nativeEvent?.isComposing) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
const keyCode = event.keyCode ?? event.nativeEvent?.keyCode;
|
|
85
|
+
const which = event.which ?? event.nativeEvent?.which;
|
|
86
|
+
return keyCode === 229 || which === 229;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/editor/richTextAtText.ts
|
|
90
|
+
var defaultRichTextI18n = createDefaultRichTextI18nRuntime();
|
|
91
|
+
function resolveRichTextAtText(overrides, removeDecorationAriaLabel, i18n = defaultRichTextI18n) {
|
|
92
|
+
return {
|
|
93
|
+
loadingLabel: overrides?.loadingLabel?.trim() || i18n.t("richTextAt.loading"),
|
|
94
|
+
noMatchesLabel: overrides?.noMatchesLabel?.trim() || i18n.t("richTextAt.noMatches"),
|
|
95
|
+
removeReferenceActionLabel: removeDecorationAriaLabel?.trim() || overrides?.removeReferenceActionLabel?.trim() || i18n.t("richTextAt.removeReferenceActionLabel")
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
var defaultRichTextAtText = resolveRichTextAtText();
|
|
99
|
+
|
|
100
|
+
// src/extensions/mentionReference.ts
|
|
101
|
+
import { mergeAttributes, Node } from "@tiptap/core";
|
|
102
|
+
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
103
|
+
|
|
104
|
+
// src/extensions/MentionReferenceNodeView.tsx
|
|
105
|
+
import { NodeViewWrapper } from "@tiptap/react";
|
|
106
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
107
|
+
function MentionReferenceNodeView({
|
|
108
|
+
node,
|
|
109
|
+
selected
|
|
110
|
+
}) {
|
|
111
|
+
const label = typeof node.attrs.label === "string" ? node.attrs.label.trim() : "";
|
|
112
|
+
return /* @__PURE__ */ jsx(
|
|
113
|
+
NodeViewWrapper,
|
|
114
|
+
{
|
|
115
|
+
as: "span",
|
|
116
|
+
className: `inline-flex max-w-full align-baseline ${selected ? "is-selected" : ""}`,
|
|
117
|
+
contentEditable: false,
|
|
118
|
+
"data-rich-text-mention-reference": "true",
|
|
119
|
+
children: /* @__PURE__ */ jsxs(
|
|
120
|
+
"span",
|
|
121
|
+
{
|
|
122
|
+
className: `inline-flex max-w-full items-center overflow-hidden rounded-md px-1.5 py-0.5 text-sm font-medium ${selected ? "bg-[var(--background-fronted)] text-[var(--text-primary)] shadow-soft" : "bg-transparency-block text-[var(--text-primary)]"}`,
|
|
123
|
+
children: [
|
|
124
|
+
"@",
|
|
125
|
+
label
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/extensions/mentionReference.ts
|
|
134
|
+
var MentionReference = Node.create({
|
|
135
|
+
name: mentionReferenceNodeName,
|
|
136
|
+
group: "inline",
|
|
137
|
+
inline: true,
|
|
138
|
+
atom: true,
|
|
139
|
+
selectable: true,
|
|
140
|
+
addAttributes() {
|
|
141
|
+
return {
|
|
142
|
+
entityId: {
|
|
143
|
+
default: ""
|
|
144
|
+
},
|
|
145
|
+
href: {
|
|
146
|
+
default: null
|
|
147
|
+
},
|
|
148
|
+
kind: {
|
|
149
|
+
default: null
|
|
150
|
+
},
|
|
151
|
+
label: {
|
|
152
|
+
default: ""
|
|
153
|
+
},
|
|
154
|
+
meta: {
|
|
155
|
+
default: null
|
|
156
|
+
},
|
|
157
|
+
plugin: {
|
|
158
|
+
default: ""
|
|
159
|
+
},
|
|
160
|
+
trigger: {
|
|
161
|
+
default: "@"
|
|
162
|
+
},
|
|
163
|
+
version: {
|
|
164
|
+
default: null
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
parseHTML() {
|
|
169
|
+
return [{ tag: "span[data-rich-text-mention-reference]" }];
|
|
170
|
+
},
|
|
171
|
+
renderHTML({ HTMLAttributes }) {
|
|
172
|
+
const label = typeof HTMLAttributes.label === "string" ? HTMLAttributes.label : "";
|
|
173
|
+
return [
|
|
174
|
+
"span",
|
|
175
|
+
mergeAttributes(HTMLAttributes, {
|
|
176
|
+
"data-rich-text-mention-reference": "true",
|
|
177
|
+
class: "inline-flex max-w-full items-center overflow-hidden rounded-md bg-transparency-block px-1.5 py-0.5 align-baseline text-sm font-medium text-[var(--text-primary)]"
|
|
178
|
+
}),
|
|
179
|
+
`@${label}`
|
|
180
|
+
];
|
|
181
|
+
},
|
|
182
|
+
addNodeView() {
|
|
183
|
+
return ReactNodeViewRenderer(MentionReferenceNodeView);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// src/extensions/workspaceReference.ts
|
|
188
|
+
import { mergeAttributes as mergeAttributes2, Node as Node2 } from "@tiptap/core";
|
|
189
|
+
import { ReactNodeViewRenderer as ReactNodeViewRenderer2 } from "@tiptap/react";
|
|
190
|
+
|
|
191
|
+
// src/extensions/WorkspaceReferenceNodeView.tsx
|
|
192
|
+
import { useEffect, useState } from "react";
|
|
193
|
+
import { NodeViewWrapper as NodeViewWrapper2 } from "@tiptap/react";
|
|
194
|
+
import {
|
|
195
|
+
MentionPill,
|
|
196
|
+
Tooltip,
|
|
197
|
+
TooltipContent,
|
|
198
|
+
TooltipTrigger
|
|
199
|
+
} from "@tutti-os/ui-system/components";
|
|
200
|
+
|
|
201
|
+
// src/extensions/workspaceReferencePresentation.ts
|
|
202
|
+
function getWorkspaceReferencePresentation(label, path) {
|
|
203
|
+
const displayLabel = normalizeReferenceLabel(label, path);
|
|
204
|
+
const fullPath = normalizeFullPath(path);
|
|
205
|
+
return {
|
|
206
|
+
displayLabel,
|
|
207
|
+
fullPath
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function normalizeReferenceLabel(label, path) {
|
|
211
|
+
const trimmedLabel = label.trim();
|
|
212
|
+
if (trimmedLabel !== "") {
|
|
213
|
+
return trimmedLabel;
|
|
214
|
+
}
|
|
215
|
+
return getPathBasename(path) || path.trim();
|
|
216
|
+
}
|
|
217
|
+
function normalizeFullPath(path) {
|
|
218
|
+
return trimTrailingSeparators(path.trim());
|
|
219
|
+
}
|
|
220
|
+
function getPathBasename(path) {
|
|
221
|
+
const normalizedPath = trimTrailingSeparators(path.trim());
|
|
222
|
+
if (normalizedPath === "" || isPathRoot(normalizedPath)) {
|
|
223
|
+
return normalizedPath;
|
|
224
|
+
}
|
|
225
|
+
const segments = normalizedPath.split(/[\\/]+/).filter(Boolean);
|
|
226
|
+
return segments.at(-1) ?? "";
|
|
227
|
+
}
|
|
228
|
+
function trimTrailingSeparators(path) {
|
|
229
|
+
if (path === "") {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
if (isPathRoot(path)) {
|
|
233
|
+
return path;
|
|
234
|
+
}
|
|
235
|
+
return path.replace(/[\\/]+$/, "");
|
|
236
|
+
}
|
|
237
|
+
function isPathRoot(path) {
|
|
238
|
+
return path === "/" || path === "\\" || path === "//" || path === "\\\\" || /^[A-Za-z]:[\\/]?$/.test(path);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/extensions/WorkspaceReferenceNodeView.tsx
|
|
242
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
243
|
+
var richTextWorkspaceReferencePillClassName = "max-w-[18rem]";
|
|
244
|
+
function WorkspaceReferenceNodeView({
|
|
245
|
+
deleteNode,
|
|
246
|
+
editor,
|
|
247
|
+
extension,
|
|
248
|
+
node,
|
|
249
|
+
selected
|
|
250
|
+
}) {
|
|
251
|
+
const [isEditable, setIsEditable] = useState(editor.isEditable);
|
|
252
|
+
const attrs = node.attrs;
|
|
253
|
+
const extensionOptions = extension.options;
|
|
254
|
+
const kind = attrs.kind === "folder" ? "folder" : "file";
|
|
255
|
+
const label = typeof attrs.label === "string" ? attrs.label : "";
|
|
256
|
+
const path = typeof attrs.path === "string" ? attrs.path : "";
|
|
257
|
+
const presentation = getWorkspaceReferencePresentation(label, path);
|
|
258
|
+
const removeActionAriaLabel = typeof extensionOptions.removeActionAriaLabel === "string" ? extensionOptions.removeActionAriaLabel : defaultRichTextAtText.removeReferenceActionLabel;
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
const syncEditable = () => {
|
|
261
|
+
setIsEditable(editor.isEditable);
|
|
262
|
+
};
|
|
263
|
+
syncEditable();
|
|
264
|
+
editor.on("transaction", syncEditable);
|
|
265
|
+
editor.on("update", syncEditable);
|
|
266
|
+
return () => {
|
|
267
|
+
editor.off("transaction", syncEditable);
|
|
268
|
+
editor.off("update", syncEditable);
|
|
269
|
+
};
|
|
270
|
+
}, [editor]);
|
|
271
|
+
const handleRemove = (event) => {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
event.stopPropagation();
|
|
274
|
+
if (!editor.isEditable) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
deleteNode();
|
|
278
|
+
};
|
|
279
|
+
return /* @__PURE__ */ jsx2(
|
|
280
|
+
NodeViewWrapper2,
|
|
281
|
+
{
|
|
282
|
+
as: "span",
|
|
283
|
+
className: `inline-flex max-w-full align-baseline ${selected ? "is-selected" : ""}`,
|
|
284
|
+
contentEditable: false,
|
|
285
|
+
"data-rich-text-workspace-reference": "true",
|
|
286
|
+
children: /* @__PURE__ */ jsxs2(Tooltip, { children: [
|
|
287
|
+
/* @__PURE__ */ jsx2(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(
|
|
288
|
+
MentionPill,
|
|
289
|
+
{
|
|
290
|
+
className: richTextWorkspaceReferencePillClassName,
|
|
291
|
+
fileKind: kind,
|
|
292
|
+
kind: "file",
|
|
293
|
+
label: presentation.displayLabel,
|
|
294
|
+
removable: isEditable,
|
|
295
|
+
removeButtonProps: isEditable ? {
|
|
296
|
+
"aria-label": removeActionAriaLabel,
|
|
297
|
+
onMouseDown: handleRemove
|
|
298
|
+
} : void 0
|
|
299
|
+
}
|
|
300
|
+
) }),
|
|
301
|
+
/* @__PURE__ */ jsx2(
|
|
302
|
+
TooltipContent,
|
|
303
|
+
{
|
|
304
|
+
className: "max-w-md whitespace-normal break-all",
|
|
305
|
+
sideOffset: 8,
|
|
306
|
+
children: presentation.fullPath
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
] })
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/extensions/workspaceReference.ts
|
|
315
|
+
var WorkspaceReference = Node2.create({
|
|
316
|
+
name: workspaceReferenceNodeName,
|
|
317
|
+
addOptions() {
|
|
318
|
+
return {
|
|
319
|
+
removeActionAriaLabel: defaultRichTextAtText.removeReferenceActionLabel
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
group: "inline",
|
|
323
|
+
inline: true,
|
|
324
|
+
atom: true,
|
|
325
|
+
selectable: true,
|
|
326
|
+
addAttributes() {
|
|
327
|
+
return {
|
|
328
|
+
kind: {
|
|
329
|
+
default: "file"
|
|
330
|
+
},
|
|
331
|
+
label: {
|
|
332
|
+
default: ""
|
|
333
|
+
},
|
|
334
|
+
path: {
|
|
335
|
+
default: ""
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
parseHTML() {
|
|
340
|
+
return [{ tag: "span[data-rich-text-workspace-reference]" }];
|
|
341
|
+
},
|
|
342
|
+
renderHTML({ HTMLAttributes }) {
|
|
343
|
+
const kind = HTMLAttributes.kind === "folder" ? "folder" : "file";
|
|
344
|
+
const label = typeof HTMLAttributes.label === "string" ? HTMLAttributes.label : "";
|
|
345
|
+
const path = typeof HTMLAttributes.path === "string" ? HTMLAttributes.path : "";
|
|
346
|
+
const presentation = getWorkspaceReferencePresentation(label, path);
|
|
347
|
+
const referenceColorClassName = kind === "folder" ? "text-[var(--rich-text-folder)]" : "text-[var(--rich-text-mention-file)]";
|
|
348
|
+
return [
|
|
349
|
+
"span",
|
|
350
|
+
mergeAttributes2(HTMLAttributes, {
|
|
351
|
+
"data-rich-text-workspace-reference": "true",
|
|
352
|
+
"data-rich-text-workspace-kind": kind,
|
|
353
|
+
"data-agent-file-mention": "true",
|
|
354
|
+
"data-agent-mention-kind": "file",
|
|
355
|
+
"data-slot": "mention-pill",
|
|
356
|
+
title: presentation.fullPath,
|
|
357
|
+
class: [
|
|
358
|
+
"group relative top-[3px] inline-flex max-w-full cursor-default items-center gap-1.5 overflow-hidden rounded-[4px] border border-transparent bg-transparent px-1.5 py-0.5 align-baseline text-sm font-medium leading-5 no-underline transition-colors hover:border-transparent hover:bg-[color-mix(in_srgb,currentColor_12%,transparent)]",
|
|
359
|
+
referenceColorClassName
|
|
360
|
+
].join(" ")
|
|
361
|
+
}),
|
|
362
|
+
[
|
|
363
|
+
"span",
|
|
364
|
+
{
|
|
365
|
+
"aria-hidden": "true",
|
|
366
|
+
class: "grid size-4 shrink-0 place-items-center text-current"
|
|
367
|
+
},
|
|
368
|
+
kind === "folder" ? "D" : "F"
|
|
369
|
+
],
|
|
370
|
+
[
|
|
371
|
+
"span",
|
|
372
|
+
{
|
|
373
|
+
class: "min-w-0 max-w-[20rem] truncate text-sm font-medium"
|
|
374
|
+
},
|
|
375
|
+
presentation.displayLabel
|
|
376
|
+
]
|
|
377
|
+
];
|
|
378
|
+
},
|
|
379
|
+
addNodeView() {
|
|
380
|
+
return ReactNodeViewRenderer2(WorkspaceReferenceNodeView);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// src/editor/RichTextAtEditor.tsx
|
|
385
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
386
|
+
function RichTextAtEditor({
|
|
387
|
+
value,
|
|
388
|
+
onChange,
|
|
389
|
+
providers = [],
|
|
390
|
+
placeholder,
|
|
391
|
+
disabled = false,
|
|
392
|
+
className,
|
|
393
|
+
textareaClassName,
|
|
394
|
+
placeholderClassName,
|
|
395
|
+
minQueryLength = 0,
|
|
396
|
+
maxResults = 8,
|
|
397
|
+
removeDecorationAriaLabel,
|
|
398
|
+
i18n,
|
|
399
|
+
textOverrides,
|
|
400
|
+
overlay,
|
|
401
|
+
focusSignal
|
|
402
|
+
}) {
|
|
403
|
+
const menuOffset = 6;
|
|
404
|
+
const normalizedValue = normalizeRichTextContent(value);
|
|
405
|
+
const text = resolveRichTextAtText(
|
|
406
|
+
textOverrides,
|
|
407
|
+
removeDecorationAriaLabel,
|
|
408
|
+
i18n
|
|
409
|
+
);
|
|
410
|
+
const latestOnChangeRef = useRef(onChange);
|
|
411
|
+
const lastSerializedValueRef = useRef(normalizedValue);
|
|
412
|
+
const lastFocusSignalRef = useRef(focusSignal);
|
|
413
|
+
const containerRef = useRef(null);
|
|
414
|
+
const registry = useMemo(
|
|
415
|
+
() => createRichTextAtRegistry(providers),
|
|
416
|
+
[providers]
|
|
417
|
+
);
|
|
418
|
+
const [isFocused, setIsFocused] = useState2(false);
|
|
419
|
+
const [query, setQuery] = useState2(null);
|
|
420
|
+
const [matches, setMatches] = useState2([]);
|
|
421
|
+
const [activeIndex, setActiveIndex] = useState2(0);
|
|
422
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
423
|
+
const [menuPoint, setMenuPoint] = useState2(
|
|
424
|
+
null
|
|
425
|
+
);
|
|
426
|
+
latestOnChangeRef.current = onChange;
|
|
427
|
+
const editor = useEditor({
|
|
428
|
+
immediatelyRender: false,
|
|
429
|
+
editable: !disabled,
|
|
430
|
+
extensions: [
|
|
431
|
+
Document,
|
|
432
|
+
Paragraph,
|
|
433
|
+
Text,
|
|
434
|
+
HardBreak,
|
|
435
|
+
WorkspaceReference.configure({
|
|
436
|
+
removeActionAriaLabel: text.removeReferenceActionLabel
|
|
437
|
+
}),
|
|
438
|
+
MentionReference
|
|
439
|
+
],
|
|
440
|
+
content: parseRichTextContentToDocument(normalizedValue),
|
|
441
|
+
editorProps: {
|
|
442
|
+
attributes: {
|
|
443
|
+
class: cn(
|
|
444
|
+
"w-full whitespace-pre-wrap break-words outline-none",
|
|
445
|
+
textareaClassName
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
onBlur() {
|
|
450
|
+
window.setTimeout(() => {
|
|
451
|
+
setIsFocused(false);
|
|
452
|
+
setMatches([]);
|
|
453
|
+
setActiveIndex(0);
|
|
454
|
+
setIsLoading(false);
|
|
455
|
+
setMenuPoint(null);
|
|
456
|
+
}, 100);
|
|
457
|
+
},
|
|
458
|
+
onFocus() {
|
|
459
|
+
setIsFocused(true);
|
|
460
|
+
},
|
|
461
|
+
onUpdate({ editor: editor2 }) {
|
|
462
|
+
const serialized = serializeRichTextDocumentToContent(editor2.getJSON());
|
|
463
|
+
lastSerializedValueRef.current = serialized;
|
|
464
|
+
latestOnChangeRef.current(serialized);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
useEffect2(() => {
|
|
468
|
+
if (!editor) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (lastSerializedValueRef.current === normalizedValue) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const currentSerialized = serializeRichTextDocumentToContent(
|
|
475
|
+
editor.getJSON()
|
|
476
|
+
);
|
|
477
|
+
if (currentSerialized === normalizedValue) {
|
|
478
|
+
lastSerializedValueRef.current = normalizedValue;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
editor.commands.setContent(
|
|
482
|
+
parseRichTextContentToDocument(normalizedValue),
|
|
483
|
+
{
|
|
484
|
+
emitUpdate: false
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
lastSerializedValueRef.current = normalizedValue;
|
|
488
|
+
}, [editor, normalizedValue]);
|
|
489
|
+
useEffect2(() => {
|
|
490
|
+
if (!editor) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (Object.is(lastFocusSignalRef.current, focusSignal)) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
lastFocusSignalRef.current = focusSignal;
|
|
497
|
+
editor.commands.focus("end");
|
|
498
|
+
}, [editor, focusSignal]);
|
|
499
|
+
useEffect2(() => {
|
|
500
|
+
if (!editor) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
editor.setEditable(!disabled);
|
|
504
|
+
editor.view.dispatch(
|
|
505
|
+
editor.state.tr.setMeta("richTextEditable", !disabled)
|
|
506
|
+
);
|
|
507
|
+
}, [disabled, editor]);
|
|
508
|
+
useEffect2(() => {
|
|
509
|
+
if (!editor) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const updateQueryState = () => {
|
|
513
|
+
const nextQuery = isFocused ? findEditorAtQuery(editor) : null;
|
|
514
|
+
setQuery(nextQuery);
|
|
515
|
+
};
|
|
516
|
+
const updateFocus = () => {
|
|
517
|
+
setIsFocused(editor.isFocused);
|
|
518
|
+
updateQueryState();
|
|
519
|
+
};
|
|
520
|
+
updateQueryState();
|
|
521
|
+
editor.on("selectionUpdate", updateQueryState);
|
|
522
|
+
editor.on("transaction", updateQueryState);
|
|
523
|
+
editor.on("focus", updateFocus);
|
|
524
|
+
editor.on("blur", updateFocus);
|
|
525
|
+
return () => {
|
|
526
|
+
editor.off("selectionUpdate", updateQueryState);
|
|
527
|
+
editor.off("transaction", updateQueryState);
|
|
528
|
+
editor.off("focus", updateFocus);
|
|
529
|
+
editor.off("blur", updateFocus);
|
|
530
|
+
};
|
|
531
|
+
}, [editor, isFocused]);
|
|
532
|
+
useEffect2(() => {
|
|
533
|
+
if (!editor || !query || !isFocused || providers.length === 0) {
|
|
534
|
+
setMatches([]);
|
|
535
|
+
setActiveIndex(0);
|
|
536
|
+
setIsLoading(false);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (query.keyword.length < minQueryLength) {
|
|
540
|
+
setMatches([]);
|
|
541
|
+
setActiveIndex(0);
|
|
542
|
+
setIsLoading(false);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const abortController = new AbortController();
|
|
546
|
+
setIsLoading(true);
|
|
547
|
+
void queryRichTextAtMatches(registry, {
|
|
548
|
+
abortSignal: abortController.signal,
|
|
549
|
+
keyword: query.keyword,
|
|
550
|
+
maxResults,
|
|
551
|
+
context: {
|
|
552
|
+
blockText: editor.state.selection.$from.parent.textBetween(
|
|
553
|
+
0,
|
|
554
|
+
editor.state.selection.$from.parent.content.size,
|
|
555
|
+
"\n",
|
|
556
|
+
"\uFFFC"
|
|
557
|
+
),
|
|
558
|
+
documentText: serializeRichTextDocumentToContent(editor.getJSON())
|
|
559
|
+
}
|
|
560
|
+
}).then((nextMatches) => {
|
|
561
|
+
if (abortController.signal.aborted) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
setMatches(nextMatches);
|
|
565
|
+
setActiveIndex(
|
|
566
|
+
(current) => nextMatches.length === 0 ? 0 : Math.max(0, Math.min(current, nextMatches.length - 1))
|
|
567
|
+
);
|
|
568
|
+
}).finally(() => {
|
|
569
|
+
if (!abortController.signal.aborted) {
|
|
570
|
+
setIsLoading(false);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
return () => {
|
|
574
|
+
abortController.abort();
|
|
575
|
+
};
|
|
576
|
+
}, [
|
|
577
|
+
editor,
|
|
578
|
+
isFocused,
|
|
579
|
+
maxResults,
|
|
580
|
+
minQueryLength,
|
|
581
|
+
providers.length,
|
|
582
|
+
query,
|
|
583
|
+
registry
|
|
584
|
+
]);
|
|
585
|
+
useLayoutEffect(() => {
|
|
586
|
+
if (!editor || !query || !isFocused) {
|
|
587
|
+
setMenuPoint(null);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const updateMenuPoint = () => {
|
|
591
|
+
const coords = editor.view.coordsAtPos(editor.state.selection.from);
|
|
592
|
+
setMenuPoint({
|
|
593
|
+
x: coords.left,
|
|
594
|
+
y: coords.bottom + menuOffset
|
|
595
|
+
});
|
|
596
|
+
};
|
|
597
|
+
updateMenuPoint();
|
|
598
|
+
window.addEventListener("resize", updateMenuPoint);
|
|
599
|
+
window.addEventListener("scroll", updateMenuPoint, {
|
|
600
|
+
capture: true,
|
|
601
|
+
passive: true
|
|
602
|
+
});
|
|
603
|
+
return () => {
|
|
604
|
+
window.removeEventListener("resize", updateMenuPoint);
|
|
605
|
+
window.removeEventListener("scroll", updateMenuPoint, true);
|
|
606
|
+
};
|
|
607
|
+
}, [editor, isFocused, menuOffset, query]);
|
|
608
|
+
const isMenuOpen = isFocused && !!query && (isLoading || matches.length > 0);
|
|
609
|
+
const isEmpty = !editor || serializeRichTextDocumentToContent(editor.getJSON()).trim().length === 0;
|
|
610
|
+
const applyMatch = (match) => {
|
|
611
|
+
if (!editor || !query) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const content = renderInsertResultAsEditorContent(
|
|
615
|
+
match.providerId,
|
|
616
|
+
match.insertResult
|
|
617
|
+
);
|
|
618
|
+
if (!content) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
editor.chain().focus().insertContentAt({ from: query.from, to: query.to }, content).run();
|
|
622
|
+
setMatches([]);
|
|
623
|
+
setActiveIndex(0);
|
|
624
|
+
setIsLoading(false);
|
|
625
|
+
setMenuPoint(null);
|
|
626
|
+
};
|
|
627
|
+
const handleKeyDown = (event) => {
|
|
628
|
+
if (isRichTextImeComposing(event)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (!isMenuOpen || matches.length === 0) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (event.key === "ArrowDown") {
|
|
635
|
+
event.preventDefault();
|
|
636
|
+
setActiveIndex((current) => (current + 1) % matches.length);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (event.key === "ArrowUp") {
|
|
640
|
+
event.preventDefault();
|
|
641
|
+
setActiveIndex(
|
|
642
|
+
(current) => (current - 1 + matches.length) % matches.length
|
|
643
|
+
);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
647
|
+
const match = matches[activeIndex];
|
|
648
|
+
if (!match) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
event.preventDefault();
|
|
652
|
+
applyMatch(match);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (event.key === "Escape") {
|
|
656
|
+
event.preventDefault();
|
|
657
|
+
setMatches([]);
|
|
658
|
+
setActiveIndex(0);
|
|
659
|
+
setIsLoading(false);
|
|
660
|
+
setMenuPoint(null);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
return /* @__PURE__ */ jsxs3(
|
|
664
|
+
"div",
|
|
665
|
+
{
|
|
666
|
+
className: cn("relative min-w-0 w-full", className),
|
|
667
|
+
ref: containerRef,
|
|
668
|
+
children: [
|
|
669
|
+
/* @__PURE__ */ jsx3("div", { className: "w-full min-w-0", onKeyDownCapture: handleKeyDown, children: /* @__PURE__ */ jsx3(EditorContent, { editor }) }),
|
|
670
|
+
isEmpty && placeholder ? /* @__PURE__ */ jsx3("div", { className: "pointer-events-none absolute top-0 right-0 left-0 px-0 py-0 text-[var(--text-placeholder)]", children: /* @__PURE__ */ jsx3(
|
|
671
|
+
"div",
|
|
672
|
+
{
|
|
673
|
+
className: cn(
|
|
674
|
+
"min-w-0 w-full whitespace-pre-wrap",
|
|
675
|
+
placeholderClassName ?? textareaClassName,
|
|
676
|
+
"text-[var(--text-placeholder)]"
|
|
677
|
+
),
|
|
678
|
+
children: placeholder
|
|
679
|
+
}
|
|
680
|
+
) }) : null,
|
|
681
|
+
overlay,
|
|
682
|
+
isMenuOpen && menuPoint ? /* @__PURE__ */ jsx3(
|
|
683
|
+
ViewportMenuSurface,
|
|
684
|
+
{
|
|
685
|
+
open: true,
|
|
686
|
+
className: "nextop-rich-text-at-menu max-h-64 w-[min(28rem,calc(100vw-24px))] overflow-y-auto p-1",
|
|
687
|
+
placement: {
|
|
688
|
+
type: "point",
|
|
689
|
+
point: menuPoint,
|
|
690
|
+
alignX: "start",
|
|
691
|
+
alignY: "start",
|
|
692
|
+
estimatedSize: {
|
|
693
|
+
width: 360,
|
|
694
|
+
height: 256
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
children: matches.length > 0 ? matches.map((match, index) => /* @__PURE__ */ jsxs3(
|
|
698
|
+
"button",
|
|
699
|
+
{
|
|
700
|
+
"aria-selected": index === activeIndex,
|
|
701
|
+
className: cn(
|
|
702
|
+
"flex w-full cursor-pointer flex-col items-start gap-0.5 rounded-md px-2.5 py-2 text-left outline-none transition-colors",
|
|
703
|
+
index === activeIndex ? "bg-transparency-block text-[var(--text-primary)]" : "text-[var(--text-primary)] hover:bg-transparency-block"
|
|
704
|
+
),
|
|
705
|
+
type: "button",
|
|
706
|
+
onMouseDown: (event) => {
|
|
707
|
+
event.preventDefault();
|
|
708
|
+
applyMatch(match);
|
|
709
|
+
},
|
|
710
|
+
children: [
|
|
711
|
+
/* @__PURE__ */ jsx3("div", { className: "text-sm leading-5 font-medium", children: match.label }),
|
|
712
|
+
match.subtitle ? /* @__PURE__ */ jsx3("div", { className: "text-xs leading-4 text-[var(--text-secondary)]", children: match.subtitle }) : null
|
|
713
|
+
]
|
|
714
|
+
},
|
|
715
|
+
`${match.providerId}:${match.key}`
|
|
716
|
+
)) : /* @__PURE__ */ jsx3("div", { className: "px-3 py-2 text-xs leading-4 text-[var(--text-secondary)]", children: isLoading ? text.loadingLabel : text.noMatchesLabel })
|
|
717
|
+
}
|
|
718
|
+
) : null
|
|
719
|
+
]
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
function findEditorAtQuery(editor) {
|
|
724
|
+
const { selection } = editor.state;
|
|
725
|
+
if (!selection.empty) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const { $from } = selection;
|
|
729
|
+
if (!$from.parent.isTextblock) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const textBeforeCursor = $from.parent.textBetween(
|
|
733
|
+
0,
|
|
734
|
+
$from.parentOffset,
|
|
735
|
+
"\n",
|
|
736
|
+
"\uFFFC"
|
|
737
|
+
);
|
|
738
|
+
const query = findRichTextAtQuery(textBeforeCursor, textBeforeCursor.length);
|
|
739
|
+
if (!query) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
const distanceFromQueryStart = textBeforeCursor.length - query.from;
|
|
743
|
+
return {
|
|
744
|
+
from: selection.from - distanceFromQueryStart,
|
|
745
|
+
keyword: query.keyword,
|
|
746
|
+
to: selection.from
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function renderInsertResultAsEditorContent(providerId, result) {
|
|
750
|
+
switch (result.kind) {
|
|
751
|
+
case "mention":
|
|
752
|
+
return {
|
|
753
|
+
type: mentionReferenceNodeName,
|
|
754
|
+
attrs: createRichTextMentionAttrs(providerId, result.mention)
|
|
755
|
+
};
|
|
756
|
+
case "markdown-link": {
|
|
757
|
+
const kind = result.href.endsWith("/") ? "folder" : "file";
|
|
758
|
+
return {
|
|
759
|
+
type: workspaceReferenceNodeName,
|
|
760
|
+
attrs: {
|
|
761
|
+
kind,
|
|
762
|
+
label: result.label,
|
|
763
|
+
path: normalizeRichTextLinkHref(result.href, kind)
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
case "text":
|
|
768
|
+
return result.text;
|
|
769
|
+
default:
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/editor/RichTextAtTextarea.tsx
|
|
775
|
+
import {
|
|
776
|
+
useEffect as useEffect3,
|
|
777
|
+
useLayoutEffect as useLayoutEffect2,
|
|
778
|
+
useMemo as useMemo2,
|
|
779
|
+
useRef as useRef2,
|
|
780
|
+
useState as useState3
|
|
781
|
+
} from "react";
|
|
782
|
+
import { ViewportMenuSurface as ViewportMenuSurface2 } from "@tutti-os/ui-system/components";
|
|
783
|
+
import { cn as cn3 } from "@tutti-os/ui-system/utils";
|
|
784
|
+
|
|
785
|
+
// src/editor/richTextTextareaDecorationModel.ts
|
|
786
|
+
var EXTERNAL_LINK_PREFIX = /^(?:[a-z]+:)?\/\//i;
|
|
787
|
+
var MENTION_LINK_PREFIX = /^mention:\/\//i;
|
|
788
|
+
var TEXTAREA_PRESENTATION_STYLE_PROPERTIES = [
|
|
789
|
+
{ styleName: "paddingTop", cssName: "padding-top" },
|
|
790
|
+
{ styleName: "paddingRight", cssName: "padding-right" },
|
|
791
|
+
{ styleName: "paddingBottom", cssName: "padding-bottom" },
|
|
792
|
+
{ styleName: "paddingLeft", cssName: "padding-left" },
|
|
793
|
+
{ styleName: "fontFamily", cssName: "font-family" },
|
|
794
|
+
{ styleName: "fontFeatureSettings", cssName: "font-feature-settings" },
|
|
795
|
+
{ styleName: "fontKerning", cssName: "font-kerning" },
|
|
796
|
+
{ styleName: "fontOpticalSizing", cssName: "font-optical-sizing" },
|
|
797
|
+
{ styleName: "fontSize", cssName: "font-size" },
|
|
798
|
+
{ styleName: "fontStretch", cssName: "font-stretch" },
|
|
799
|
+
{ styleName: "fontStyle", cssName: "font-style" },
|
|
800
|
+
{ styleName: "fontVariant", cssName: "font-variant" },
|
|
801
|
+
{ styleName: "fontVariationSettings", cssName: "font-variation-settings" },
|
|
802
|
+
{ styleName: "fontWeight", cssName: "font-weight" },
|
|
803
|
+
{ styleName: "letterSpacing", cssName: "letter-spacing" },
|
|
804
|
+
{ styleName: "lineHeight", cssName: "line-height" },
|
|
805
|
+
{ styleName: "textAlign", cssName: "text-align" },
|
|
806
|
+
{ styleName: "textIndent", cssName: "text-indent" },
|
|
807
|
+
{ styleName: "textTransform", cssName: "text-transform" },
|
|
808
|
+
{ styleName: "tabSize", cssName: "tab-size" },
|
|
809
|
+
{ styleName: "MozTabSize", cssName: "-moz-tab-size" }
|
|
810
|
+
];
|
|
811
|
+
function isDecoratableMarkdownHref(href) {
|
|
812
|
+
const trimmedHref = href.trim();
|
|
813
|
+
if (!trimmedHref) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
if (EXTERNAL_LINK_PREFIX.test(trimmedHref) || MENTION_LINK_PREFIX.test(trimmedHref)) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
function buildRichTextTextareaDecorationSegments(value) {
|
|
822
|
+
const segments = [];
|
|
823
|
+
let cursor = 0;
|
|
824
|
+
for (const match of findRichTextMarkdownLinks(value)) {
|
|
825
|
+
const label = match.label.trim();
|
|
826
|
+
const href = match.href.trim();
|
|
827
|
+
const { index, source, to } = match;
|
|
828
|
+
if (index > cursor) {
|
|
829
|
+
segments.push({
|
|
830
|
+
type: "text",
|
|
831
|
+
text: value.slice(cursor, index),
|
|
832
|
+
from: cursor,
|
|
833
|
+
to: index
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (label && href && isDecoratableMarkdownHref(href)) {
|
|
837
|
+
segments.push({
|
|
838
|
+
type: "link",
|
|
839
|
+
text: source,
|
|
840
|
+
from: index,
|
|
841
|
+
to,
|
|
842
|
+
label,
|
|
843
|
+
href,
|
|
844
|
+
kind: href.endsWith("/") ? "folder" : "file"
|
|
845
|
+
});
|
|
846
|
+
} else {
|
|
847
|
+
segments.push({
|
|
848
|
+
type: "text",
|
|
849
|
+
text: source,
|
|
850
|
+
from: index,
|
|
851
|
+
to
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
cursor = to;
|
|
855
|
+
}
|
|
856
|
+
if (cursor < value.length) {
|
|
857
|
+
segments.push({
|
|
858
|
+
type: "text",
|
|
859
|
+
text: value.slice(cursor),
|
|
860
|
+
from: cursor,
|
|
861
|
+
to: value.length
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return segments;
|
|
865
|
+
}
|
|
866
|
+
function hasRichTextTextareaDecorations(segments) {
|
|
867
|
+
return segments.some((segment) => segment.type === "link");
|
|
868
|
+
}
|
|
869
|
+
function resolveRichTextTextareaSelectionBoundary(segments, selectionStart) {
|
|
870
|
+
for (const segment of segments) {
|
|
871
|
+
if (segment.type !== "link") {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (selectionStart <= segment.from || selectionStart >= segment.to) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const midpoint = segment.from + (segment.to - segment.from) / 2;
|
|
878
|
+
return selectionStart < midpoint ? segment.from : segment.to;
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
function setTextareaPresentationStyleValue(styleRecord, styleName, value) {
|
|
883
|
+
styleRecord[styleName] = value;
|
|
884
|
+
}
|
|
885
|
+
function getTextareaPresentationStyle(textarea) {
|
|
886
|
+
const computedStyle = window.getComputedStyle(textarea);
|
|
887
|
+
const styleRecord = {
|
|
888
|
+
whiteSpace: "pre-wrap"
|
|
889
|
+
};
|
|
890
|
+
setTextareaPresentationStyleValue(
|
|
891
|
+
styleRecord,
|
|
892
|
+
"wordBreak",
|
|
893
|
+
computedStyle.wordBreak
|
|
894
|
+
);
|
|
895
|
+
setTextareaPresentationStyleValue(
|
|
896
|
+
styleRecord,
|
|
897
|
+
"overflowWrap",
|
|
898
|
+
computedStyle.overflowWrap
|
|
899
|
+
);
|
|
900
|
+
for (const { styleName, cssName } of TEXTAREA_PRESENTATION_STYLE_PROPERTIES) {
|
|
901
|
+
setTextareaPresentationStyleValue(
|
|
902
|
+
styleRecord,
|
|
903
|
+
styleName,
|
|
904
|
+
computedStyle.getPropertyValue(cssName)
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return styleRecord;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/editor/richTextTextareaDecorations.tsx
|
|
911
|
+
import { MentionPill as MentionPill2 } from "@tutti-os/ui-system/components";
|
|
912
|
+
import { cn as cn2 } from "@tutti-os/ui-system/utils";
|
|
913
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
914
|
+
var richTextWorkspaceReferencePillClassName2 = "max-w-[18rem]";
|
|
915
|
+
function RichTextTextareaDecoratedContent({
|
|
916
|
+
onClickSegment,
|
|
917
|
+
onRemoveSegment,
|
|
918
|
+
removeActionAriaLabel,
|
|
919
|
+
scrollLeft,
|
|
920
|
+
scrollTop,
|
|
921
|
+
segments,
|
|
922
|
+
textareaStyle
|
|
923
|
+
}) {
|
|
924
|
+
return /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 z-10 overflow-hidden", children: /* @__PURE__ */ jsx4(
|
|
925
|
+
"div",
|
|
926
|
+
{
|
|
927
|
+
className: "min-h-full min-w-full",
|
|
928
|
+
style: {
|
|
929
|
+
...textareaStyle,
|
|
930
|
+
transform: `translate(${-scrollLeft}px, ${-scrollTop}px)`
|
|
931
|
+
},
|
|
932
|
+
children: segments.map((segment, index) => {
|
|
933
|
+
if (segment.type === "text") {
|
|
934
|
+
return /* @__PURE__ */ jsx4("span", { children: segment.text }, index);
|
|
935
|
+
}
|
|
936
|
+
return /* @__PURE__ */ jsxs4("span", { className: "relative", children: [
|
|
937
|
+
/* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "text-transparent", children: segment.text }),
|
|
938
|
+
/* @__PURE__ */ jsx4(
|
|
939
|
+
MentionPill2,
|
|
940
|
+
{
|
|
941
|
+
className: cn2(
|
|
942
|
+
"pointer-events-auto absolute inset-y-0 left-0 pr-1 pl-1.5",
|
|
943
|
+
richTextWorkspaceReferencePillClassName2
|
|
944
|
+
),
|
|
945
|
+
fileKind: segment.kind,
|
|
946
|
+
kind: "file",
|
|
947
|
+
label: segment.label,
|
|
948
|
+
removeButtonProps: {
|
|
949
|
+
"aria-label": removeActionAriaLabel,
|
|
950
|
+
onPointerDown: (event) => {
|
|
951
|
+
event.preventDefault();
|
|
952
|
+
event.stopPropagation();
|
|
953
|
+
onRemoveSegment(segment);
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
summary: /* @__PURE__ */ jsx4(
|
|
957
|
+
"span",
|
|
958
|
+
{
|
|
959
|
+
className: cn2(
|
|
960
|
+
"min-w-0 truncate text-xs text-current opacity-80",
|
|
961
|
+
segment.kind === "folder" ? "max-w-[18rem]" : "max-w-[20rem]"
|
|
962
|
+
),
|
|
963
|
+
children: segment.href
|
|
964
|
+
}
|
|
965
|
+
),
|
|
966
|
+
onPointerDown: (event) => {
|
|
967
|
+
onClickSegment(segment, event);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
)
|
|
971
|
+
] }, index);
|
|
972
|
+
})
|
|
973
|
+
}
|
|
974
|
+
) });
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/editor/richTextTextareaCaret.ts
|
|
978
|
+
var TEXTAREA_MIRROR_STYLE_PROPERTIES = [
|
|
979
|
+
"boxSizing",
|
|
980
|
+
"width",
|
|
981
|
+
"paddingTop",
|
|
982
|
+
"paddingRight",
|
|
983
|
+
"paddingBottom",
|
|
984
|
+
"paddingLeft",
|
|
985
|
+
"borderTopWidth",
|
|
986
|
+
"borderRightWidth",
|
|
987
|
+
"borderBottomWidth",
|
|
988
|
+
"borderLeftWidth",
|
|
989
|
+
"borderTopStyle",
|
|
990
|
+
"borderRightStyle",
|
|
991
|
+
"borderBottomStyle",
|
|
992
|
+
"borderLeftStyle",
|
|
993
|
+
"borderTopColor",
|
|
994
|
+
"borderRightColor",
|
|
995
|
+
"borderBottomColor",
|
|
996
|
+
"borderLeftColor",
|
|
997
|
+
"borderRadius",
|
|
998
|
+
"fontFamily",
|
|
999
|
+
"fontFeatureSettings",
|
|
1000
|
+
"fontKerning",
|
|
1001
|
+
"fontOpticalSizing",
|
|
1002
|
+
"fontSize",
|
|
1003
|
+
"fontStretch",
|
|
1004
|
+
"fontStyle",
|
|
1005
|
+
"fontVariant",
|
|
1006
|
+
"fontVariationSettings",
|
|
1007
|
+
"fontWeight",
|
|
1008
|
+
"letterSpacing",
|
|
1009
|
+
"lineHeight",
|
|
1010
|
+
"textAlign",
|
|
1011
|
+
"textDecoration",
|
|
1012
|
+
"textIndent",
|
|
1013
|
+
"textTransform",
|
|
1014
|
+
"wordBreak",
|
|
1015
|
+
"overflowWrap",
|
|
1016
|
+
"tabSize",
|
|
1017
|
+
"MozTabSize"
|
|
1018
|
+
];
|
|
1019
|
+
function parsePixelValue(value) {
|
|
1020
|
+
if (!value || value === "normal") {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
const parsedValue = Number.parseFloat(value);
|
|
1024
|
+
return Number.isFinite(parsedValue) ? parsedValue : null;
|
|
1025
|
+
}
|
|
1026
|
+
function resolveLineHeight(computedStyle) {
|
|
1027
|
+
const explicitLineHeight = parsePixelValue(computedStyle.lineHeight);
|
|
1028
|
+
if (explicitLineHeight !== null) {
|
|
1029
|
+
return explicitLineHeight;
|
|
1030
|
+
}
|
|
1031
|
+
const fontSize = parsePixelValue(computedStyle.fontSize);
|
|
1032
|
+
return fontSize !== null ? fontSize * 1.4 : 20;
|
|
1033
|
+
}
|
|
1034
|
+
function getTextareaCaretViewportPoint(textarea, selectionStart) {
|
|
1035
|
+
if (typeof document === "undefined") {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
const safeSelectionStart = Math.max(
|
|
1039
|
+
0,
|
|
1040
|
+
Math.min(selectionStart, textarea.value.length)
|
|
1041
|
+
);
|
|
1042
|
+
const textareaRect = textarea.getBoundingClientRect();
|
|
1043
|
+
const computedStyle = window.getComputedStyle(textarea);
|
|
1044
|
+
const mirror = document.createElement("div");
|
|
1045
|
+
const marker = document.createElement("span");
|
|
1046
|
+
mirror.setAttribute("aria-hidden", "true");
|
|
1047
|
+
mirror.style.position = "fixed";
|
|
1048
|
+
mirror.style.left = `${textareaRect.left}px`;
|
|
1049
|
+
mirror.style.top = `${textareaRect.top}px`;
|
|
1050
|
+
mirror.style.visibility = "hidden";
|
|
1051
|
+
mirror.style.pointerEvents = "none";
|
|
1052
|
+
mirror.style.overflow = "hidden";
|
|
1053
|
+
for (const propertyName of TEXTAREA_MIRROR_STYLE_PROPERTIES) {
|
|
1054
|
+
mirror.style[propertyName] = computedStyle[propertyName] ?? "";
|
|
1055
|
+
}
|
|
1056
|
+
mirror.style.whiteSpace = "pre-wrap";
|
|
1057
|
+
mirror.style.wordBreak = computedStyle.wordBreak;
|
|
1058
|
+
mirror.style.overflowWrap = computedStyle.overflowWrap;
|
|
1059
|
+
const preSelectionText = textarea.value.slice(0, safeSelectionStart);
|
|
1060
|
+
mirror.textContent = preSelectionText.length > 0 ? preSelectionText : "";
|
|
1061
|
+
if (preSelectionText.endsWith("\n")) {
|
|
1062
|
+
mirror.append(document.createTextNode("\u200B"));
|
|
1063
|
+
}
|
|
1064
|
+
marker.textContent = "\u200B";
|
|
1065
|
+
mirror.append(marker);
|
|
1066
|
+
document.body.append(mirror);
|
|
1067
|
+
try {
|
|
1068
|
+
const markerRect = marker.getBoundingClientRect();
|
|
1069
|
+
const lineHeight = resolveLineHeight(computedStyle);
|
|
1070
|
+
return {
|
|
1071
|
+
x: markerRect.left - textarea.scrollLeft,
|
|
1072
|
+
y: markerRect.top - textarea.scrollTop,
|
|
1073
|
+
lineHeight
|
|
1074
|
+
};
|
|
1075
|
+
} finally {
|
|
1076
|
+
mirror.remove();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/editor/RichTextAtTextarea.tsx
|
|
1081
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1082
|
+
function RichTextAtTextarea({
|
|
1083
|
+
value,
|
|
1084
|
+
onChange,
|
|
1085
|
+
providers = [],
|
|
1086
|
+
placeholder,
|
|
1087
|
+
disabled = false,
|
|
1088
|
+
className,
|
|
1089
|
+
textareaClassName,
|
|
1090
|
+
rows,
|
|
1091
|
+
minQueryLength = 0,
|
|
1092
|
+
maxResults = 8,
|
|
1093
|
+
removeDecorationAriaLabel,
|
|
1094
|
+
i18n,
|
|
1095
|
+
textOverrides,
|
|
1096
|
+
overlay
|
|
1097
|
+
}) {
|
|
1098
|
+
const menuOffset = 6;
|
|
1099
|
+
const text = resolveRichTextAtText(
|
|
1100
|
+
textOverrides,
|
|
1101
|
+
removeDecorationAriaLabel,
|
|
1102
|
+
i18n
|
|
1103
|
+
);
|
|
1104
|
+
const textareaRef = useRef2(null);
|
|
1105
|
+
const [selectionStart, setSelectionStart] = useState3(0);
|
|
1106
|
+
const [matches, setMatches] = useState3([]);
|
|
1107
|
+
const [activeIndex, setActiveIndex] = useState3(0);
|
|
1108
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
1109
|
+
const [isFocused, setIsFocused] = useState3(false);
|
|
1110
|
+
const [menuPoint, setMenuPoint] = useState3(
|
|
1111
|
+
null
|
|
1112
|
+
);
|
|
1113
|
+
const [scrollPosition, setScrollPosition] = useState3({ left: 0, top: 0 });
|
|
1114
|
+
const [textareaPresentationStyle, setTextareaPresentationStyle] = useState3(null);
|
|
1115
|
+
const pendingSelectionRef = useRef2(null);
|
|
1116
|
+
const registry = useMemo2(
|
|
1117
|
+
() => createRichTextAtRegistry(providers),
|
|
1118
|
+
[providers]
|
|
1119
|
+
);
|
|
1120
|
+
const decorationSegments = useMemo2(
|
|
1121
|
+
() => buildRichTextTextareaDecorationSegments(value),
|
|
1122
|
+
[value]
|
|
1123
|
+
);
|
|
1124
|
+
const hasDecorations = hasRichTextTextareaDecorations(decorationSegments);
|
|
1125
|
+
const query = isFocused ? findRichTextAtQuery(value, selectionStart) : null;
|
|
1126
|
+
const shouldQuery = query !== null && query.keyword.length >= minQueryLength && providers.length > 0;
|
|
1127
|
+
const isMenuOpen = isFocused && shouldQuery && (isLoading || matches.length > 0);
|
|
1128
|
+
useEffect3(() => {
|
|
1129
|
+
const nextSelection = pendingSelectionRef.current;
|
|
1130
|
+
if (nextSelection === null || !textareaRef.current) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
textareaRef.current.setSelectionRange(nextSelection, nextSelection);
|
|
1134
|
+
pendingSelectionRef.current = null;
|
|
1135
|
+
}, [value]);
|
|
1136
|
+
useEffect3(() => {
|
|
1137
|
+
if (!shouldQuery || !query) {
|
|
1138
|
+
setMatches([]);
|
|
1139
|
+
setActiveIndex(0);
|
|
1140
|
+
setIsLoading(false);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const abortController = new AbortController();
|
|
1144
|
+
setIsLoading(true);
|
|
1145
|
+
void queryRichTextAtMatches(registry, {
|
|
1146
|
+
abortSignal: abortController.signal,
|
|
1147
|
+
keyword: query.keyword,
|
|
1148
|
+
maxResults,
|
|
1149
|
+
context: {
|
|
1150
|
+
documentText: value,
|
|
1151
|
+
blockText: value
|
|
1152
|
+
}
|
|
1153
|
+
}).then((nextMatches) => {
|
|
1154
|
+
if (abortController.signal.aborted) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
setMatches(nextMatches);
|
|
1158
|
+
setActiveIndex(
|
|
1159
|
+
(current) => nextMatches.length === 0 ? 0 : Math.max(0, Math.min(current, nextMatches.length - 1))
|
|
1160
|
+
);
|
|
1161
|
+
}).finally(() => {
|
|
1162
|
+
if (!abortController.signal.aborted) {
|
|
1163
|
+
setIsLoading(false);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
return () => {
|
|
1167
|
+
abortController.abort();
|
|
1168
|
+
};
|
|
1169
|
+
}, [maxResults, query, registry, shouldQuery, value]);
|
|
1170
|
+
useLayoutEffect2(() => {
|
|
1171
|
+
if (!textareaRef.current || !hasDecorations) {
|
|
1172
|
+
setTextareaPresentationStyle(null);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const textarea = textareaRef.current;
|
|
1176
|
+
setTextareaPresentationStyle(getTextareaPresentationStyle(textarea));
|
|
1177
|
+
setScrollPosition({
|
|
1178
|
+
left: textarea.scrollLeft,
|
|
1179
|
+
top: textarea.scrollTop
|
|
1180
|
+
});
|
|
1181
|
+
}, [hasDecorations, textareaClassName, value]);
|
|
1182
|
+
useLayoutEffect2(() => {
|
|
1183
|
+
if (!isMenuOpen || !query || !textareaRef.current) {
|
|
1184
|
+
setMenuPoint(null);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const caretPoint = getTextareaCaretViewportPoint(
|
|
1188
|
+
textareaRef.current,
|
|
1189
|
+
query.to
|
|
1190
|
+
);
|
|
1191
|
+
const textareaRect = textareaRef.current.getBoundingClientRect();
|
|
1192
|
+
if (!caretPoint) {
|
|
1193
|
+
setMenuPoint({
|
|
1194
|
+
x: textareaRect.left + 12,
|
|
1195
|
+
y: textareaRect.bottom + menuOffset
|
|
1196
|
+
});
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
setMenuPoint({
|
|
1200
|
+
x: caretPoint.x,
|
|
1201
|
+
y: caretPoint.y + caretPoint.lineHeight + menuOffset
|
|
1202
|
+
});
|
|
1203
|
+
}, [isMenuOpen, menuOffset, query, selectionStart, value]);
|
|
1204
|
+
useEffect3(() => {
|
|
1205
|
+
if (!isMenuOpen || !query || !textareaRef.current) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const textarea = textareaRef.current;
|
|
1209
|
+
const updateMenuPoint = () => {
|
|
1210
|
+
const caretPoint = getTextareaCaretViewportPoint(textarea, query.to);
|
|
1211
|
+
const textareaRect = textarea.getBoundingClientRect();
|
|
1212
|
+
setMenuPoint(
|
|
1213
|
+
caretPoint ? {
|
|
1214
|
+
x: caretPoint.x,
|
|
1215
|
+
y: caretPoint.y + caretPoint.lineHeight + menuOffset
|
|
1216
|
+
} : {
|
|
1217
|
+
x: textareaRect.left + 12,
|
|
1218
|
+
y: textareaRect.bottom + menuOffset
|
|
1219
|
+
}
|
|
1220
|
+
);
|
|
1221
|
+
};
|
|
1222
|
+
const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
|
|
1223
|
+
updateMenuPoint();
|
|
1224
|
+
});
|
|
1225
|
+
resizeObserver?.observe(textarea);
|
|
1226
|
+
textarea.addEventListener("scroll", updateMenuPoint, { passive: true });
|
|
1227
|
+
window.addEventListener("resize", updateMenuPoint);
|
|
1228
|
+
window.addEventListener("scroll", updateMenuPoint, {
|
|
1229
|
+
capture: true,
|
|
1230
|
+
passive: true
|
|
1231
|
+
});
|
|
1232
|
+
return () => {
|
|
1233
|
+
resizeObserver?.disconnect();
|
|
1234
|
+
textarea.removeEventListener("scroll", updateMenuPoint);
|
|
1235
|
+
window.removeEventListener("resize", updateMenuPoint);
|
|
1236
|
+
window.removeEventListener("scroll", updateMenuPoint, true);
|
|
1237
|
+
};
|
|
1238
|
+
}, [isMenuOpen, menuOffset, query]);
|
|
1239
|
+
const closeMenu = () => {
|
|
1240
|
+
setMatches([]);
|
|
1241
|
+
setActiveIndex(0);
|
|
1242
|
+
setIsLoading(false);
|
|
1243
|
+
setMenuPoint(null);
|
|
1244
|
+
};
|
|
1245
|
+
const setSelection = (nextSelection) => {
|
|
1246
|
+
pendingSelectionRef.current = nextSelection;
|
|
1247
|
+
setSelectionStart(nextSelection);
|
|
1248
|
+
if (textareaRef.current) {
|
|
1249
|
+
textareaRef.current.focus();
|
|
1250
|
+
textareaRef.current.setSelectionRange(nextSelection, nextSelection);
|
|
1251
|
+
pendingSelectionRef.current = null;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
const handleClickDecoration = (segment, event) => {
|
|
1255
|
+
event.preventDefault();
|
|
1256
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
1257
|
+
const nextSelection = event.clientX < bounds.left + bounds.width / 2 ? segment.from : segment.to;
|
|
1258
|
+
setSelection(nextSelection);
|
|
1259
|
+
};
|
|
1260
|
+
const handleRemoveDecoration = (segment) => {
|
|
1261
|
+
const nextValue = `${value.slice(0, segment.from)}${value.slice(segment.to)}`;
|
|
1262
|
+
const nextSelection = Math.min(segment.from, nextValue.length);
|
|
1263
|
+
pendingSelectionRef.current = nextSelection;
|
|
1264
|
+
onChange(nextValue);
|
|
1265
|
+
setSelectionStart(nextSelection);
|
|
1266
|
+
window.requestAnimationFrame(() => {
|
|
1267
|
+
if (!textareaRef.current) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
textareaRef.current.focus();
|
|
1271
|
+
textareaRef.current.setSelectionRange(nextSelection, nextSelection);
|
|
1272
|
+
pendingSelectionRef.current = null;
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1275
|
+
const applyMatch = (match) => {
|
|
1276
|
+
const currentQuery = findRichTextAtQuery(
|
|
1277
|
+
value,
|
|
1278
|
+
textareaRef.current?.selectionStart ?? selectionStart
|
|
1279
|
+
);
|
|
1280
|
+
if (!currentQuery) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const insertedValue = renderRichTextAtInsertResult(
|
|
1284
|
+
match.providerId,
|
|
1285
|
+
match.insertResult
|
|
1286
|
+
);
|
|
1287
|
+
const nextValue = `${value.slice(0, currentQuery.from)}${insertedValue}${value.slice(currentQuery.to)}`;
|
|
1288
|
+
const nextSelection = currentQuery.from + insertedValue.length;
|
|
1289
|
+
pendingSelectionRef.current = nextSelection;
|
|
1290
|
+
onChange(nextValue);
|
|
1291
|
+
closeMenu();
|
|
1292
|
+
};
|
|
1293
|
+
const handleKeyDown = (event) => {
|
|
1294
|
+
if (isRichTextImeComposing(event)) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (matches.length === 0) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (event.key === "ArrowDown") {
|
|
1301
|
+
event.preventDefault();
|
|
1302
|
+
setActiveIndex((current) => (current + 1) % matches.length);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (event.key === "ArrowUp") {
|
|
1306
|
+
event.preventDefault();
|
|
1307
|
+
setActiveIndex(
|
|
1308
|
+
(current) => (current - 1 + matches.length) % matches.length
|
|
1309
|
+
);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
1313
|
+
const match = matches[activeIndex];
|
|
1314
|
+
if (!match) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
event.preventDefault();
|
|
1318
|
+
applyMatch(match);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (event.key === "Escape") {
|
|
1322
|
+
event.preventDefault();
|
|
1323
|
+
closeMenu();
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
return /* @__PURE__ */ jsxs5("div", { className: cn3("relative min-w-0 w-full", className), children: [
|
|
1327
|
+
hasDecorations && textareaPresentationStyle ? /* @__PURE__ */ jsx5(
|
|
1328
|
+
RichTextTextareaDecoratedContent,
|
|
1329
|
+
{
|
|
1330
|
+
onClickSegment: handleClickDecoration,
|
|
1331
|
+
onRemoveSegment: handleRemoveDecoration,
|
|
1332
|
+
removeActionAriaLabel: text.removeReferenceActionLabel,
|
|
1333
|
+
scrollLeft: scrollPosition.left,
|
|
1334
|
+
scrollTop: scrollPosition.top,
|
|
1335
|
+
segments: decorationSegments,
|
|
1336
|
+
textareaStyle: textareaPresentationStyle
|
|
1337
|
+
}
|
|
1338
|
+
) : null,
|
|
1339
|
+
/* @__PURE__ */ jsx5(
|
|
1340
|
+
"textarea",
|
|
1341
|
+
{
|
|
1342
|
+
ref: textareaRef,
|
|
1343
|
+
className: textareaClassName,
|
|
1344
|
+
disabled,
|
|
1345
|
+
placeholder,
|
|
1346
|
+
rows,
|
|
1347
|
+
style: hasDecorations ? {
|
|
1348
|
+
WebkitTextFillColor: "transparent",
|
|
1349
|
+
caretColor: "var(--text-primary)",
|
|
1350
|
+
color: "transparent",
|
|
1351
|
+
position: "relative",
|
|
1352
|
+
zIndex: 0
|
|
1353
|
+
} : void 0,
|
|
1354
|
+
value,
|
|
1355
|
+
onBlur: () => {
|
|
1356
|
+
window.setTimeout(() => {
|
|
1357
|
+
setIsFocused(false);
|
|
1358
|
+
closeMenu();
|
|
1359
|
+
}, 100);
|
|
1360
|
+
},
|
|
1361
|
+
onChange: (event) => {
|
|
1362
|
+
const rawSelectionStart = event.target.selectionStart ?? 0;
|
|
1363
|
+
const nextSelection = resolveRichTextTextareaSelectionBoundary(
|
|
1364
|
+
decorationSegments,
|
|
1365
|
+
rawSelectionStart
|
|
1366
|
+
) ?? rawSelectionStart;
|
|
1367
|
+
if (nextSelection !== rawSelectionStart) {
|
|
1368
|
+
event.target.setSelectionRange(nextSelection, nextSelection);
|
|
1369
|
+
}
|
|
1370
|
+
setSelectionStart(nextSelection);
|
|
1371
|
+
onChange(event.target.value);
|
|
1372
|
+
},
|
|
1373
|
+
onFocus: (event) => {
|
|
1374
|
+
setIsFocused(true);
|
|
1375
|
+
const rawSelectionStart = event.target.selectionStart ?? 0;
|
|
1376
|
+
const nextSelection = resolveRichTextTextareaSelectionBoundary(
|
|
1377
|
+
decorationSegments,
|
|
1378
|
+
rawSelectionStart
|
|
1379
|
+
) ?? rawSelectionStart;
|
|
1380
|
+
if (nextSelection !== rawSelectionStart) {
|
|
1381
|
+
event.target.setSelectionRange(nextSelection, nextSelection);
|
|
1382
|
+
}
|
|
1383
|
+
setSelectionStart(nextSelection);
|
|
1384
|
+
setScrollPosition({
|
|
1385
|
+
left: event.target.scrollLeft,
|
|
1386
|
+
top: event.target.scrollTop
|
|
1387
|
+
});
|
|
1388
|
+
},
|
|
1389
|
+
onKeyDown: handleKeyDown,
|
|
1390
|
+
onScroll: (event) => {
|
|
1391
|
+
setScrollPosition({
|
|
1392
|
+
left: event.currentTarget.scrollLeft,
|
|
1393
|
+
top: event.currentTarget.scrollTop
|
|
1394
|
+
});
|
|
1395
|
+
},
|
|
1396
|
+
onSelect: (event) => {
|
|
1397
|
+
const rawSelectionStart = event.currentTarget.selectionStart ?? 0;
|
|
1398
|
+
const nextSelection = resolveRichTextTextareaSelectionBoundary(
|
|
1399
|
+
decorationSegments,
|
|
1400
|
+
rawSelectionStart
|
|
1401
|
+
) ?? rawSelectionStart;
|
|
1402
|
+
if (nextSelection !== rawSelectionStart) {
|
|
1403
|
+
event.currentTarget.setSelectionRange(nextSelection, nextSelection);
|
|
1404
|
+
}
|
|
1405
|
+
setSelectionStart(nextSelection);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
),
|
|
1409
|
+
overlay,
|
|
1410
|
+
isMenuOpen && menuPoint ? /* @__PURE__ */ jsx5(
|
|
1411
|
+
ViewportMenuSurface2,
|
|
1412
|
+
{
|
|
1413
|
+
open: true,
|
|
1414
|
+
className: "nextop-rich-text-at-menu max-h-64 w-[min(28rem,calc(100vw-24px))] overflow-y-auto p-1",
|
|
1415
|
+
dismissIgnoreRefs: [textareaRef],
|
|
1416
|
+
placement: {
|
|
1417
|
+
type: "point",
|
|
1418
|
+
point: menuPoint,
|
|
1419
|
+
alignX: "start",
|
|
1420
|
+
alignY: "start",
|
|
1421
|
+
estimatedSize: {
|
|
1422
|
+
width: 360,
|
|
1423
|
+
height: 256
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
children: matches.length > 0 ? matches.map((match, index) => /* @__PURE__ */ jsxs5(
|
|
1427
|
+
"button",
|
|
1428
|
+
{
|
|
1429
|
+
"aria-selected": index === activeIndex,
|
|
1430
|
+
className: cn3(
|
|
1431
|
+
"flex w-full cursor-pointer flex-col items-start gap-0.5 rounded-md px-2.5 py-2 text-left outline-none transition-colors",
|
|
1432
|
+
index === activeIndex ? "bg-transparency-block text-[var(--text-primary)]" : "text-[var(--text-primary)] hover:bg-transparency-block"
|
|
1433
|
+
),
|
|
1434
|
+
type: "button",
|
|
1435
|
+
onMouseDown: (event) => {
|
|
1436
|
+
event.preventDefault();
|
|
1437
|
+
applyMatch(match);
|
|
1438
|
+
},
|
|
1439
|
+
children: [
|
|
1440
|
+
/* @__PURE__ */ jsx5("div", { className: "text-sm leading-5 font-medium", children: match.label }),
|
|
1441
|
+
match.subtitle ? /* @__PURE__ */ jsx5("div", { className: "text-xs leading-4 text-[var(--text-secondary)]", children: match.subtitle }) : null
|
|
1442
|
+
]
|
|
1443
|
+
},
|
|
1444
|
+
`${match.providerId}:${match.key}`
|
|
1445
|
+
)) : /* @__PURE__ */ jsx5("div", { className: "px-3 py-2 text-xs leading-4 text-[var(--text-secondary)]", children: isLoading ? text.loadingLabel : text.noMatchesLabel })
|
|
1446
|
+
}
|
|
1447
|
+
) : null
|
|
1448
|
+
] });
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/editor/RichTextMentionReadonly.tsx
|
|
1452
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1453
|
+
var baseStyle = {
|
|
1454
|
+
alignItems: "center",
|
|
1455
|
+
border: "1px solid transparent",
|
|
1456
|
+
borderRadius: "999px",
|
|
1457
|
+
display: "inline-flex",
|
|
1458
|
+
fontSize: "0.95em",
|
|
1459
|
+
gap: "0.25rem",
|
|
1460
|
+
lineHeight: 1.4,
|
|
1461
|
+
maxWidth: "100%",
|
|
1462
|
+
padding: "0.05rem 0.45rem",
|
|
1463
|
+
textDecoration: "none",
|
|
1464
|
+
verticalAlign: "baseline",
|
|
1465
|
+
whiteSpace: "nowrap"
|
|
1466
|
+
};
|
|
1467
|
+
var stateStyles = {
|
|
1468
|
+
active: {
|
|
1469
|
+
background: "var(--nextop-rich-text-mention-active-bg, color-mix(in srgb, currentColor 12%, transparent))",
|
|
1470
|
+
color: "var(--nextop-rich-text-mention-active-fg, inherit)",
|
|
1471
|
+
cursor: "pointer"
|
|
1472
|
+
},
|
|
1473
|
+
missing: {
|
|
1474
|
+
background: "var(--nextop-rich-text-mention-missing-bg, color-mix(in srgb, currentColor 6%, transparent))",
|
|
1475
|
+
color: "var(--nextop-rich-text-mention-missing-fg, color-mix(in srgb, currentColor 48%, transparent))",
|
|
1476
|
+
cursor: "default",
|
|
1477
|
+
textDecoration: "line-through"
|
|
1478
|
+
},
|
|
1479
|
+
disabled: {
|
|
1480
|
+
background: "var(--nextop-rich-text-mention-disabled-bg, color-mix(in srgb, currentColor 6%, transparent))",
|
|
1481
|
+
color: "var(--nextop-rich-text-mention-disabled-fg, color-mix(in srgb, currentColor 58%, transparent))",
|
|
1482
|
+
cursor: "not-allowed",
|
|
1483
|
+
opacity: 0.88
|
|
1484
|
+
},
|
|
1485
|
+
loading: {
|
|
1486
|
+
background: "var(--nextop-rich-text-mention-loading-bg, color-mix(in srgb, currentColor 8%, transparent))",
|
|
1487
|
+
color: "var(--nextop-rich-text-mention-loading-fg, color-mix(in srgb, currentColor 82%, transparent))",
|
|
1488
|
+
cursor: "progress",
|
|
1489
|
+
opacity: 0.92
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
function joinClassNames(...parts) {
|
|
1493
|
+
const value = parts.filter(Boolean).join(" ").trim();
|
|
1494
|
+
return value || void 0;
|
|
1495
|
+
}
|
|
1496
|
+
function RichTextMentionReadonly({
|
|
1497
|
+
mention,
|
|
1498
|
+
resolved,
|
|
1499
|
+
className,
|
|
1500
|
+
title,
|
|
1501
|
+
onClick,
|
|
1502
|
+
renderLabel
|
|
1503
|
+
}) {
|
|
1504
|
+
const view = resolveRichTextMentionView(mention, resolved);
|
|
1505
|
+
const payload = {
|
|
1506
|
+
mention,
|
|
1507
|
+
resolved: view
|
|
1508
|
+
};
|
|
1509
|
+
const label = renderLabel?.(payload) ?? getRichTextMentionDisplayText({
|
|
1510
|
+
...mention,
|
|
1511
|
+
label: view.label
|
|
1512
|
+
});
|
|
1513
|
+
const elementTitle = title ?? view.tooltip;
|
|
1514
|
+
const handleClick = (event) => {
|
|
1515
|
+
if (!view.interactive) {
|
|
1516
|
+
event.preventDefault();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
onClick?.(payload);
|
|
1520
|
+
};
|
|
1521
|
+
const sharedProps = {
|
|
1522
|
+
"aria-busy": view.state === "loading" || void 0,
|
|
1523
|
+
"aria-disabled": !view.interactive || void 0,
|
|
1524
|
+
className: joinClassNames(
|
|
1525
|
+
"nextop-rich-text-mention",
|
|
1526
|
+
`nextop-rich-text-mention--${view.state}`,
|
|
1527
|
+
className
|
|
1528
|
+
),
|
|
1529
|
+
"data-plugin": mention.plugin,
|
|
1530
|
+
"data-state": view.state,
|
|
1531
|
+
style: {
|
|
1532
|
+
...baseStyle,
|
|
1533
|
+
...stateStyles[view.state]
|
|
1534
|
+
},
|
|
1535
|
+
title: elementTitle
|
|
1536
|
+
};
|
|
1537
|
+
if (view.interactive && view.href) {
|
|
1538
|
+
return /* @__PURE__ */ jsx6(
|
|
1539
|
+
"a",
|
|
1540
|
+
{
|
|
1541
|
+
...sharedProps,
|
|
1542
|
+
href: view.href,
|
|
1543
|
+
onClick: (event) => {
|
|
1544
|
+
handleClick(event);
|
|
1545
|
+
},
|
|
1546
|
+
children: label
|
|
1547
|
+
}
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
if (view.interactive && onClick) {
|
|
1551
|
+
return /* @__PURE__ */ jsx6(
|
|
1552
|
+
"button",
|
|
1553
|
+
{
|
|
1554
|
+
...sharedProps,
|
|
1555
|
+
onClick: (event) => {
|
|
1556
|
+
handleClick(event);
|
|
1557
|
+
},
|
|
1558
|
+
style: {
|
|
1559
|
+
...sharedProps.style,
|
|
1560
|
+
appearance: "none",
|
|
1561
|
+
font: "inherit"
|
|
1562
|
+
},
|
|
1563
|
+
type: "button",
|
|
1564
|
+
children: label
|
|
1565
|
+
}
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
return /* @__PURE__ */ jsx6("span", { ...sharedProps, children: label });
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/editor/RichTextReadonlyContent.tsx
|
|
1572
|
+
import {
|
|
1573
|
+
MentionPill as MentionPill3,
|
|
1574
|
+
Tooltip as Tooltip2,
|
|
1575
|
+
TooltipContent as TooltipContent2,
|
|
1576
|
+
TooltipTrigger as TooltipTrigger2
|
|
1577
|
+
} from "@tutti-os/ui-system/components";
|
|
1578
|
+
import { cn as cn4 } from "@tutti-os/ui-system/utils";
|
|
1579
|
+
|
|
1580
|
+
// src/editor/richTextReadonlyContentModel.ts
|
|
1581
|
+
function buildRichTextReadonlyInlineSegments(content) {
|
|
1582
|
+
const segments = [];
|
|
1583
|
+
let cursor = 0;
|
|
1584
|
+
for (const match of findRichTextMarkdownLinks(content)) {
|
|
1585
|
+
if (match.index > cursor) {
|
|
1586
|
+
segments.push({
|
|
1587
|
+
text: content.slice(cursor, match.index),
|
|
1588
|
+
type: "text"
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
segments.push({
|
|
1592
|
+
href: match.href,
|
|
1593
|
+
label: match.label || match.href,
|
|
1594
|
+
type: "link"
|
|
1595
|
+
});
|
|
1596
|
+
cursor = match.to;
|
|
1597
|
+
}
|
|
1598
|
+
if (cursor < content.length) {
|
|
1599
|
+
segments.push({
|
|
1600
|
+
text: content.slice(cursor),
|
|
1601
|
+
type: "text"
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
return segments;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/editor/RichTextReadonlyContent.tsx
|
|
1608
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1609
|
+
var externalHrefPattern = /^(?:[a-z]+:)?\/\//i;
|
|
1610
|
+
var richTextWorkspaceReferencePillClassName3 = "max-w-[18rem]";
|
|
1611
|
+
function RichTextReadonlyContent({
|
|
1612
|
+
value,
|
|
1613
|
+
className,
|
|
1614
|
+
paragraphClassName,
|
|
1615
|
+
onOpenWorkspaceReference
|
|
1616
|
+
}) {
|
|
1617
|
+
const normalizedValue = normalizeRichTextContent(value).trim();
|
|
1618
|
+
if (!normalizedValue) {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
const paragraphs = normalizedValue.split(/\n{2,}/).map((paragraph) => paragraph.trim()).filter(Boolean);
|
|
1622
|
+
return /* @__PURE__ */ jsx7("div", { className: cn4("space-y-4", className), children: paragraphs.map((paragraph, paragraphIndex) => /* @__PURE__ */ jsx7(
|
|
1623
|
+
"p",
|
|
1624
|
+
{
|
|
1625
|
+
className: cn4("whitespace-pre-wrap", paragraphClassName),
|
|
1626
|
+
children: renderReadonlyInlineMarkdown(paragraph, onOpenWorkspaceReference)
|
|
1627
|
+
},
|
|
1628
|
+
`${paragraphIndex}:${paragraph}`
|
|
1629
|
+
)) });
|
|
1630
|
+
}
|
|
1631
|
+
function renderReadonlyInlineMarkdown(content, onOpenWorkspaceReference) {
|
|
1632
|
+
const parts = [];
|
|
1633
|
+
const segments = buildRichTextReadonlyInlineSegments(content);
|
|
1634
|
+
segments.forEach((segment, index) => {
|
|
1635
|
+
if (segment.type === "text") {
|
|
1636
|
+
parts.push(/* @__PURE__ */ jsx7("span", { children: segment.text }, `text:${index}`));
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
parts.push(
|
|
1640
|
+
/* @__PURE__ */ jsx7(
|
|
1641
|
+
RichTextReadonlyInlineLink,
|
|
1642
|
+
{
|
|
1643
|
+
href: segment.href,
|
|
1644
|
+
label: segment.label,
|
|
1645
|
+
onOpenWorkspaceReference
|
|
1646
|
+
},
|
|
1647
|
+
`link:${index}:${segment.href}`
|
|
1648
|
+
)
|
|
1649
|
+
);
|
|
1650
|
+
});
|
|
1651
|
+
return parts;
|
|
1652
|
+
}
|
|
1653
|
+
function RichTextReadonlyInlineLink({
|
|
1654
|
+
href,
|
|
1655
|
+
label,
|
|
1656
|
+
onOpenWorkspaceReference
|
|
1657
|
+
}) {
|
|
1658
|
+
const trimmedHref = href.trim();
|
|
1659
|
+
const mention = parseRichTextMentionHref(trimmedHref, label);
|
|
1660
|
+
if (mention) {
|
|
1661
|
+
return /* @__PURE__ */ jsx7(RichTextMentionReadonly, { mention });
|
|
1662
|
+
}
|
|
1663
|
+
if (!trimmedHref) {
|
|
1664
|
+
return /* @__PURE__ */ jsx7("span", { children: label });
|
|
1665
|
+
}
|
|
1666
|
+
if (externalHrefPattern.test(trimmedHref) && !isRichTextMentionHref(trimmedHref)) {
|
|
1667
|
+
return /* @__PURE__ */ jsx7(
|
|
1668
|
+
"a",
|
|
1669
|
+
{
|
|
1670
|
+
className: "font-medium text-[var(--text-primary)] underline decoration-[var(--border-1)] underline-offset-4 hover:text-[var(--text-primary-hover)]",
|
|
1671
|
+
href: trimmedHref,
|
|
1672
|
+
rel: "noreferrer",
|
|
1673
|
+
target: "_blank",
|
|
1674
|
+
children: label
|
|
1675
|
+
}
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
const kind = trimmedHref.endsWith("/") ? "folder" : "file";
|
|
1679
|
+
const path = normalizeRichTextLinkHref(trimmedHref, kind);
|
|
1680
|
+
return /* @__PURE__ */ jsx7(
|
|
1681
|
+
WorkspaceReferenceReadonly,
|
|
1682
|
+
{
|
|
1683
|
+
reference: {
|
|
1684
|
+
kind,
|
|
1685
|
+
label,
|
|
1686
|
+
path
|
|
1687
|
+
},
|
|
1688
|
+
onOpenWorkspaceReference
|
|
1689
|
+
}
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
function WorkspaceReferenceReadonly({
|
|
1693
|
+
reference,
|
|
1694
|
+
onOpenWorkspaceReference
|
|
1695
|
+
}) {
|
|
1696
|
+
const presentation = getWorkspaceReferencePresentation(
|
|
1697
|
+
reference.label,
|
|
1698
|
+
reference.path
|
|
1699
|
+
);
|
|
1700
|
+
const content = /* @__PURE__ */ jsx7(
|
|
1701
|
+
MentionPill3,
|
|
1702
|
+
{
|
|
1703
|
+
className: richTextWorkspaceReferencePillClassName3,
|
|
1704
|
+
fileKind: reference.kind,
|
|
1705
|
+
kind: "file",
|
|
1706
|
+
label: presentation.displayLabel,
|
|
1707
|
+
removable: false
|
|
1708
|
+
}
|
|
1709
|
+
);
|
|
1710
|
+
return /* @__PURE__ */ jsxs6(Tooltip2, { children: [
|
|
1711
|
+
/* @__PURE__ */ jsx7(TooltipTrigger2, { asChild: true, children: onOpenWorkspaceReference ? /* @__PURE__ */ jsx7(
|
|
1712
|
+
"button",
|
|
1713
|
+
{
|
|
1714
|
+
className: "inline-flex max-w-full appearance-none items-baseline bg-transparent p-0 text-inherit",
|
|
1715
|
+
type: "button",
|
|
1716
|
+
onClick: () => {
|
|
1717
|
+
void onOpenWorkspaceReference(reference);
|
|
1718
|
+
},
|
|
1719
|
+
children: content
|
|
1720
|
+
}
|
|
1721
|
+
) : /* @__PURE__ */ jsx7("span", { className: "inline-flex max-w-full align-baseline", children: content }) }),
|
|
1722
|
+
/* @__PURE__ */ jsx7(
|
|
1723
|
+
TooltipContent2,
|
|
1724
|
+
{
|
|
1725
|
+
className: "max-w-md whitespace-normal break-all",
|
|
1726
|
+
sideOffset: 8,
|
|
1727
|
+
children: presentation.fullPath
|
|
1728
|
+
}
|
|
1729
|
+
)
|
|
1730
|
+
] });
|
|
1731
|
+
}
|
|
1732
|
+
export {
|
|
1733
|
+
RichTextAtEditor,
|
|
1734
|
+
RichTextAtTextarea,
|
|
1735
|
+
RichTextMentionReadonly,
|
|
1736
|
+
RichTextReadonlyContent
|
|
1737
|
+
};
|
|
1738
|
+
//# sourceMappingURL=index.js.map
|