composeai 0.1.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/README.md +265 -0
- package/dist/index.cjs +4750 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.js +4747 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/composer.css +481 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4750 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var LexicalComposer = require('@lexical/react/LexicalComposer');
|
|
5
|
+
var LexicalComposerContext = require('@lexical/react/LexicalComposerContext');
|
|
6
|
+
var lexical = require('lexical');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
var LexicalRichTextPlugin = require('@lexical/react/LexicalRichTextPlugin');
|
|
9
|
+
var LexicalPlainTextPlugin = require('@lexical/react/LexicalPlainTextPlugin');
|
|
10
|
+
var LexicalContentEditable = require('@lexical/react/LexicalContentEditable');
|
|
11
|
+
var LexicalErrorBoundary = require('@lexical/react/LexicalErrorBoundary');
|
|
12
|
+
var LexicalHistoryPlugin = require('@lexical/react/LexicalHistoryPlugin');
|
|
13
|
+
var reactDom = require('react-dom');
|
|
14
|
+
var LexicalTypeaheadMenuPlugin = require('@lexical/react/LexicalTypeaheadMenuPlugin');
|
|
15
|
+
|
|
16
|
+
// src/Composer.tsx
|
|
17
|
+
function normalize(text) {
|
|
18
|
+
return text.replace(/\r\n?/g, "\n");
|
|
19
|
+
}
|
|
20
|
+
function $insertTextWithParagraphBreaks(text) {
|
|
21
|
+
const lines = normalize(text).split("\n");
|
|
22
|
+
const current = () => {
|
|
23
|
+
const s = lexical.$getSelection();
|
|
24
|
+
return lexical.$isRangeSelection(s) ? s : null;
|
|
25
|
+
};
|
|
26
|
+
let sel = current();
|
|
27
|
+
if (!sel) {
|
|
28
|
+
const root = lexical.$getRoot();
|
|
29
|
+
const last = root.getLastChild();
|
|
30
|
+
if (last && "selectEnd" in last && typeof last.selectEnd === "function") {
|
|
31
|
+
last.selectEnd();
|
|
32
|
+
} else {
|
|
33
|
+
root.selectEnd();
|
|
34
|
+
}
|
|
35
|
+
sel = current();
|
|
36
|
+
if (!sel) return;
|
|
37
|
+
}
|
|
38
|
+
if (!sel.isCollapsed()) {
|
|
39
|
+
sel.removeText();
|
|
40
|
+
sel = current();
|
|
41
|
+
if (!sel) return;
|
|
42
|
+
}
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
if (i > 0) {
|
|
45
|
+
sel.insertParagraph();
|
|
46
|
+
sel = current();
|
|
47
|
+
if (!sel) return;
|
|
48
|
+
}
|
|
49
|
+
if (lines[i].length > 0) {
|
|
50
|
+
sel.insertText(lines[i]);
|
|
51
|
+
sel = current();
|
|
52
|
+
if (!sel) return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function $seedInitialValue(text) {
|
|
57
|
+
const root = lexical.$getRoot();
|
|
58
|
+
root.clear();
|
|
59
|
+
const lines = normalize(text).split("\n");
|
|
60
|
+
if (lines.length === 0) {
|
|
61
|
+
const para = lexical.$createParagraphNode();
|
|
62
|
+
root.append(para);
|
|
63
|
+
para.selectEnd();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let last = null;
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const para = lexical.$createParagraphNode();
|
|
69
|
+
if (line.length > 0) para.append(lexical.$createTextNode(line));
|
|
70
|
+
root.append(para);
|
|
71
|
+
last = para;
|
|
72
|
+
}
|
|
73
|
+
last?.selectEnd();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/internal/cn.ts
|
|
77
|
+
function cn(...inputs) {
|
|
78
|
+
const out = [];
|
|
79
|
+
walk(inputs, out);
|
|
80
|
+
return out.join(" ");
|
|
81
|
+
}
|
|
82
|
+
function walk(value, out) {
|
|
83
|
+
if (!value) return;
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
if (value.length > 0) out.push(value);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "number") {
|
|
89
|
+
out.push(String(value));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
for (const v of value) walk(v, out);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "object") {
|
|
97
|
+
for (const key in value) {
|
|
98
|
+
if (value[key]) out.push(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/internal/color.ts
|
|
104
|
+
var COMPONENT_RE = /^\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%\s*$/;
|
|
105
|
+
var HSL_FN_RE = /^\s*hsla?\(\s*(\d+(?:\.\d+)?)(?:deg)?\s*[,\s]\s*(\d+(?:\.\d+)?)%\s*[,\s]\s*(\d+(?:\.\d+)?)%/i;
|
|
106
|
+
var HEX_RE = /^\s*#([0-9a-f]{3}|[0-9a-f]{6})\s*$/i;
|
|
107
|
+
var RGB_FN_RE = /^\s*rgba?\(\s*(\d+(?:\.\d+)?)\s*[,\s]\s*(\d+(?:\.\d+)?)\s*[,\s]\s*(\d+(?:\.\d+)?)/i;
|
|
108
|
+
function parseToHsl(value) {
|
|
109
|
+
if (typeof value !== "string") return null;
|
|
110
|
+
const components = value.match(COMPONENT_RE);
|
|
111
|
+
if (components) {
|
|
112
|
+
return {
|
|
113
|
+
h: parseFloat(components[1]),
|
|
114
|
+
s: parseFloat(components[2]),
|
|
115
|
+
l: parseFloat(components[3])
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const hslFn = value.match(HSL_FN_RE);
|
|
119
|
+
if (hslFn) {
|
|
120
|
+
return {
|
|
121
|
+
h: parseFloat(hslFn[1]),
|
|
122
|
+
s: parseFloat(hslFn[2]),
|
|
123
|
+
l: parseFloat(hslFn[3])
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const hex = value.match(HEX_RE);
|
|
127
|
+
if (hex) {
|
|
128
|
+
let h = hex[1];
|
|
129
|
+
if (h.length === 3) {
|
|
130
|
+
h = h.split("").map((c) => c + c).join("");
|
|
131
|
+
}
|
|
132
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
133
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
134
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
135
|
+
return rgbToHsl(r, g, b);
|
|
136
|
+
}
|
|
137
|
+
const rgbFn = value.match(RGB_FN_RE);
|
|
138
|
+
if (rgbFn) {
|
|
139
|
+
return rgbToHsl(
|
|
140
|
+
clamp01(parseFloat(rgbFn[1]) / 255),
|
|
141
|
+
clamp01(parseFloat(rgbFn[2]) / 255),
|
|
142
|
+
clamp01(parseFloat(rgbFn[3]) / 255)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function clamp01(n) {
|
|
148
|
+
return Math.min(1, Math.max(0, n));
|
|
149
|
+
}
|
|
150
|
+
function clampPct(n) {
|
|
151
|
+
return Math.min(100, Math.max(0, n));
|
|
152
|
+
}
|
|
153
|
+
function rgbToHsl(r, g, b) {
|
|
154
|
+
const max = Math.max(r, g, b);
|
|
155
|
+
const min = Math.min(r, g, b);
|
|
156
|
+
const l = (max + min) / 2;
|
|
157
|
+
let h = 0;
|
|
158
|
+
let s = 0;
|
|
159
|
+
if (max !== min) {
|
|
160
|
+
const d = max - min;
|
|
161
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
162
|
+
switch (max) {
|
|
163
|
+
case r:
|
|
164
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
165
|
+
break;
|
|
166
|
+
case g:
|
|
167
|
+
h = (b - r) / d + 2;
|
|
168
|
+
break;
|
|
169
|
+
case b:
|
|
170
|
+
h = (r - g) / d + 4;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
h /= 6;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
h: Math.round(h * 360),
|
|
177
|
+
s: Math.round(s * 100),
|
|
178
|
+
l: Math.round(l * 100)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function formatHslComponents(triple) {
|
|
182
|
+
return `${triple.h} ${triple.s}% ${triple.l}%`;
|
|
183
|
+
}
|
|
184
|
+
function deriveColorTokens(value) {
|
|
185
|
+
const base = parseToHsl(value);
|
|
186
|
+
if (!base) return null;
|
|
187
|
+
const isLightBase = base.l >= 50;
|
|
188
|
+
const primary = formatHslComponents(base);
|
|
189
|
+
const primaryForeground = formatHslComponents({
|
|
190
|
+
h: base.h,
|
|
191
|
+
s: Math.min(base.s, 30),
|
|
192
|
+
l: isLightBase ? 10 : 98
|
|
193
|
+
});
|
|
194
|
+
const accent = formatHslComponents({
|
|
195
|
+
h: base.h,
|
|
196
|
+
s: clampPct(Math.min(base.s, 70)),
|
|
197
|
+
l: 95
|
|
198
|
+
});
|
|
199
|
+
const accentForeground = formatHslComponents({
|
|
200
|
+
h: base.h,
|
|
201
|
+
s: clampPct(Math.max(base.s, 40)),
|
|
202
|
+
l: 30
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
primary,
|
|
206
|
+
primaryForeground,
|
|
207
|
+
accent,
|
|
208
|
+
accentForeground,
|
|
209
|
+
ring: primary
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/internal/sx.ts
|
|
214
|
+
var COLOR_TOKENS = {
|
|
215
|
+
primary: "var(--primary)",
|
|
216
|
+
"primary-foreground": "var(--primary-foreground)",
|
|
217
|
+
primaryForeground: "var(--primary-foreground)",
|
|
218
|
+
accent: "var(--accent)",
|
|
219
|
+
"accent-foreground": "var(--accent-foreground)",
|
|
220
|
+
accentForeground: "var(--accent-foreground)",
|
|
221
|
+
background: "var(--background)",
|
|
222
|
+
foreground: "var(--foreground)",
|
|
223
|
+
card: "var(--card)",
|
|
224
|
+
"card-foreground": "var(--card-foreground)",
|
|
225
|
+
cardForeground: "var(--card-foreground)",
|
|
226
|
+
popover: "var(--popover)",
|
|
227
|
+
"popover-foreground": "var(--popover-foreground)",
|
|
228
|
+
popoverForeground: "var(--popover-foreground)",
|
|
229
|
+
muted: "var(--muted)",
|
|
230
|
+
"muted-foreground": "var(--muted-foreground)",
|
|
231
|
+
mutedForeground: "var(--muted-foreground)",
|
|
232
|
+
border: "var(--border)",
|
|
233
|
+
ring: "var(--ring)",
|
|
234
|
+
input: "var(--input)",
|
|
235
|
+
destructive: "var(--destructive)",
|
|
236
|
+
"destructive-foreground": "var(--destructive-foreground)",
|
|
237
|
+
destructiveForeground: "var(--destructive-foreground)",
|
|
238
|
+
success: "var(--success)",
|
|
239
|
+
"success-foreground": "var(--success-foreground)",
|
|
240
|
+
successForeground: "var(--success-foreground)",
|
|
241
|
+
warning: "var(--warning)",
|
|
242
|
+
"warning-foreground": "var(--warning-foreground)",
|
|
243
|
+
warningForeground: "var(--warning-foreground)"
|
|
244
|
+
};
|
|
245
|
+
var COLOR_KEYS = /* @__PURE__ */ new Set([
|
|
246
|
+
"color",
|
|
247
|
+
"backgroundColor",
|
|
248
|
+
"borderColor",
|
|
249
|
+
"borderTopColor",
|
|
250
|
+
"borderRightColor",
|
|
251
|
+
"borderBottomColor",
|
|
252
|
+
"borderLeftColor",
|
|
253
|
+
"outlineColor",
|
|
254
|
+
"fill",
|
|
255
|
+
"stroke",
|
|
256
|
+
"caretColor",
|
|
257
|
+
"textDecorationColor"
|
|
258
|
+
]);
|
|
259
|
+
function expandColor(value) {
|
|
260
|
+
if (typeof value !== "string") return value;
|
|
261
|
+
const trimmed = value.trim();
|
|
262
|
+
const tokenVar = COLOR_TOKENS[trimmed];
|
|
263
|
+
if (!tokenVar) return value;
|
|
264
|
+
return `hsl(${tokenVar})`;
|
|
265
|
+
}
|
|
266
|
+
function resolveSx(value) {
|
|
267
|
+
if (!value) return void 0;
|
|
268
|
+
const out = {};
|
|
269
|
+
for (const key in value) {
|
|
270
|
+
const raw = value[key];
|
|
271
|
+
if (raw === void 0) continue;
|
|
272
|
+
if (key === "bg") {
|
|
273
|
+
out.backgroundColor = expandColor(raw);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (COLOR_KEYS.has(key)) {
|
|
277
|
+
out[key] = expandColor(raw);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
out[key] = raw;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
function slotProps(slot, base, classNames, sx, baseStyle) {
|
|
285
|
+
const className = cn(base, classNames?.[slot]);
|
|
286
|
+
const resolved = resolveSx(sx?.[slot]);
|
|
287
|
+
if (!resolved && !baseStyle) {
|
|
288
|
+
return { className };
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
className,
|
|
292
|
+
style: { ...baseStyle, ...resolved }
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
var COLOR_TOKEN_KEYS = [
|
|
296
|
+
"primary",
|
|
297
|
+
"primaryForeground",
|
|
298
|
+
"accent",
|
|
299
|
+
"accentForeground",
|
|
300
|
+
"background",
|
|
301
|
+
"foreground",
|
|
302
|
+
"card",
|
|
303
|
+
"cardForeground",
|
|
304
|
+
"popover",
|
|
305
|
+
"popoverForeground",
|
|
306
|
+
"muted",
|
|
307
|
+
"mutedForeground",
|
|
308
|
+
"border",
|
|
309
|
+
"ring",
|
|
310
|
+
"input",
|
|
311
|
+
"destructive",
|
|
312
|
+
"destructiveForeground",
|
|
313
|
+
"success",
|
|
314
|
+
"successForeground",
|
|
315
|
+
"warning",
|
|
316
|
+
"warningForeground"
|
|
317
|
+
];
|
|
318
|
+
var COLOR_TOKEN_VAR = {
|
|
319
|
+
primary: "--primary",
|
|
320
|
+
primaryForeground: "--primary-foreground",
|
|
321
|
+
accent: "--accent",
|
|
322
|
+
accentForeground: "--accent-foreground",
|
|
323
|
+
background: "--background",
|
|
324
|
+
foreground: "--foreground",
|
|
325
|
+
card: "--card",
|
|
326
|
+
cardForeground: "--card-foreground",
|
|
327
|
+
popover: "--popover",
|
|
328
|
+
popoverForeground: "--popover-foreground",
|
|
329
|
+
muted: "--muted",
|
|
330
|
+
mutedForeground: "--muted-foreground",
|
|
331
|
+
border: "--border",
|
|
332
|
+
ring: "--ring",
|
|
333
|
+
input: "--input",
|
|
334
|
+
destructive: "--destructive",
|
|
335
|
+
destructiveForeground: "--destructive-foreground",
|
|
336
|
+
success: "--success",
|
|
337
|
+
successForeground: "--success-foreground",
|
|
338
|
+
warning: "--warning",
|
|
339
|
+
warningForeground: "--warning-foreground"
|
|
340
|
+
};
|
|
341
|
+
function asLength(value) {
|
|
342
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
343
|
+
}
|
|
344
|
+
function tokensToStyle(tokens) {
|
|
345
|
+
if (!tokens) return void 0;
|
|
346
|
+
const out = {};
|
|
347
|
+
for (const key of COLOR_TOKEN_KEYS) {
|
|
348
|
+
const v = tokens[key];
|
|
349
|
+
if (typeof v === "string" && v.length > 0) {
|
|
350
|
+
out[COLOR_TOKEN_VAR[key]] = v;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (tokens.radius !== void 0) out["--composer-radius"] = asLength(tokens.radius);
|
|
354
|
+
if (tokens.fontSize !== void 0) out["--composer-font-size"] = asLength(tokens.fontSize);
|
|
355
|
+
if (tokens.fontFamily !== void 0) out["--composer-font-family"] = tokens.fontFamily;
|
|
356
|
+
return Object.keys(out).length ? out : void 0;
|
|
357
|
+
}
|
|
358
|
+
function makeIcon(displayName, paths) {
|
|
359
|
+
const Icon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
360
|
+
"svg",
|
|
361
|
+
{
|
|
362
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
363
|
+
width: "24",
|
|
364
|
+
height: "24",
|
|
365
|
+
viewBox: "0 0 24 24",
|
|
366
|
+
fill: "none",
|
|
367
|
+
stroke: "currentColor",
|
|
368
|
+
strokeWidth: 2,
|
|
369
|
+
strokeLinecap: "round",
|
|
370
|
+
strokeLinejoin: "round",
|
|
371
|
+
"aria-hidden": "true",
|
|
372
|
+
focusable: "false",
|
|
373
|
+
...props,
|
|
374
|
+
children: paths
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
Icon.displayName = displayName;
|
|
378
|
+
return Icon;
|
|
379
|
+
}
|
|
380
|
+
var IconSend = makeIcon(
|
|
381
|
+
"IconSend",
|
|
382
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
383
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 5v14" }),
|
|
384
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m19 12-7-7-7 7" })
|
|
385
|
+
] })
|
|
386
|
+
);
|
|
387
|
+
var IconStop = makeIcon(
|
|
388
|
+
"IconStop",
|
|
389
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { width: "14", height: "14", x: "5", y: "5", rx: "2" })
|
|
390
|
+
);
|
|
391
|
+
var IconAttach = makeIcon(
|
|
392
|
+
"IconAttach",
|
|
393
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
394
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M13.234 20.252 21 12.3" }),
|
|
395
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m16 6-8.414 8.586a2 2 0 0 0 0 2.828 2 2 0 0 0 2.828 0l8.414-8.586a4 4 0 0 0 0-5.656 4 4 0 0 0-5.656 0l-8.415 8.585a6 6 0 1 0 8.486 8.486" })
|
|
396
|
+
] })
|
|
397
|
+
);
|
|
398
|
+
var IconImage = makeIcon(
|
|
399
|
+
"IconImage",
|
|
400
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
401
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
|
|
402
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "9", r: "2" }),
|
|
403
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
|
|
404
|
+
] })
|
|
405
|
+
);
|
|
406
|
+
var IconVoice = makeIcon(
|
|
407
|
+
"IconVoice",
|
|
408
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
409
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
|
|
410
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
411
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
|
|
412
|
+
] })
|
|
413
|
+
);
|
|
414
|
+
var IconVoiceRecording = makeIcon(
|
|
415
|
+
"IconVoiceRecording",
|
|
416
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
|
|
417
|
+
);
|
|
418
|
+
var IconWeb = makeIcon(
|
|
419
|
+
"IconWeb",
|
|
420
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
421
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
422
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
|
|
423
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 12h20" })
|
|
424
|
+
] })
|
|
425
|
+
);
|
|
426
|
+
var IconClose = makeIcon(
|
|
427
|
+
"IconClose",
|
|
428
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
429
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18" }),
|
|
430
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 6 12 12" })
|
|
431
|
+
] })
|
|
432
|
+
);
|
|
433
|
+
var IconZoom = makeIcon(
|
|
434
|
+
"IconZoom",
|
|
435
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
436
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "8" }),
|
|
437
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "21", x2: "16.65", y1: "21", y2: "16.65" }),
|
|
438
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "11", x2: "11", y1: "8", y2: "14" }),
|
|
439
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "8", x2: "14", y1: "11", y2: "11" })
|
|
440
|
+
] })
|
|
441
|
+
);
|
|
442
|
+
var IconFile = makeIcon(
|
|
443
|
+
"IconFile",
|
|
444
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
445
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }),
|
|
446
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "14 2 14 8 20 8" }),
|
|
447
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16", x2: "8", y1: "13", y2: "13" }),
|
|
448
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16", x2: "8", y1: "17", y2: "17" }),
|
|
449
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", x2: "8", y1: "9", y2: "9" })
|
|
450
|
+
] })
|
|
451
|
+
);
|
|
452
|
+
var IconAudio = makeIcon(
|
|
453
|
+
"IconAudio",
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
455
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 18V5l12-2v13" }),
|
|
456
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "6", cy: "18", r: "3" }),
|
|
457
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "18", cy: "16", r: "3" })
|
|
458
|
+
] })
|
|
459
|
+
);
|
|
460
|
+
var IconSparkle = makeIcon(
|
|
461
|
+
"IconSparkle",
|
|
462
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
463
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275Z" }),
|
|
464
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 3v4" }),
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 17v4" }),
|
|
466
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 5h4" }),
|
|
467
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17 19h4" })
|
|
468
|
+
] })
|
|
469
|
+
);
|
|
470
|
+
var IconSpinner = makeIcon(
|
|
471
|
+
"IconSpinner",
|
|
472
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
|
|
473
|
+
);
|
|
474
|
+
var IconWarning = makeIcon(
|
|
475
|
+
"IconWarning",
|
|
476
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
477
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
|
|
478
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 9v4" }),
|
|
479
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 17h.01" })
|
|
480
|
+
] })
|
|
481
|
+
);
|
|
482
|
+
var DEFAULT_ICONS = {
|
|
483
|
+
send: IconSend,
|
|
484
|
+
stop: IconStop,
|
|
485
|
+
attach: IconAttach,
|
|
486
|
+
image: IconImage,
|
|
487
|
+
voice: IconVoice,
|
|
488
|
+
voiceRecording: IconVoiceRecording,
|
|
489
|
+
web: IconWeb,
|
|
490
|
+
close: IconClose,
|
|
491
|
+
zoom: IconZoom,
|
|
492
|
+
file: IconFile,
|
|
493
|
+
audio: IconAudio,
|
|
494
|
+
sparkle: IconSparkle,
|
|
495
|
+
spinner: IconSpinner,
|
|
496
|
+
warning: IconWarning
|
|
497
|
+
};
|
|
498
|
+
function resolveIcons(overrides) {
|
|
499
|
+
if (!overrides) return DEFAULT_ICONS;
|
|
500
|
+
return { ...DEFAULT_ICONS, ...overrides };
|
|
501
|
+
}
|
|
502
|
+
var ComposerContext = react.createContext(null);
|
|
503
|
+
var DEFAULT_FEATURES = {
|
|
504
|
+
markdown: true,
|
|
505
|
+
attachments: true,
|
|
506
|
+
mentions: false,
|
|
507
|
+
slashCommands: false,
|
|
508
|
+
voice: true,
|
|
509
|
+
mermaid: true,
|
|
510
|
+
web: true,
|
|
511
|
+
// Off by default — ghost autocomplete is an opt-in input affordance that
|
|
512
|
+
// only makes sense when the consumer has a curated list of completions.
|
|
513
|
+
ghostedAutoComplete: false
|
|
514
|
+
};
|
|
515
|
+
var DEFAULT_ATTACHMENTS = {
|
|
516
|
+
// Show only the generic paperclip by default — its `accept` string already
|
|
517
|
+
// includes images, so users can still attach photos via the OS dialog.
|
|
518
|
+
// The dedicated image button is opt-in (`attachments: { image: true }`),
|
|
519
|
+
// primarily for chat-heavy apps where jumping straight to the mobile
|
|
520
|
+
// camera-roll picker is a UX win worth the second button.
|
|
521
|
+
file: true,
|
|
522
|
+
image: false,
|
|
523
|
+
accept: "image/*,application/pdf,text/*,audio/*,video/*",
|
|
524
|
+
// No type-picker popover by default — the paperclip opens the OS file
|
|
525
|
+
// picker directly. When the consumer supplies a non-empty `types` list,
|
|
526
|
+
// the paperclip flips into a small dropdown that lets the user pick a
|
|
527
|
+
// category first, scoping the OS dialog to that type's `accept`.
|
|
528
|
+
types: [],
|
|
529
|
+
maxSize: 25 * 1024 * 1024,
|
|
530
|
+
maxCount: 10
|
|
531
|
+
};
|
|
532
|
+
function normalizeFeatures(features) {
|
|
533
|
+
if (!features) return DEFAULT_FEATURES;
|
|
534
|
+
return {
|
|
535
|
+
markdown: features.markdown ?? DEFAULT_FEATURES.markdown,
|
|
536
|
+
attachments: features.attachments ?? DEFAULT_FEATURES.attachments,
|
|
537
|
+
mentions: features.mentions ?? DEFAULT_FEATURES.mentions,
|
|
538
|
+
slashCommands: features.slashCommands ?? DEFAULT_FEATURES.slashCommands,
|
|
539
|
+
voice: features.voice ?? DEFAULT_FEATURES.voice,
|
|
540
|
+
mermaid: features.mermaid ?? DEFAULT_FEATURES.mermaid,
|
|
541
|
+
web: features.web ?? DEFAULT_FEATURES.web,
|
|
542
|
+
ghostedAutoComplete: features.ghostedAutoComplete ?? DEFAULT_FEATURES.ghostedAutoComplete
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function detectKind(file) {
|
|
546
|
+
if (file.type.startsWith("image/")) return "image";
|
|
547
|
+
if (file.type.startsWith("audio/")) return "audio";
|
|
548
|
+
return "file";
|
|
549
|
+
}
|
|
550
|
+
var EMPTY_SLOTS = Object.freeze({});
|
|
551
|
+
function ComposerProvider({
|
|
552
|
+
features,
|
|
553
|
+
isStreaming,
|
|
554
|
+
closeMenusOnOutsideClick = true,
|
|
555
|
+
attachmentOptions,
|
|
556
|
+
mode = "markdown",
|
|
557
|
+
multiline = true,
|
|
558
|
+
submitOnEnter = true,
|
|
559
|
+
smartNewline = true,
|
|
560
|
+
focusShortcut = "mod+/",
|
|
561
|
+
icons,
|
|
562
|
+
slots,
|
|
563
|
+
renderDiagram,
|
|
564
|
+
dir,
|
|
565
|
+
classNames,
|
|
566
|
+
sx,
|
|
567
|
+
tokenStyle,
|
|
568
|
+
children
|
|
569
|
+
}) {
|
|
570
|
+
const resolvedIcons = react.useMemo(() => resolveIcons(icons), [icons]);
|
|
571
|
+
const normalizedFeatures = react.useMemo(() => normalizeFeatures(features), [features]);
|
|
572
|
+
const markdownMode = react.useMemo(() => {
|
|
573
|
+
const md = normalizedFeatures.markdown;
|
|
574
|
+
if (typeof md === "object" && md.mode) return md.mode;
|
|
575
|
+
return "hybrid";
|
|
576
|
+
}, [normalizedFeatures.markdown]);
|
|
577
|
+
const attachmentsConfig = react.useMemo(() => {
|
|
578
|
+
if (typeof normalizedFeatures.attachments === "object") {
|
|
579
|
+
return { ...DEFAULT_ATTACHMENTS, ...normalizedFeatures.attachments };
|
|
580
|
+
}
|
|
581
|
+
return DEFAULT_ATTACHMENTS;
|
|
582
|
+
}, [normalizedFeatures.attachments]);
|
|
583
|
+
const [attachments, setAttachments] = react.useState([]);
|
|
584
|
+
const [webEnabled, setWebEnabled] = react.useState(false);
|
|
585
|
+
const [isDraggingFiles, setIsDraggingFiles] = react.useState(false);
|
|
586
|
+
const submitSubsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
587
|
+
const addFilesSubsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
588
|
+
const runPromptSubsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
589
|
+
const normalizedAttachmentOptions = react.useMemo(
|
|
590
|
+
() => ({
|
|
591
|
+
uploadFirst: !!attachmentOptions?.uploadFirst,
|
|
592
|
+
onUpload: attachmentOptions?.onUpload,
|
|
593
|
+
canSendOnlyAttachment: attachmentOptions?.canSendOnlyAttachment ?? true
|
|
594
|
+
}),
|
|
595
|
+
[
|
|
596
|
+
attachmentOptions?.uploadFirst,
|
|
597
|
+
attachmentOptions?.onUpload,
|
|
598
|
+
attachmentOptions?.canSendOnlyAttachment
|
|
599
|
+
]
|
|
600
|
+
);
|
|
601
|
+
const uploadHandlerRef = react.useRef(
|
|
602
|
+
attachmentOptions?.onUpload
|
|
603
|
+
);
|
|
604
|
+
uploadHandlerRef.current = attachmentOptions?.onUpload;
|
|
605
|
+
const uploadFirstRef = react.useRef(
|
|
606
|
+
!!attachmentOptions?.uploadFirst
|
|
607
|
+
);
|
|
608
|
+
uploadFirstRef.current = !!attachmentOptions?.uploadFirst;
|
|
609
|
+
const removeAttachment = react.useCallback((id) => {
|
|
610
|
+
setAttachments((prev) => {
|
|
611
|
+
const next = prev.filter((a) => a.id !== id);
|
|
612
|
+
const removed = prev.find((a) => a.id === id);
|
|
613
|
+
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
614
|
+
return next;
|
|
615
|
+
});
|
|
616
|
+
}, []);
|
|
617
|
+
const clearAttachments = react.useCallback(() => {
|
|
618
|
+
setAttachments((prev) => {
|
|
619
|
+
prev.forEach((a) => a.previewUrl && URL.revokeObjectURL(a.previewUrl));
|
|
620
|
+
return [];
|
|
621
|
+
});
|
|
622
|
+
}, []);
|
|
623
|
+
const attachmentsCountRef = react.useRef(0);
|
|
624
|
+
attachmentsCountRef.current = attachments.length;
|
|
625
|
+
const addFiles = react.useCallback(
|
|
626
|
+
(files) => {
|
|
627
|
+
if (files.length === 0) return;
|
|
628
|
+
const enabled = !!normalizedFeatures.attachments;
|
|
629
|
+
if (!enabled) return;
|
|
630
|
+
const accepted = [];
|
|
631
|
+
let remaining = (attachmentsConfig.maxCount ?? Infinity) - attachmentsCountRef.current;
|
|
632
|
+
const shouldUpload = uploadFirstRef.current && !!uploadHandlerRef.current;
|
|
633
|
+
for (const file of files) {
|
|
634
|
+
if (remaining <= 0) break;
|
|
635
|
+
if (file.size > (attachmentsConfig.maxSize ?? Infinity)) continue;
|
|
636
|
+
const kind = detectKind(file);
|
|
637
|
+
accepted.push({
|
|
638
|
+
id: `att-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
639
|
+
kind,
|
|
640
|
+
name: file.name || `attachment.${kind}`,
|
|
641
|
+
mimeType: file.type,
|
|
642
|
+
size: file.size,
|
|
643
|
+
file,
|
|
644
|
+
previewUrl: kind === "image" || kind === "audio" ? URL.createObjectURL(file) : void 0,
|
|
645
|
+
status: shouldUpload ? "uploading" : void 0
|
|
646
|
+
});
|
|
647
|
+
remaining -= 1;
|
|
648
|
+
}
|
|
649
|
+
if (accepted.length === 0) return;
|
|
650
|
+
setAttachments((prev) => [...prev, ...accepted]);
|
|
651
|
+
addFilesSubsRef.current.forEach((cb) => cb(files));
|
|
652
|
+
if (shouldUpload) {
|
|
653
|
+
const handler = uploadHandlerRef.current;
|
|
654
|
+
for (const att of accepted) {
|
|
655
|
+
Promise.resolve().then(() => handler(att.file)).then((ok) => {
|
|
656
|
+
setAttachments(
|
|
657
|
+
(prev) => prev.map(
|
|
658
|
+
(a) => a.id === att.id ? {
|
|
659
|
+
...a,
|
|
660
|
+
status: ok ? "uploaded" : "failed",
|
|
661
|
+
error: ok ? void 0 : "Upload failed"
|
|
662
|
+
} : a
|
|
663
|
+
)
|
|
664
|
+
);
|
|
665
|
+
}).catch((err) => {
|
|
666
|
+
const message = err instanceof Error ? err.message : String(err ?? "Upload failed");
|
|
667
|
+
setAttachments(
|
|
668
|
+
(prev) => prev.map(
|
|
669
|
+
(a) => a.id === att.id ? { ...a, status: "failed", error: message } : a
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
[
|
|
677
|
+
attachmentsConfig.maxCount,
|
|
678
|
+
attachmentsConfig.maxSize,
|
|
679
|
+
normalizedFeatures.attachments
|
|
680
|
+
]
|
|
681
|
+
);
|
|
682
|
+
const registerAddFiles = react.useCallback(
|
|
683
|
+
(cb) => {
|
|
684
|
+
addFilesSubsRef.current.add(cb);
|
|
685
|
+
return () => {
|
|
686
|
+
addFilesSubsRef.current.delete(cb);
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
[]
|
|
690
|
+
);
|
|
691
|
+
const registerSubmit = react.useCallback(
|
|
692
|
+
(cb) => {
|
|
693
|
+
submitSubsRef.current.add(cb);
|
|
694
|
+
return () => {
|
|
695
|
+
submitSubsRef.current.delete(cb);
|
|
696
|
+
};
|
|
697
|
+
},
|
|
698
|
+
[]
|
|
699
|
+
);
|
|
700
|
+
const triggerSubmit = react.useCallback(() => {
|
|
701
|
+
submitSubsRef.current.forEach((cb) => cb());
|
|
702
|
+
}, []);
|
|
703
|
+
const registerRunPrompt = react.useCallback((cb) => {
|
|
704
|
+
runPromptSubsRef.current.add(cb);
|
|
705
|
+
return () => {
|
|
706
|
+
runPromptSubsRef.current.delete(cb);
|
|
707
|
+
};
|
|
708
|
+
}, []);
|
|
709
|
+
const runPrompt = react.useCallback((prompt, behavior) => {
|
|
710
|
+
runPromptSubsRef.current.forEach((cb) => cb(prompt, behavior));
|
|
711
|
+
}, []);
|
|
712
|
+
const toggleWeb = react.useCallback(() => setWebEnabled((w) => !w), []);
|
|
713
|
+
const value = react.useMemo(
|
|
714
|
+
() => ({
|
|
715
|
+
features: normalizedFeatures,
|
|
716
|
+
attachmentsConfig,
|
|
717
|
+
attachments,
|
|
718
|
+
addFiles,
|
|
719
|
+
removeAttachment,
|
|
720
|
+
clearAttachments,
|
|
721
|
+
registerAddFiles,
|
|
722
|
+
registerSubmit,
|
|
723
|
+
triggerSubmit,
|
|
724
|
+
registerRunPrompt,
|
|
725
|
+
runPrompt,
|
|
726
|
+
webEnabled,
|
|
727
|
+
toggleWeb,
|
|
728
|
+
isStreaming: !!isStreaming,
|
|
729
|
+
isDraggingFiles,
|
|
730
|
+
setIsDraggingFiles,
|
|
731
|
+
closeMenusOnOutsideClick,
|
|
732
|
+
attachmentOptions: normalizedAttachmentOptions,
|
|
733
|
+
markdownMode,
|
|
734
|
+
mode,
|
|
735
|
+
multiline,
|
|
736
|
+
submitOnEnter,
|
|
737
|
+
smartNewline,
|
|
738
|
+
focusShortcut: focusShortcut || null,
|
|
739
|
+
icons: resolvedIcons,
|
|
740
|
+
slots: slots ?? EMPTY_SLOTS,
|
|
741
|
+
renderDiagram,
|
|
742
|
+
dir,
|
|
743
|
+
classNames,
|
|
744
|
+
sx,
|
|
745
|
+
tokenStyle
|
|
746
|
+
}),
|
|
747
|
+
[
|
|
748
|
+
normalizedFeatures,
|
|
749
|
+
attachmentsConfig,
|
|
750
|
+
attachments,
|
|
751
|
+
addFiles,
|
|
752
|
+
removeAttachment,
|
|
753
|
+
clearAttachments,
|
|
754
|
+
registerAddFiles,
|
|
755
|
+
registerSubmit,
|
|
756
|
+
triggerSubmit,
|
|
757
|
+
registerRunPrompt,
|
|
758
|
+
runPrompt,
|
|
759
|
+
webEnabled,
|
|
760
|
+
toggleWeb,
|
|
761
|
+
isStreaming,
|
|
762
|
+
isDraggingFiles,
|
|
763
|
+
closeMenusOnOutsideClick,
|
|
764
|
+
normalizedAttachmentOptions,
|
|
765
|
+
markdownMode,
|
|
766
|
+
mode,
|
|
767
|
+
multiline,
|
|
768
|
+
submitOnEnter,
|
|
769
|
+
smartNewline,
|
|
770
|
+
focusShortcut,
|
|
771
|
+
resolvedIcons,
|
|
772
|
+
slots,
|
|
773
|
+
renderDiagram,
|
|
774
|
+
dir,
|
|
775
|
+
classNames,
|
|
776
|
+
sx,
|
|
777
|
+
tokenStyle
|
|
778
|
+
]
|
|
779
|
+
);
|
|
780
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ComposerContext.Provider, { value, children });
|
|
781
|
+
}
|
|
782
|
+
function useComposerContext() {
|
|
783
|
+
const ctx = react.useContext(ComposerContext);
|
|
784
|
+
if (!ctx) {
|
|
785
|
+
throw new Error("useComposerContext must be used inside <ComposerProvider>");
|
|
786
|
+
}
|
|
787
|
+
return ctx;
|
|
788
|
+
}
|
|
789
|
+
function EditorShell({
|
|
790
|
+
placeholder,
|
|
791
|
+
mode,
|
|
792
|
+
multiline,
|
|
793
|
+
header,
|
|
794
|
+
toolbar,
|
|
795
|
+
sendButton,
|
|
796
|
+
footer
|
|
797
|
+
}) {
|
|
798
|
+
const { classNames, sx, dir } = useComposerContext();
|
|
799
|
+
const isMarkdown = mode === "markdown";
|
|
800
|
+
const editorClass = multiline ? "composer-editor min-h-[44px] max-h-56 scrollbar-thin overflow-y-auto px-5 py-3.5" : "composer-editor composer-editor--inline h-9 overflow-x-auto overflow-y-hidden whitespace-nowrap px-2 leading-9";
|
|
801
|
+
const editor = slotProps("editor", editorClass, classNames, sx);
|
|
802
|
+
const editorResolved = resolveSx(sx?.editor);
|
|
803
|
+
const placeholderBase = mirrorEditorPadding(editorResolved);
|
|
804
|
+
const placeholderClass = multiline ? "composer-placeholder pointer-events-none absolute inset-x-0 top-0 select-none px-5 py-3.5 text-[15px] leading-relaxed text-muted-foreground" : "composer-placeholder pointer-events-none absolute inset-y-0 inset-x-0 select-none px-2 text-[15px] leading-9 text-muted-foreground truncate";
|
|
805
|
+
const placeholderProps = slotProps(
|
|
806
|
+
"placeholder",
|
|
807
|
+
placeholderClass,
|
|
808
|
+
classNames,
|
|
809
|
+
sx,
|
|
810
|
+
placeholderBase
|
|
811
|
+
);
|
|
812
|
+
const contentEditable = /* @__PURE__ */ jsxRuntime.jsx(LexicalContentEditable.ContentEditable, { ...editor, "aria-label": "Message", spellCheck: true, dir });
|
|
813
|
+
const placeholderEl = /* @__PURE__ */ jsxRuntime.jsx("div", { ...placeholderProps, dir, children: placeholder });
|
|
814
|
+
const editorBlock = /* @__PURE__ */ jsxRuntime.jsx(
|
|
815
|
+
"div",
|
|
816
|
+
{
|
|
817
|
+
className: cn(
|
|
818
|
+
"composer-editor-block relative min-w-0",
|
|
819
|
+
// Inline: the editor block is the flex child that fills the row.
|
|
820
|
+
!multiline && "flex-1"
|
|
821
|
+
),
|
|
822
|
+
children: isMarkdown ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
823
|
+
LexicalRichTextPlugin.RichTextPlugin,
|
|
824
|
+
{
|
|
825
|
+
contentEditable,
|
|
826
|
+
placeholder: placeholderEl,
|
|
827
|
+
ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
|
|
828
|
+
}
|
|
829
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
830
|
+
LexicalPlainTextPlugin.PlainTextPlugin,
|
|
831
|
+
{
|
|
832
|
+
contentEditable,
|
|
833
|
+
placeholder: placeholderEl,
|
|
834
|
+
ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
|
|
835
|
+
}
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
if (!multiline) {
|
|
840
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
841
|
+
header,
|
|
842
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "composer-inline-row flex items-center gap-1 px-2 py-1.5", children: [
|
|
843
|
+
toolbar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "composer-inline-toolbar flex shrink-0 items-center", children: toolbar }),
|
|
844
|
+
editorBlock,
|
|
845
|
+
sendButton && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "composer-inline-send flex shrink-0 items-center", children: sendButton })
|
|
846
|
+
] }),
|
|
847
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalHistoryPlugin.HistoryPlugin, {})
|
|
848
|
+
] });
|
|
849
|
+
}
|
|
850
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
851
|
+
header,
|
|
852
|
+
editorBlock,
|
|
853
|
+
(toolbar || sendButton) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 pb-2.5", children: [
|
|
854
|
+
toolbar ?? /* @__PURE__ */ jsxRuntime.jsx("span", {}),
|
|
855
|
+
sendButton
|
|
856
|
+
] }),
|
|
857
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalHistoryPlugin.HistoryPlugin, {}),
|
|
858
|
+
footer
|
|
859
|
+
] });
|
|
860
|
+
}
|
|
861
|
+
var PLACEHOLDER_MIRROR_KEYS = [
|
|
862
|
+
"padding",
|
|
863
|
+
"paddingInline",
|
|
864
|
+
"paddingInlineStart",
|
|
865
|
+
"paddingInlineEnd",
|
|
866
|
+
"paddingBlock",
|
|
867
|
+
"paddingBlockStart",
|
|
868
|
+
"paddingTop",
|
|
869
|
+
"paddingLeft",
|
|
870
|
+
"paddingRight",
|
|
871
|
+
"fontSize",
|
|
872
|
+
"fontFamily",
|
|
873
|
+
"lineHeight",
|
|
874
|
+
"letterSpacing"
|
|
875
|
+
];
|
|
876
|
+
function mirrorEditorPadding(editorStyle) {
|
|
877
|
+
if (!editorStyle) return void 0;
|
|
878
|
+
const out = {};
|
|
879
|
+
for (const key of PLACEHOLDER_MIRROR_KEYS) {
|
|
880
|
+
const v = editorStyle[key];
|
|
881
|
+
if (v !== void 0) out[key] = v;
|
|
882
|
+
}
|
|
883
|
+
return Object.keys(out).length ? out : void 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/core/theme.ts
|
|
887
|
+
var composerTheme = {
|
|
888
|
+
paragraph: "composer-paragraph",
|
|
889
|
+
quote: "composer-quote",
|
|
890
|
+
heading: {
|
|
891
|
+
h1: "composer-h1",
|
|
892
|
+
h2: "composer-h2",
|
|
893
|
+
h3: "composer-h3",
|
|
894
|
+
h4: "composer-h4",
|
|
895
|
+
h5: "composer-h5",
|
|
896
|
+
h6: "composer-h6"
|
|
897
|
+
},
|
|
898
|
+
list: {
|
|
899
|
+
ul: "composer-ul",
|
|
900
|
+
ol: "composer-ol",
|
|
901
|
+
listitem: "composer-li",
|
|
902
|
+
nested: {
|
|
903
|
+
listitem: "composer-li-nested"
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
text: {
|
|
907
|
+
bold: "composer-bold",
|
|
908
|
+
italic: "composer-italic",
|
|
909
|
+
underline: "composer-underline",
|
|
910
|
+
strikethrough: "composer-strike",
|
|
911
|
+
underlineStrikethrough: "composer-underline composer-strike",
|
|
912
|
+
code: "composer-code"
|
|
913
|
+
},
|
|
914
|
+
code: "composer-code-block",
|
|
915
|
+
codeHighlight: {
|
|
916
|
+
atrule: "composer-token-attr",
|
|
917
|
+
attr: "composer-token-attr",
|
|
918
|
+
boolean: "composer-token-property",
|
|
919
|
+
builtin: "composer-token-selector",
|
|
920
|
+
cdata: "composer-token-comment",
|
|
921
|
+
char: "composer-token-selector",
|
|
922
|
+
class: "composer-token-function",
|
|
923
|
+
"class-name": "composer-token-function",
|
|
924
|
+
comment: "composer-token-comment",
|
|
925
|
+
constant: "composer-token-property",
|
|
926
|
+
deleted: "composer-token-property",
|
|
927
|
+
doctype: "composer-token-comment",
|
|
928
|
+
entity: "composer-token-operator",
|
|
929
|
+
function: "composer-token-function",
|
|
930
|
+
important: "composer-token-variable",
|
|
931
|
+
inserted: "composer-token-selector",
|
|
932
|
+
keyword: "composer-token-attr",
|
|
933
|
+
namespace: "composer-token-variable",
|
|
934
|
+
number: "composer-token-property",
|
|
935
|
+
operator: "composer-token-operator",
|
|
936
|
+
prolog: "composer-token-comment",
|
|
937
|
+
property: "composer-token-property",
|
|
938
|
+
punctuation: "composer-token-punctuation",
|
|
939
|
+
regex: "composer-token-variable",
|
|
940
|
+
selector: "composer-token-selector",
|
|
941
|
+
string: "composer-token-selector",
|
|
942
|
+
symbol: "composer-token-property",
|
|
943
|
+
tag: "composer-token-property",
|
|
944
|
+
url: "composer-token-operator",
|
|
945
|
+
variable: "composer-token-variable"
|
|
946
|
+
},
|
|
947
|
+
link: "composer-link"
|
|
948
|
+
};
|
|
949
|
+
var MentionNode = class _MentionNode extends lexical.ElementNode {
|
|
950
|
+
/** Stable identifier supplied by the consumer's MentionItem. */
|
|
951
|
+
__id;
|
|
952
|
+
/** Trigger character shown via CSS `::before` (e.g. "@" or "#"). */
|
|
953
|
+
__prefix;
|
|
954
|
+
static getType() {
|
|
955
|
+
return "composeai-mention";
|
|
956
|
+
}
|
|
957
|
+
static clone(node) {
|
|
958
|
+
return new _MentionNode(node.__id, node.__prefix, node.__key);
|
|
959
|
+
}
|
|
960
|
+
constructor(id, prefix = "@", key) {
|
|
961
|
+
super(key);
|
|
962
|
+
this.__id = id;
|
|
963
|
+
this.__prefix = prefix;
|
|
964
|
+
}
|
|
965
|
+
// ── Stable accessors ────────────────────────────────────────────────
|
|
966
|
+
getMentionId() {
|
|
967
|
+
return this.__id;
|
|
968
|
+
}
|
|
969
|
+
getMentionPrefix() {
|
|
970
|
+
return this.__prefix;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* The current label text. Reflects user edits — if the user backspaced
|
|
974
|
+
* "@John Doe" down to "@John", this returns "John".
|
|
975
|
+
*/
|
|
976
|
+
getMentionLabel() {
|
|
977
|
+
return this.getTextContent();
|
|
978
|
+
}
|
|
979
|
+
// ── Behavior flags ──────────────────────────────────────────────────
|
|
980
|
+
isInline() {
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Returning false makes Lexical auto-remove the element as soon as its
|
|
985
|
+
* children collection becomes empty. That implements "delete the whole
|
|
986
|
+
* mention when I remove all the text" with zero plugin code.
|
|
987
|
+
*/
|
|
988
|
+
canBeEmpty() {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Prevent adjacent text outside the chip from accidentally merging
|
|
993
|
+
* INTO the chip — typing "x" right after a mention should produce
|
|
994
|
+
* "@John|x", not "@Johnx".
|
|
995
|
+
*/
|
|
996
|
+
canInsertTextBefore() {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
canInsertTextAfter() {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
// ── DOM ─────────────────────────────────────────────────────────────
|
|
1003
|
+
createDOM(_config) {
|
|
1004
|
+
const span = document.createElement("span");
|
|
1005
|
+
span.className = "composer-mention";
|
|
1006
|
+
span.setAttribute("data-mention-id", this.__id);
|
|
1007
|
+
span.setAttribute("data-mention-prefix", this.__prefix);
|
|
1008
|
+
return span;
|
|
1009
|
+
}
|
|
1010
|
+
updateDOM(prev, dom) {
|
|
1011
|
+
if (prev.__id !== this.__id) dom.setAttribute("data-mention-id", this.__id);
|
|
1012
|
+
if (prev.__prefix !== this.__prefix) {
|
|
1013
|
+
dom.setAttribute("data-mention-prefix", this.__prefix);
|
|
1014
|
+
}
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
exportDOM() {
|
|
1018
|
+
const el = document.createElement("span");
|
|
1019
|
+
el.setAttribute("data-mention-id", this.__id);
|
|
1020
|
+
el.setAttribute("data-mention-prefix", this.__prefix);
|
|
1021
|
+
el.textContent = `${this.__prefix}${this.getTextContent()}`;
|
|
1022
|
+
return { element: el };
|
|
1023
|
+
}
|
|
1024
|
+
static importDOM() {
|
|
1025
|
+
return {
|
|
1026
|
+
span: (node) => {
|
|
1027
|
+
if (!node.hasAttribute("data-mention-id")) return null;
|
|
1028
|
+
return {
|
|
1029
|
+
conversion: () => {
|
|
1030
|
+
const id = node.getAttribute("data-mention-id") ?? "";
|
|
1031
|
+
const prefix = node.getAttribute("data-mention-prefix") ?? "@";
|
|
1032
|
+
const rawText = node.textContent ?? "";
|
|
1033
|
+
const label = rawText.startsWith(prefix) ? rawText.slice(prefix.length) : rawText;
|
|
1034
|
+
const mention = $createMentionNode(id, prefix);
|
|
1035
|
+
if (label) {
|
|
1036
|
+
mention.append(lexical.$createTextNode(label));
|
|
1037
|
+
}
|
|
1038
|
+
return { node: mention };
|
|
1039
|
+
},
|
|
1040
|
+
priority: 1
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// ── JSON ────────────────────────────────────────────────────────────
|
|
1046
|
+
exportJSON() {
|
|
1047
|
+
return {
|
|
1048
|
+
...super.exportJSON(),
|
|
1049
|
+
type: _MentionNode.getType(),
|
|
1050
|
+
version: 1,
|
|
1051
|
+
mentionId: this.__id,
|
|
1052
|
+
mentionPrefix: this.__prefix
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
static importJSON(json) {
|
|
1056
|
+
return $createMentionNode(json.mentionId, json.mentionPrefix ?? "@");
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
function $createMentionNode(id, prefix = "@") {
|
|
1060
|
+
return lexical.$applyNodeReplacement(new MentionNode(id, prefix));
|
|
1061
|
+
}
|
|
1062
|
+
function $isMentionNode(node) {
|
|
1063
|
+
return node instanceof MentionNode;
|
|
1064
|
+
}
|
|
1065
|
+
var TOKEN_CLASS = "composer-md-token";
|
|
1066
|
+
var MarkdownTokenNode = class _MarkdownTokenNode extends lexical.TextNode {
|
|
1067
|
+
static getType() {
|
|
1068
|
+
return "composeai-md-token";
|
|
1069
|
+
}
|
|
1070
|
+
static clone(node) {
|
|
1071
|
+
return new _MarkdownTokenNode(node.__text, node.__key);
|
|
1072
|
+
}
|
|
1073
|
+
constructor(text = "", key) {
|
|
1074
|
+
super(text, key);
|
|
1075
|
+
}
|
|
1076
|
+
createDOM(config) {
|
|
1077
|
+
const dom = super.createDOM(config);
|
|
1078
|
+
lexical.addClassNamesToElement(dom, TOKEN_CLASS);
|
|
1079
|
+
return dom;
|
|
1080
|
+
}
|
|
1081
|
+
static importJSON(serializedNode) {
|
|
1082
|
+
return $createMarkdownTokenNode().updateFromJSON(serializedNode);
|
|
1083
|
+
}
|
|
1084
|
+
updateFromJSON(serializedNode) {
|
|
1085
|
+
return super.updateFromJSON(serializedNode);
|
|
1086
|
+
}
|
|
1087
|
+
exportJSON() {
|
|
1088
|
+
return {
|
|
1089
|
+
...super.exportJSON(),
|
|
1090
|
+
type: _MarkdownTokenNode.getType(),
|
|
1091
|
+
version: 1
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
function $createMarkdownTokenNode(text = "") {
|
|
1096
|
+
return lexical.$applyNodeReplacement(new MarkdownTokenNode(text));
|
|
1097
|
+
}
|
|
1098
|
+
function $isMarkdownTokenNode(node) {
|
|
1099
|
+
return node instanceof MarkdownTokenNode;
|
|
1100
|
+
}
|
|
1101
|
+
var BlockParagraphNode = class _BlockParagraphNode extends lexical.ParagraphNode {
|
|
1102
|
+
__blockMarker;
|
|
1103
|
+
constructor(key) {
|
|
1104
|
+
super(key);
|
|
1105
|
+
this.__blockMarker = "";
|
|
1106
|
+
}
|
|
1107
|
+
static getType() {
|
|
1108
|
+
return "composeai-block-paragraph";
|
|
1109
|
+
}
|
|
1110
|
+
static clone(node) {
|
|
1111
|
+
const next = new _BlockParagraphNode(node.__key);
|
|
1112
|
+
next.__blockMarker = node.__blockMarker;
|
|
1113
|
+
return next;
|
|
1114
|
+
}
|
|
1115
|
+
static importJSON(serializedNode) {
|
|
1116
|
+
return $createBlockParagraphNode().updateFromJSON(serializedNode);
|
|
1117
|
+
}
|
|
1118
|
+
updateFromJSON(serializedNode) {
|
|
1119
|
+
super.updateFromJSON(serializedNode);
|
|
1120
|
+
const writable = this.getWritable();
|
|
1121
|
+
writable.__blockMarker = serializedNode.blockMarker ?? "";
|
|
1122
|
+
return writable;
|
|
1123
|
+
}
|
|
1124
|
+
exportJSON() {
|
|
1125
|
+
return {
|
|
1126
|
+
...super.exportJSON(),
|
|
1127
|
+
type: _BlockParagraphNode.getType(),
|
|
1128
|
+
version: 1,
|
|
1129
|
+
blockMarker: this.getBlockMarker()
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
getBlockMarker() {
|
|
1133
|
+
return this.getLatest().__blockMarker;
|
|
1134
|
+
}
|
|
1135
|
+
setBlockMarker(marker) {
|
|
1136
|
+
const writable = this.getWritable();
|
|
1137
|
+
writable.__blockMarker = marker;
|
|
1138
|
+
return writable;
|
|
1139
|
+
}
|
|
1140
|
+
hasBlockMarker() {
|
|
1141
|
+
return this.getLatest().__blockMarker.length > 0;
|
|
1142
|
+
}
|
|
1143
|
+
// Enter inside a block paragraph should start a clean paragraph below —
|
|
1144
|
+
// headings, quotes, lists, fences etc. don't bleed into the next line.
|
|
1145
|
+
insertNewAfter(rangeSelection, restoreSelection = false) {
|
|
1146
|
+
const next = super.insertNewAfter(rangeSelection, restoreSelection);
|
|
1147
|
+
if (next instanceof _BlockParagraphNode) {
|
|
1148
|
+
next.setBlockMarker("");
|
|
1149
|
+
}
|
|
1150
|
+
return next;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
function $createBlockParagraphNode() {
|
|
1154
|
+
return lexical.$applyNodeReplacement(new BlockParagraphNode());
|
|
1155
|
+
}
|
|
1156
|
+
function $isBlockParagraphNode(node) {
|
|
1157
|
+
return node instanceof BlockParagraphNode;
|
|
1158
|
+
}
|
|
1159
|
+
var LINK_CLASS = "composer-link";
|
|
1160
|
+
var LinkTextNode = class _LinkTextNode extends lexical.TextNode {
|
|
1161
|
+
__url;
|
|
1162
|
+
constructor(text = "", url = "", key) {
|
|
1163
|
+
super(text, key);
|
|
1164
|
+
this.__url = url;
|
|
1165
|
+
}
|
|
1166
|
+
static getType() {
|
|
1167
|
+
return "composeai-link-text";
|
|
1168
|
+
}
|
|
1169
|
+
static clone(node) {
|
|
1170
|
+
return new _LinkTextNode(node.__text, node.__url, node.__key);
|
|
1171
|
+
}
|
|
1172
|
+
createDOM(config) {
|
|
1173
|
+
const dom = super.createDOM(config);
|
|
1174
|
+
lexical.addClassNamesToElement(dom, LINK_CLASS);
|
|
1175
|
+
if (this.__url) dom.setAttribute("data-url", this.__url);
|
|
1176
|
+
return dom;
|
|
1177
|
+
}
|
|
1178
|
+
updateDOM(prevNode, dom, config) {
|
|
1179
|
+
const updated = super.updateDOM(prevNode, dom, config);
|
|
1180
|
+
if (prevNode.__url !== this.__url) {
|
|
1181
|
+
if (this.__url) dom.setAttribute("data-url", this.__url);
|
|
1182
|
+
else dom.removeAttribute("data-url");
|
|
1183
|
+
}
|
|
1184
|
+
return updated;
|
|
1185
|
+
}
|
|
1186
|
+
static importJSON(serializedNode) {
|
|
1187
|
+
return $createLinkTextNode("", "").updateFromJSON(serializedNode);
|
|
1188
|
+
}
|
|
1189
|
+
updateFromJSON(serializedNode) {
|
|
1190
|
+
super.updateFromJSON(serializedNode);
|
|
1191
|
+
const writable = this.getWritable();
|
|
1192
|
+
writable.__url = serializedNode.url ?? "";
|
|
1193
|
+
return writable;
|
|
1194
|
+
}
|
|
1195
|
+
exportJSON() {
|
|
1196
|
+
return {
|
|
1197
|
+
...super.exportJSON(),
|
|
1198
|
+
type: _LinkTextNode.getType(),
|
|
1199
|
+
version: 1,
|
|
1200
|
+
url: this.getUrl()
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
getUrl() {
|
|
1204
|
+
return this.getLatest().__url;
|
|
1205
|
+
}
|
|
1206
|
+
setUrl(url) {
|
|
1207
|
+
const writable = this.getWritable();
|
|
1208
|
+
writable.__url = url;
|
|
1209
|
+
return writable;
|
|
1210
|
+
}
|
|
1211
|
+
// Typing at the boundary of the link should NOT extend the link — the
|
|
1212
|
+
// user wants their next char to be plain text, not "part of the link
|
|
1213
|
+
// label". Lexical honours these by creating a new sibling TextNode for
|
|
1214
|
+
// boundary insertions. Internal edits (cursor between chars) still go
|
|
1215
|
+
// into this node, which keeps label editing natural.
|
|
1216
|
+
canInsertTextBefore() {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
canInsertTextAfter() {
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
function $createLinkTextNode(text = "", url = "") {
|
|
1224
|
+
return lexical.$applyNodeReplacement(new LinkTextNode(text, url));
|
|
1225
|
+
}
|
|
1226
|
+
function $isLinkTextNode(node) {
|
|
1227
|
+
return node instanceof LinkTextNode;
|
|
1228
|
+
}
|
|
1229
|
+
var HEADING_RE = /^(#{1,6}) /;
|
|
1230
|
+
var QUOTE_RE = /^> /;
|
|
1231
|
+
var BULLET_RE = /^[-*+] /;
|
|
1232
|
+
var NUMBERED_RE = /^\d+\. /;
|
|
1233
|
+
var HR_RE = /^(?:---|\*\*\*|___)\s*$/;
|
|
1234
|
+
var FENCE_OPEN_RE = /^```([A-Za-z0-9_-]*)(?:\s.*)?$/;
|
|
1235
|
+
var FENCE_CLOSE_RE = /^```\s*$/;
|
|
1236
|
+
var PLAIN = { kind: "paragraph", markerLen: 0 };
|
|
1237
|
+
function detectFromMarker(marker) {
|
|
1238
|
+
if (marker.length === 0) return null;
|
|
1239
|
+
if (FENCE_OPEN_RE.test(marker)) {
|
|
1240
|
+
const m = marker.match(FENCE_OPEN_RE);
|
|
1241
|
+
return {
|
|
1242
|
+
kind: "code-fence-open",
|
|
1243
|
+
markerLen: 0,
|
|
1244
|
+
lang: m && m[1] || void 0
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (FENCE_CLOSE_RE.test(marker)) {
|
|
1248
|
+
return { kind: "code-fence-close", markerLen: 0 };
|
|
1249
|
+
}
|
|
1250
|
+
const probed = detectBlock(`${marker}x`, false);
|
|
1251
|
+
if (probed.kind === "paragraph") return null;
|
|
1252
|
+
return { ...probed, markerLen: 0 };
|
|
1253
|
+
}
|
|
1254
|
+
function detectBlock(text, insideCode) {
|
|
1255
|
+
if (insideCode) {
|
|
1256
|
+
if (FENCE_CLOSE_RE.test(text)) {
|
|
1257
|
+
return { kind: "code-fence-close", markerLen: 0 };
|
|
1258
|
+
}
|
|
1259
|
+
return { kind: "code-line", markerLen: 0 };
|
|
1260
|
+
}
|
|
1261
|
+
const openMatch = text.match(FENCE_OPEN_RE);
|
|
1262
|
+
if (openMatch) {
|
|
1263
|
+
return { kind: "code-fence-open", markerLen: 0, lang: openMatch[1] || void 0 };
|
|
1264
|
+
}
|
|
1265
|
+
if (text.length === 0) return PLAIN;
|
|
1266
|
+
const h = text.match(HEADING_RE);
|
|
1267
|
+
if (h) {
|
|
1268
|
+
const level = h[1].length;
|
|
1269
|
+
return { kind: `heading-${level}`, markerLen: h[0].length };
|
|
1270
|
+
}
|
|
1271
|
+
if (QUOTE_RE.test(text)) {
|
|
1272
|
+
return { kind: "quote", markerLen: 2 };
|
|
1273
|
+
}
|
|
1274
|
+
if (BULLET_RE.test(text)) {
|
|
1275
|
+
return { kind: "list-bullet", markerLen: 2 };
|
|
1276
|
+
}
|
|
1277
|
+
const num = text.match(NUMBERED_RE);
|
|
1278
|
+
if (num) {
|
|
1279
|
+
return { kind: "list-numbered", markerLen: num[0].length };
|
|
1280
|
+
}
|
|
1281
|
+
if (HR_RE.test(text)) {
|
|
1282
|
+
return { kind: "hr", markerLen: 0 };
|
|
1283
|
+
}
|
|
1284
|
+
return PLAIN;
|
|
1285
|
+
}
|
|
1286
|
+
function $resolveBlockFor(paragraph, insideCode) {
|
|
1287
|
+
if ($isBlockParagraphNode(paragraph) && paragraph.hasBlockMarker()) {
|
|
1288
|
+
const fromMarker = detectFromMarker(paragraph.getBlockMarker());
|
|
1289
|
+
if (fromMarker !== null) {
|
|
1290
|
+
if (insideCode && fromMarker.kind !== "code-fence-close") {
|
|
1291
|
+
return { kind: "code-line", markerLen: 0 };
|
|
1292
|
+
}
|
|
1293
|
+
return fromMarker;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return detectBlock(paragraph.getTextContent(), insideCode);
|
|
1297
|
+
}
|
|
1298
|
+
function $computeBlockMap() {
|
|
1299
|
+
const map = /* @__PURE__ */ new Map();
|
|
1300
|
+
const root = lexical.$getRoot();
|
|
1301
|
+
let insideCode = false;
|
|
1302
|
+
for (const child of root.getChildren()) {
|
|
1303
|
+
if (!lexical.$isParagraphNode(child)) continue;
|
|
1304
|
+
const info = $resolveBlockFor(child, insideCode);
|
|
1305
|
+
map.set(child.getKey(), info);
|
|
1306
|
+
if (info.kind === "code-fence-open") insideCode = true;
|
|
1307
|
+
else if (info.kind === "code-fence-close") insideCode = false;
|
|
1308
|
+
}
|
|
1309
|
+
return map;
|
|
1310
|
+
}
|
|
1311
|
+
function $isParagraphInsideCodeFence(paragraph) {
|
|
1312
|
+
const root = paragraph.getParent();
|
|
1313
|
+
if (!root) return false;
|
|
1314
|
+
let inside = false;
|
|
1315
|
+
for (const child of root.getChildren()) {
|
|
1316
|
+
if (child === paragraph) return inside;
|
|
1317
|
+
if (!lexical.$isParagraphNode(child)) continue;
|
|
1318
|
+
if ($isBlockParagraphNode(child) && child.hasBlockMarker()) {
|
|
1319
|
+
const marker = child.getBlockMarker();
|
|
1320
|
+
if (inside) {
|
|
1321
|
+
if (FENCE_CLOSE_RE.test(marker)) inside = false;
|
|
1322
|
+
} else {
|
|
1323
|
+
if (FENCE_OPEN_RE.test(marker)) inside = true;
|
|
1324
|
+
}
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const text = child.getTextContent();
|
|
1328
|
+
if (inside) {
|
|
1329
|
+
if (FENCE_CLOSE_RE.test(text)) inside = false;
|
|
1330
|
+
} else {
|
|
1331
|
+
if (FENCE_OPEN_RE.test(text)) inside = true;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return inside;
|
|
1335
|
+
}
|
|
1336
|
+
function $detectBlockFor(paragraph) {
|
|
1337
|
+
const inside = $isParagraphInsideCodeFence(paragraph);
|
|
1338
|
+
return $resolveBlockFor(paragraph, inside);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/core/serializer.ts
|
|
1342
|
+
function collectPlainAndMentions(editor) {
|
|
1343
|
+
return editor.getEditorState().read(() => {
|
|
1344
|
+
const root = lexical.$getRoot();
|
|
1345
|
+
const mentions = [];
|
|
1346
|
+
const walkInline = (parent) => {
|
|
1347
|
+
if (!lexical.$isElementNode(parent)) return "";
|
|
1348
|
+
let out = "";
|
|
1349
|
+
for (const child of parent.getChildren()) {
|
|
1350
|
+
if ($isMentionNode(child)) {
|
|
1351
|
+
const label = child.getMentionLabel();
|
|
1352
|
+
mentions.push({
|
|
1353
|
+
id: child.getMentionId(),
|
|
1354
|
+
label
|
|
1355
|
+
});
|
|
1356
|
+
out += `${child.getMentionPrefix()}${label}`;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if ($isMarkdownTokenNode(child)) continue;
|
|
1360
|
+
if (lexical.$isLineBreakNode(child)) {
|
|
1361
|
+
out += "\n";
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (lexical.$isTextNode(child)) {
|
|
1365
|
+
out += child.getTextContent();
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (lexical.$isElementNode(child)) {
|
|
1369
|
+
out += walkInline(child);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return out;
|
|
1373
|
+
};
|
|
1374
|
+
const blocks = [];
|
|
1375
|
+
for (const child of root.getChildren()) {
|
|
1376
|
+
if (lexical.$isElementNode(child)) {
|
|
1377
|
+
blocks.push(walkInline(child));
|
|
1378
|
+
} else if (lexical.$isTextNode(child)) {
|
|
1379
|
+
blocks.push(child.getTextContent());
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return { text: blocks.join("\n").replace(/\n+$/g, ""), mentions };
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
var FORMAT_BIT = {
|
|
1386
|
+
bold: 1,
|
|
1387
|
+
italic: 2,
|
|
1388
|
+
strike: 4,
|
|
1389
|
+
// underline (8) — no widely-supported markdown syntax; we drop it on
|
|
1390
|
+
// serialize rather than invent one.
|
|
1391
|
+
code: 16
|
|
1392
|
+
};
|
|
1393
|
+
function wrapByFormat(text, format) {
|
|
1394
|
+
if (!text) return text;
|
|
1395
|
+
let out = text;
|
|
1396
|
+
if (format & FORMAT_BIT.code) out = `\`${out}\``;
|
|
1397
|
+
if (format & FORMAT_BIT.bold) out = `**${out}**`;
|
|
1398
|
+
if (format & FORMAT_BIT.italic) out = `*${out}*`;
|
|
1399
|
+
if (format & FORMAT_BIT.strike) out = `~~${out}~~`;
|
|
1400
|
+
return out;
|
|
1401
|
+
}
|
|
1402
|
+
function toMarkdown(editor) {
|
|
1403
|
+
return editor.getEditorState().read(() => {
|
|
1404
|
+
const root = lexical.$getRoot();
|
|
1405
|
+
let usingLive = true;
|
|
1406
|
+
const scan = (node) => {
|
|
1407
|
+
if ($isMarkdownTokenNode(node)) {
|
|
1408
|
+
const prev = node.getPreviousSibling();
|
|
1409
|
+
if (prev !== null) usingLive = false;
|
|
1410
|
+
} else if (lexical.$isElementNode(node)) {
|
|
1411
|
+
for (const child of node.getChildren()) {
|
|
1412
|
+
if (!usingLive) return;
|
|
1413
|
+
scan(child);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
for (const child of root.getChildren()) {
|
|
1418
|
+
if (!usingLive) break;
|
|
1419
|
+
scan(child);
|
|
1420
|
+
}
|
|
1421
|
+
const lines = [];
|
|
1422
|
+
const serializeParagraph = (paragraph) => {
|
|
1423
|
+
if (!lexical.$isElementNode(paragraph)) return "";
|
|
1424
|
+
let out = "";
|
|
1425
|
+
for (const child of paragraph.getChildren()) {
|
|
1426
|
+
if ($isMentionNode(child)) {
|
|
1427
|
+
out += `${child.getMentionPrefix()}${child.getMentionLabel()}`;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (lexical.$isLineBreakNode(child)) {
|
|
1431
|
+
out += "\n";
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if ($isMarkdownTokenNode(child)) {
|
|
1435
|
+
out += child.getTextContent();
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
if ($isLinkTextNode(child)) {
|
|
1439
|
+
const label = child.getTextContent();
|
|
1440
|
+
const url = child.getUrl();
|
|
1441
|
+
out += url ? `[${label}](${url})` : label;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (lexical.$isTextNode(child)) {
|
|
1445
|
+
const text = child.getTextContent();
|
|
1446
|
+
if (usingLive) {
|
|
1447
|
+
out += wrapByFormat(text, child.getFormat());
|
|
1448
|
+
} else {
|
|
1449
|
+
out += text;
|
|
1450
|
+
}
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (lexical.$isElementNode(child)) {
|
|
1454
|
+
out += serializeParagraph(child);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return out;
|
|
1458
|
+
};
|
|
1459
|
+
for (const child of root.getChildren()) {
|
|
1460
|
+
if (lexical.$isParagraphNode(child)) {
|
|
1461
|
+
const body = serializeParagraph(child);
|
|
1462
|
+
if ($isBlockParagraphNode(child) && child.hasBlockMarker()) {
|
|
1463
|
+
lines.push(child.getBlockMarker() + body);
|
|
1464
|
+
} else {
|
|
1465
|
+
lines.push(body);
|
|
1466
|
+
}
|
|
1467
|
+
} else if (lexical.$isTextNode(child)) {
|
|
1468
|
+
lines.push(child.getTextContent());
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return joinWithCommonMarkSpacing(lines).trim();
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
function isLazyContinuationBlock(kind) {
|
|
1475
|
+
return kind === "list-bullet" || kind === "list-numbered" || kind === "quote";
|
|
1476
|
+
}
|
|
1477
|
+
function joinWithCommonMarkSpacing(lines) {
|
|
1478
|
+
if (lines.length === 0) return "";
|
|
1479
|
+
let insideCode = false;
|
|
1480
|
+
const blocks = lines.map((line) => {
|
|
1481
|
+
const info = detectBlock(line, insideCode);
|
|
1482
|
+
if (info.kind === "code-fence-open") insideCode = true;
|
|
1483
|
+
else if (info.kind === "code-fence-close") insideCode = false;
|
|
1484
|
+
return info;
|
|
1485
|
+
});
|
|
1486
|
+
let out = lines[0];
|
|
1487
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1488
|
+
const prev = blocks[i - 1];
|
|
1489
|
+
const next = blocks[i];
|
|
1490
|
+
const nextText = lines[i];
|
|
1491
|
+
const needsBlankSeparator = isLazyContinuationBlock(prev.kind) && next.kind === "paragraph" && nextText.length > 0;
|
|
1492
|
+
out += needsBlankSeparator ? "\n\n" : "\n";
|
|
1493
|
+
out += nextText;
|
|
1494
|
+
}
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
function $isInsideCodeFence() {
|
|
1498
|
+
const selection = lexical.$getSelection();
|
|
1499
|
+
if (!lexical.$isRangeSelection(selection)) return false;
|
|
1500
|
+
const anchorNode = selection.anchor.getNode();
|
|
1501
|
+
const top = anchorNode.getTopLevelElement();
|
|
1502
|
+
if (!top || !lexical.$isParagraphNode(top)) return false;
|
|
1503
|
+
const block = $detectBlockFor(top);
|
|
1504
|
+
return block.kind === "code-line" || block.kind === "code-fence-open";
|
|
1505
|
+
}
|
|
1506
|
+
function $hasMultiLineContent() {
|
|
1507
|
+
const root = lexical.$getRoot();
|
|
1508
|
+
if (root.getChildrenSize() > 1) return true;
|
|
1509
|
+
return root.getTextContent().includes("\n");
|
|
1510
|
+
}
|
|
1511
|
+
function $offsetWithinParagraph(paragraph, point) {
|
|
1512
|
+
if (point.type === "element") {
|
|
1513
|
+
const children = paragraph.getChildren();
|
|
1514
|
+
let offset2 = 0;
|
|
1515
|
+
const limit = Math.min(point.offset, children.length);
|
|
1516
|
+
for (let i = 0; i < limit; i++) {
|
|
1517
|
+
offset2 += children[i].getTextContentSize();
|
|
1518
|
+
}
|
|
1519
|
+
return offset2;
|
|
1520
|
+
}
|
|
1521
|
+
const anchorKey = point.getNode().getKey();
|
|
1522
|
+
let offset = 0;
|
|
1523
|
+
for (const child of paragraph.getChildren()) {
|
|
1524
|
+
if (child.getKey() === anchorKey) return offset + point.offset;
|
|
1525
|
+
offset += child.getTextContentSize();
|
|
1526
|
+
}
|
|
1527
|
+
return offset + point.offset;
|
|
1528
|
+
}
|
|
1529
|
+
function $handleListContinuation() {
|
|
1530
|
+
const selection = lexical.$getSelection();
|
|
1531
|
+
if (!lexical.$isRangeSelection(selection)) return false;
|
|
1532
|
+
if (!selection.isCollapsed()) return false;
|
|
1533
|
+
const anchorNode = selection.anchor.getNode();
|
|
1534
|
+
const top = anchorNode.getTopLevelElement();
|
|
1535
|
+
if (!top || !lexical.$isParagraphNode(top)) return false;
|
|
1536
|
+
const block = $detectBlockFor(top);
|
|
1537
|
+
if (block.kind !== "list-bullet" && block.kind !== "list-numbered") {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
const text = top.getTextContent();
|
|
1541
|
+
const contentAfterMarker = text.slice(block.markerLen);
|
|
1542
|
+
const cursorOffset = $offsetWithinParagraph(top, selection.anchor);
|
|
1543
|
+
if (cursorOffset < block.markerLen) return false;
|
|
1544
|
+
if (contentAfterMarker.length === 0) {
|
|
1545
|
+
top.clear();
|
|
1546
|
+
top.select(0, 0);
|
|
1547
|
+
return true;
|
|
1548
|
+
}
|
|
1549
|
+
let nextMarker;
|
|
1550
|
+
if (block.kind === "list-bullet") {
|
|
1551
|
+
const ch = text.charAt(0) || "-";
|
|
1552
|
+
nextMarker = `${ch} `;
|
|
1553
|
+
} else {
|
|
1554
|
+
const numMatch = text.match(/^(\d+)/);
|
|
1555
|
+
const n = numMatch ? parseInt(numMatch[1], 10) + 1 : 2;
|
|
1556
|
+
nextMarker = `${n}. `;
|
|
1557
|
+
}
|
|
1558
|
+
selection.insertParagraph();
|
|
1559
|
+
const newSelection = lexical.$getSelection();
|
|
1560
|
+
if (lexical.$isRangeSelection(newSelection)) {
|
|
1561
|
+
newSelection.insertText(nextMarker);
|
|
1562
|
+
}
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
function KeyboardPlugin({ onSubmit }) {
|
|
1566
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
1567
|
+
const {
|
|
1568
|
+
triggerSubmit,
|
|
1569
|
+
multiline,
|
|
1570
|
+
submitOnEnter,
|
|
1571
|
+
smartNewline,
|
|
1572
|
+
mode
|
|
1573
|
+
} = useComposerContext();
|
|
1574
|
+
react.useEffect(() => {
|
|
1575
|
+
const trySubmit = (event) => {
|
|
1576
|
+
event.preventDefault();
|
|
1577
|
+
triggerSubmit();
|
|
1578
|
+
onSubmit();
|
|
1579
|
+
return true;
|
|
1580
|
+
};
|
|
1581
|
+
return editor.registerCommand(
|
|
1582
|
+
lexical.KEY_ENTER_COMMAND,
|
|
1583
|
+
(event) => {
|
|
1584
|
+
if (!event) return false;
|
|
1585
|
+
if (document.querySelector('[data-composer-popover="open"]')) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
const isModEnter = event.metaKey || event.ctrlKey;
|
|
1589
|
+
const isShiftEnter = event.shiftKey;
|
|
1590
|
+
if (isModEnter) return trySubmit(event);
|
|
1591
|
+
if (isShiftEnter) {
|
|
1592
|
+
if (!multiline) {
|
|
1593
|
+
event.preventDefault();
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
const selection = lexical.$getSelection();
|
|
1597
|
+
if (!lexical.$isRangeSelection(selection)) return false;
|
|
1598
|
+
selection.insertParagraph();
|
|
1599
|
+
event.preventDefault();
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
let inCodeFence = false;
|
|
1603
|
+
let hasMultiLine = false;
|
|
1604
|
+
editor.getEditorState().read(() => {
|
|
1605
|
+
inCodeFence = $isInsideCodeFence();
|
|
1606
|
+
hasMultiLine = $hasMultiLineContent();
|
|
1607
|
+
});
|
|
1608
|
+
if (inCodeFence) {
|
|
1609
|
+
if (!multiline) {
|
|
1610
|
+
event.preventDefault();
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
if (!multiline) {
|
|
1616
|
+
if (!submitOnEnter) {
|
|
1617
|
+
event.preventDefault();
|
|
1618
|
+
return true;
|
|
1619
|
+
}
|
|
1620
|
+
return trySubmit(event);
|
|
1621
|
+
}
|
|
1622
|
+
if (smartNewline && mode === "markdown") {
|
|
1623
|
+
if ($handleListContinuation()) {
|
|
1624
|
+
event.preventDefault();
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (smartNewline && hasMultiLine) return false;
|
|
1629
|
+
if (!submitOnEnter) return false;
|
|
1630
|
+
return trySubmit(event);
|
|
1631
|
+
},
|
|
1632
|
+
lexical.COMMAND_PRIORITY_HIGH
|
|
1633
|
+
);
|
|
1634
|
+
}, [
|
|
1635
|
+
editor,
|
|
1636
|
+
onSubmit,
|
|
1637
|
+
triggerSubmit,
|
|
1638
|
+
multiline,
|
|
1639
|
+
submitOnEnter,
|
|
1640
|
+
smartNewline,
|
|
1641
|
+
mode
|
|
1642
|
+
]);
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/internal/focusEditor.ts
|
|
1647
|
+
function focusEditor(editor) {
|
|
1648
|
+
editor.focus(
|
|
1649
|
+
() => {
|
|
1650
|
+
const active = document.activeElement;
|
|
1651
|
+
const root = editor.getRootElement();
|
|
1652
|
+
if (root !== null && (active === null || !root.contains(active))) {
|
|
1653
|
+
root.focus({ preventScroll: true });
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
{ defaultSelection: "rootEnd" }
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/plugins/AutoFocusPlugin.tsx
|
|
1661
|
+
function AutoFocusPlugin({ enabled }) {
|
|
1662
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
1663
|
+
react.useEffect(() => {
|
|
1664
|
+
if (!enabled) return;
|
|
1665
|
+
focusEditor(editor);
|
|
1666
|
+
}, [editor, enabled]);
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
function PasteDropPlugin() {
|
|
1670
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
1671
|
+
const { addFiles, features, multiline, setIsDraggingFiles } = useComposerContext();
|
|
1672
|
+
react.useEffect(() => {
|
|
1673
|
+
return editor.registerCommand(
|
|
1674
|
+
lexical.PASTE_COMMAND,
|
|
1675
|
+
(event) => {
|
|
1676
|
+
if (!(event instanceof ClipboardEvent)) return false;
|
|
1677
|
+
const clipboard = event.clipboardData;
|
|
1678
|
+
if (!clipboard) return false;
|
|
1679
|
+
if (features.attachments) {
|
|
1680
|
+
const files = [];
|
|
1681
|
+
for (const item of clipboard.items) {
|
|
1682
|
+
if (item.kind === "file") {
|
|
1683
|
+
const file = item.getAsFile();
|
|
1684
|
+
if (file) files.push(file);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
if (files.length > 0) {
|
|
1688
|
+
event.preventDefault();
|
|
1689
|
+
addFiles(files);
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const text = clipboard.getData("text/plain");
|
|
1694
|
+
if (!text) return false;
|
|
1695
|
+
if (!multiline) {
|
|
1696
|
+
if (!text.includes("\n") && !text.includes("\r")) return false;
|
|
1697
|
+
event.preventDefault();
|
|
1698
|
+
const flat = text.replace(/\r\n?|\n/g, " ").replace(/\s+/g, " ");
|
|
1699
|
+
editor.update(() => {
|
|
1700
|
+
$insertTextWithParagraphBreaks(flat);
|
|
1701
|
+
});
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
if (!text.includes("\n")) return false;
|
|
1705
|
+
event.preventDefault();
|
|
1706
|
+
editor.update(() => {
|
|
1707
|
+
$insertTextWithParagraphBreaks(text);
|
|
1708
|
+
});
|
|
1709
|
+
return true;
|
|
1710
|
+
},
|
|
1711
|
+
lexical.COMMAND_PRIORITY_HIGH
|
|
1712
|
+
);
|
|
1713
|
+
}, [editor, features.attachments, multiline, addFiles]);
|
|
1714
|
+
react.useEffect(() => {
|
|
1715
|
+
if (!features.attachments) return;
|
|
1716
|
+
const root = editor.getRootElement();
|
|
1717
|
+
if (!root) return;
|
|
1718
|
+
let dragDepth = 0;
|
|
1719
|
+
const onDragEnter = (event) => {
|
|
1720
|
+
if (!event.dataTransfer?.types.includes("Files")) return;
|
|
1721
|
+
dragDepth += 1;
|
|
1722
|
+
setIsDraggingFiles(true);
|
|
1723
|
+
};
|
|
1724
|
+
const onDragOver = (event) => {
|
|
1725
|
+
if (event.dataTransfer?.types.includes("Files")) {
|
|
1726
|
+
event.preventDefault();
|
|
1727
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
const onDragLeave = (event) => {
|
|
1731
|
+
if (!event.dataTransfer?.types.includes("Files")) return;
|
|
1732
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
1733
|
+
if (dragDepth === 0) setIsDraggingFiles(false);
|
|
1734
|
+
};
|
|
1735
|
+
const onDrop = (event) => {
|
|
1736
|
+
dragDepth = 0;
|
|
1737
|
+
setIsDraggingFiles(false);
|
|
1738
|
+
const files = event.dataTransfer?.files;
|
|
1739
|
+
if (files && files.length > 0) {
|
|
1740
|
+
event.preventDefault();
|
|
1741
|
+
addFiles(Array.from(files));
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
root.addEventListener("dragenter", onDragEnter);
|
|
1745
|
+
root.addEventListener("dragover", onDragOver);
|
|
1746
|
+
root.addEventListener("dragleave", onDragLeave);
|
|
1747
|
+
root.addEventListener("drop", onDrop);
|
|
1748
|
+
return () => {
|
|
1749
|
+
root.removeEventListener("dragenter", onDragEnter);
|
|
1750
|
+
root.removeEventListener("dragover", onDragOver);
|
|
1751
|
+
root.removeEventListener("dragleave", onDragLeave);
|
|
1752
|
+
root.removeEventListener("drop", onDrop);
|
|
1753
|
+
};
|
|
1754
|
+
}, [editor, addFiles, features.attachments, setIsDraggingFiles]);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/plugins/markdown-tokenizer.ts
|
|
1759
|
+
var PAIRED_PATTERNS = [
|
|
1760
|
+
{ open: "**", close: "**", format: "bold" },
|
|
1761
|
+
{ open: "__", close: "__", format: "bold" },
|
|
1762
|
+
{ open: "~~", close: "~~", format: "strike" },
|
|
1763
|
+
{ open: "`", close: "`", format: "code" },
|
|
1764
|
+
{ open: "*", close: "*", format: "italic" },
|
|
1765
|
+
{ open: "_", close: "_", format: "italic" },
|
|
1766
|
+
{ open: "~", close: "~", format: "strike" }
|
|
1767
|
+
// Slack-style strike
|
|
1768
|
+
];
|
|
1769
|
+
var LINK_RE = /^(!?)\[([^\]\n]+)\]\(([^)\n\s]+)\)/;
|
|
1770
|
+
function isInvalidInner(inner) {
|
|
1771
|
+
if (inner.length === 0) return true;
|
|
1772
|
+
if (/\n/.test(inner)) return true;
|
|
1773
|
+
if (/^\s|\s$/.test(inner)) return true;
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
function tokenize(text) {
|
|
1777
|
+
const tokens = [];
|
|
1778
|
+
let i = 0;
|
|
1779
|
+
let buf = "";
|
|
1780
|
+
const flushBuf = () => {
|
|
1781
|
+
if (buf.length > 0) {
|
|
1782
|
+
tokens.push({ type: "text", text: buf });
|
|
1783
|
+
buf = "";
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
while (i < text.length) {
|
|
1787
|
+
const ch = text[i];
|
|
1788
|
+
if (ch === "[" || ch === "!" && text[i + 1] === "[") {
|
|
1789
|
+
const m = text.slice(i).match(LINK_RE);
|
|
1790
|
+
if (m) {
|
|
1791
|
+
const [whole, bang, label, url] = m;
|
|
1792
|
+
flushBuf();
|
|
1793
|
+
if (bang) {
|
|
1794
|
+
tokens.push({ type: "marker", text: "!", format: "link" });
|
|
1795
|
+
}
|
|
1796
|
+
tokens.push({ type: "marker", text: "[", format: "link" });
|
|
1797
|
+
tokens.push({ type: "formatted", text: label, format: "link" });
|
|
1798
|
+
tokens.push({ type: "marker", text: "](", format: "link" });
|
|
1799
|
+
tokens.push({ type: "formatted", text: url, format: "code" });
|
|
1800
|
+
tokens.push({ type: "marker", text: ")", format: "link" });
|
|
1801
|
+
i += whole.length;
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
let matched = false;
|
|
1806
|
+
for (const pat of PAIRED_PATTERNS) {
|
|
1807
|
+
if (!text.startsWith(pat.open, i)) continue;
|
|
1808
|
+
const searchStart = i + pat.open.length;
|
|
1809
|
+
let endIdx = -1;
|
|
1810
|
+
let probe = searchStart;
|
|
1811
|
+
while (probe < text.length) {
|
|
1812
|
+
const candidate = text.indexOf(pat.close, probe);
|
|
1813
|
+
if (candidate === -1) break;
|
|
1814
|
+
if (pat.open.length === 1) {
|
|
1815
|
+
const prev = text[candidate - 1];
|
|
1816
|
+
const next = text[candidate + 1];
|
|
1817
|
+
if (prev === pat.close || next === pat.close) {
|
|
1818
|
+
probe = candidate + 1;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
endIdx = candidate;
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
if (endIdx === -1) continue;
|
|
1826
|
+
const inner = text.slice(searchStart, endIdx);
|
|
1827
|
+
if (isInvalidInner(inner)) continue;
|
|
1828
|
+
if (pat.open.length === 1) {
|
|
1829
|
+
if (text[i + 1] === pat.open[0]) continue;
|
|
1830
|
+
if (text[i - 1] === pat.open[0]) continue;
|
|
1831
|
+
}
|
|
1832
|
+
flushBuf();
|
|
1833
|
+
tokens.push({ type: "marker", text: pat.open, format: pat.format });
|
|
1834
|
+
tokens.push({ type: "formatted", text: inner, format: pat.format });
|
|
1835
|
+
tokens.push({ type: "marker", text: pat.close, format: pat.format });
|
|
1836
|
+
i = endIdx + pat.close.length;
|
|
1837
|
+
matched = true;
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
if (matched) continue;
|
|
1841
|
+
buf += text[i];
|
|
1842
|
+
i += 1;
|
|
1843
|
+
}
|
|
1844
|
+
flushBuf();
|
|
1845
|
+
return tokens;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/plugins/MarkdownPlugin.tsx
|
|
1849
|
+
var FORMAT_FLAGS = {
|
|
1850
|
+
bold: 1,
|
|
1851
|
+
italic: 2,
|
|
1852
|
+
strike: 4,
|
|
1853
|
+
underline: 8,
|
|
1854
|
+
code: 16,
|
|
1855
|
+
link: 0
|
|
1856
|
+
};
|
|
1857
|
+
function readCurrentChildren(paragraph) {
|
|
1858
|
+
const out = [];
|
|
1859
|
+
for (const child of paragraph.getChildren()) {
|
|
1860
|
+
if ($isLinkTextNode(child)) {
|
|
1861
|
+
out.push({
|
|
1862
|
+
kind: "link",
|
|
1863
|
+
text: child.getTextContent(),
|
|
1864
|
+
format: child.getFormat(),
|
|
1865
|
+
url: child.getUrl()
|
|
1866
|
+
});
|
|
1867
|
+
} else if ($isMarkdownTokenNode(child)) {
|
|
1868
|
+
out.push({ kind: "token", text: child.getTextContent(), format: 0 });
|
|
1869
|
+
} else if (lexical.$isTextNode(child)) {
|
|
1870
|
+
out.push({
|
|
1871
|
+
kind: "text",
|
|
1872
|
+
text: child.getTextContent(),
|
|
1873
|
+
format: child.getFormat()
|
|
1874
|
+
});
|
|
1875
|
+
} else {
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return out;
|
|
1880
|
+
}
|
|
1881
|
+
function nodesEqual(a, b) {
|
|
1882
|
+
if (a.length !== b.length) return false;
|
|
1883
|
+
for (let i = 0; i < a.length; i++) {
|
|
1884
|
+
const ai = a[i];
|
|
1885
|
+
const bi = b[i];
|
|
1886
|
+
const aKind = ai.kind === "link" ? "text" : ai.kind;
|
|
1887
|
+
const bKind = bi.kind === "link" ? "text" : bi.kind;
|
|
1888
|
+
if (aKind !== bKind) return false;
|
|
1889
|
+
if (ai.text !== bi.text) return false;
|
|
1890
|
+
if (ai.format !== bi.format) return false;
|
|
1891
|
+
}
|
|
1892
|
+
return true;
|
|
1893
|
+
}
|
|
1894
|
+
function getSelectionOffsetWithin(paragraph) {
|
|
1895
|
+
const selection = lexical.$getSelection();
|
|
1896
|
+
if (!lexical.$isRangeSelection(selection)) return null;
|
|
1897
|
+
const measure = (key, offset) => {
|
|
1898
|
+
let acc = 0;
|
|
1899
|
+
for (const child of paragraph.getChildren()) {
|
|
1900
|
+
if (child.getKey() === key) return acc + offset;
|
|
1901
|
+
acc += child.getTextContentSize();
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
};
|
|
1905
|
+
const measureNodeOffset = (key, offset) => {
|
|
1906
|
+
if (paragraph.getKey() === key) {
|
|
1907
|
+
let acc = 0;
|
|
1908
|
+
const children = paragraph.getChildren();
|
|
1909
|
+
for (let i = 0; i < Math.min(offset, children.length); i++) {
|
|
1910
|
+
acc += children[i].getTextContentSize();
|
|
1911
|
+
}
|
|
1912
|
+
return acc;
|
|
1913
|
+
}
|
|
1914
|
+
return measure(key, offset);
|
|
1915
|
+
};
|
|
1916
|
+
const a = measureNodeOffset(selection.anchor.key, selection.anchor.offset);
|
|
1917
|
+
const f = measureNodeOffset(selection.focus.key, selection.focus.offset);
|
|
1918
|
+
if (a === null || f === null) return null;
|
|
1919
|
+
return { anchor: a, focus: f };
|
|
1920
|
+
}
|
|
1921
|
+
function setSelectionFromOffsets(paragraph, offsets) {
|
|
1922
|
+
const locate = (target) => {
|
|
1923
|
+
const children = paragraph.getChildren();
|
|
1924
|
+
if (children.length === 0) {
|
|
1925
|
+
return { key: paragraph.getKey(), offset: 0, type: "element" };
|
|
1926
|
+
}
|
|
1927
|
+
let acc = 0;
|
|
1928
|
+
for (const child of children) {
|
|
1929
|
+
const size = child.getTextContentSize();
|
|
1930
|
+
if (target <= acc + size) {
|
|
1931
|
+
return {
|
|
1932
|
+
key: child.getKey(),
|
|
1933
|
+
offset: Math.max(0, target - acc),
|
|
1934
|
+
type: "text"
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
acc += size;
|
|
1938
|
+
}
|
|
1939
|
+
const last = children[children.length - 1];
|
|
1940
|
+
return {
|
|
1941
|
+
key: last.getKey(),
|
|
1942
|
+
offset: last.getTextContentSize(),
|
|
1943
|
+
type: "text"
|
|
1944
|
+
};
|
|
1945
|
+
};
|
|
1946
|
+
const a = locate(offsets.anchor);
|
|
1947
|
+
const f = locate(offsets.focus);
|
|
1948
|
+
if (!a || !f) return;
|
|
1949
|
+
const selection = lexical.$getSelection();
|
|
1950
|
+
if (!lexical.$isRangeSelection(selection)) return;
|
|
1951
|
+
selection.anchor.set(a.key, a.offset, a.type);
|
|
1952
|
+
selection.focus.set(f.key, f.offset, f.type);
|
|
1953
|
+
}
|
|
1954
|
+
function isCodeKind(kind) {
|
|
1955
|
+
return kind === "code-line" || kind === "code-fence-open" || kind === "code-fence-close";
|
|
1956
|
+
}
|
|
1957
|
+
function buildCurrentFormatMap(paragraph) {
|
|
1958
|
+
const out = [];
|
|
1959
|
+
for (const child of paragraph.getChildren()) {
|
|
1960
|
+
if (!lexical.$isTextNode(child)) continue;
|
|
1961
|
+
const text = child.getTextContent();
|
|
1962
|
+
const fmt = $isMarkdownTokenNode(child) ? 0 : child.getFormat();
|
|
1963
|
+
for (let i = 0; i < text.length; i++) out.push(fmt);
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
}
|
|
1967
|
+
function buildKeptMask(tokens) {
|
|
1968
|
+
const mask = new Array(tokens.length);
|
|
1969
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1970
|
+
mask[i] = tokens[i].type !== "marker";
|
|
1971
|
+
}
|
|
1972
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1973
|
+
const t = tokens[i];
|
|
1974
|
+
if (t.type !== "marker") continue;
|
|
1975
|
+
if (t.text !== "[" && t.text !== "!") continue;
|
|
1976
|
+
const offset = t.text === "!" ? 1 : 0;
|
|
1977
|
+
const open = tokens[i + offset];
|
|
1978
|
+
const label = tokens[i + offset + 1];
|
|
1979
|
+
const mid = tokens[i + offset + 2];
|
|
1980
|
+
const url = tokens[i + offset + 3];
|
|
1981
|
+
const close = tokens[i + offset + 4];
|
|
1982
|
+
if (open?.type === "marker" && open.text === "[" && label?.type === "formatted" && label.format === "link" && mid?.type === "marker" && mid.text === "](" && url?.type === "formatted" && close?.type === "marker" && close.text === ")") {
|
|
1983
|
+
mask[i + offset + 3] = false;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return mask;
|
|
1987
|
+
}
|
|
1988
|
+
function buildLiveOffsetMap(text, leadingDrop, tokens, trailingDrop = 0) {
|
|
1989
|
+
const kept = new Array(text.length + 1);
|
|
1990
|
+
kept[0] = 0;
|
|
1991
|
+
let cursor = 0;
|
|
1992
|
+
for (let i = 0; i < leadingDrop && cursor < text.length; i++) {
|
|
1993
|
+
kept[cursor + 1] = kept[cursor];
|
|
1994
|
+
cursor++;
|
|
1995
|
+
}
|
|
1996
|
+
if (tokens) {
|
|
1997
|
+
const mask = buildKeptMask(tokens);
|
|
1998
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1999
|
+
const t = tokens[ti];
|
|
2000
|
+
const isKept = mask[ti];
|
|
2001
|
+
for (let i = 0; i < t.text.length && cursor < text.length; i++) {
|
|
2002
|
+
kept[cursor + 1] = kept[cursor] + (isKept ? 1 : 0);
|
|
2003
|
+
cursor++;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
const trailDropStart = text.length - trailingDrop;
|
|
2008
|
+
for (; cursor < text.length; cursor++) {
|
|
2009
|
+
const dropped = cursor >= trailDropStart;
|
|
2010
|
+
kept[cursor + 1] = kept[cursor] + (dropped ? 0 : 1);
|
|
2011
|
+
}
|
|
2012
|
+
return (srcOffset) => kept[Math.max(0, Math.min(srcOffset, text.length))];
|
|
2013
|
+
}
|
|
2014
|
+
function $applyStyling(paragraph, block, mode) {
|
|
2015
|
+
const text = paragraph.getTextContent();
|
|
2016
|
+
const current = readCurrentChildren(paragraph);
|
|
2017
|
+
if (current === null) return false;
|
|
2018
|
+
const desired = [];
|
|
2019
|
+
const isLive = mode === "live";
|
|
2020
|
+
const currentFmt = isLive ? buildCurrentFormatMap(paragraph) : null;
|
|
2021
|
+
const stashedNow = $isBlockParagraphNode(paragraph) ? paragraph.getBlockMarker() : "";
|
|
2022
|
+
let body = text;
|
|
2023
|
+
let stashedMarkerForOffsetMap = "";
|
|
2024
|
+
let hybridPromoteShift = 0;
|
|
2025
|
+
if (block.markerLen > 0 && text.length >= block.markerLen) {
|
|
2026
|
+
const marker = text.slice(0, block.markerLen);
|
|
2027
|
+
if (isLive) {
|
|
2028
|
+
if (stashedNow !== marker && $isBlockParagraphNode(paragraph)) {
|
|
2029
|
+
paragraph.setBlockMarker(marker);
|
|
2030
|
+
}
|
|
2031
|
+
stashedMarkerForOffsetMap = marker;
|
|
2032
|
+
body = text.slice(marker.length);
|
|
2033
|
+
} else {
|
|
2034
|
+
desired.push({ kind: "token", text: marker, format: 0 });
|
|
2035
|
+
body = text.slice(marker.length);
|
|
2036
|
+
if (stashedNow.length > 0 && $isBlockParagraphNode(paragraph)) {
|
|
2037
|
+
paragraph.setBlockMarker("");
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
} else if (!isLive && stashedNow.length > 0) {
|
|
2041
|
+
desired.push({ kind: "token", text: stashedNow, format: 0 });
|
|
2042
|
+
hybridPromoteShift = stashedNow.length;
|
|
2043
|
+
if ($isBlockParagraphNode(paragraph)) {
|
|
2044
|
+
paragraph.setBlockMarker("");
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
let srcPos = block.markerLen;
|
|
2048
|
+
let inlineTokens = null;
|
|
2049
|
+
let trailingDropForOffsetMap = 0;
|
|
2050
|
+
if (isCodeKind(block.kind)) {
|
|
2051
|
+
if (isLive && (block.kind === "code-fence-open" || block.kind === "code-fence-close") && body.length > 0 && $isBlockParagraphNode(paragraph)) {
|
|
2052
|
+
if (paragraph.getBlockMarker() !== body) {
|
|
2053
|
+
paragraph.setBlockMarker(body);
|
|
2054
|
+
}
|
|
2055
|
+
trailingDropForOffsetMap = body.length;
|
|
2056
|
+
body = "";
|
|
2057
|
+
} else if (body.length > 0) {
|
|
2058
|
+
desired.push({ kind: "text", text: body, format: 0 });
|
|
2059
|
+
}
|
|
2060
|
+
} else if (block.kind === "hr") {
|
|
2061
|
+
if (body.length > 0) {
|
|
2062
|
+
desired.push({ kind: "token", text: body, format: 0 });
|
|
2063
|
+
}
|
|
2064
|
+
} else {
|
|
2065
|
+
inlineTokens = tokenize(body);
|
|
2066
|
+
for (let ti = 0; ti < inlineTokens.length; ti++) {
|
|
2067
|
+
const t = inlineTokens[ti];
|
|
2068
|
+
const tokenLen = t.text.length;
|
|
2069
|
+
if (isLive && t.type === "marker" && (t.text === "[" || t.text === "!") && ti + 4 < inlineTokens.length) {
|
|
2070
|
+
const offset = t.text === "!" ? 1 : 0;
|
|
2071
|
+
const open = inlineTokens[ti + offset];
|
|
2072
|
+
const label = inlineTokens[ti + offset + 1];
|
|
2073
|
+
const mid = inlineTokens[ti + offset + 2];
|
|
2074
|
+
const url = inlineTokens[ti + offset + 3];
|
|
2075
|
+
const close = inlineTokens[ti + offset + 4];
|
|
2076
|
+
if (open?.type === "marker" && open.text === "[" && label?.type === "formatted" && label.format === "link" && mid?.type === "marker" && mid.text === "](" && url?.type === "formatted" && close?.type === "marker" && close.text === ")") {
|
|
2077
|
+
desired.push({
|
|
2078
|
+
kind: "link",
|
|
2079
|
+
text: label.text,
|
|
2080
|
+
format: 0,
|
|
2081
|
+
url: url.text
|
|
2082
|
+
});
|
|
2083
|
+
srcPos += t.text.length;
|
|
2084
|
+
if (offset === 1) {
|
|
2085
|
+
srcPos += open.text.length + label.text.length + mid.text.length + url.text.length + close.text.length;
|
|
2086
|
+
ti += 5;
|
|
2087
|
+
} else {
|
|
2088
|
+
srcPos += label.text.length + mid.text.length + url.text.length + close.text.length;
|
|
2089
|
+
ti += 4;
|
|
2090
|
+
}
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (t.type === "marker") {
|
|
2095
|
+
if (!isLive) {
|
|
2096
|
+
desired.push({ kind: "token", text: t.text, format: 0 });
|
|
2097
|
+
}
|
|
2098
|
+
} else if (t.type === "formatted") {
|
|
2099
|
+
desired.push({
|
|
2100
|
+
kind: "text",
|
|
2101
|
+
text: t.text,
|
|
2102
|
+
format: FORMAT_FLAGS[t.format]
|
|
2103
|
+
});
|
|
2104
|
+
} else if (isLive && currentFmt) {
|
|
2105
|
+
let runStart = 0;
|
|
2106
|
+
let runFmt = currentFmt[srcPos] ?? 0;
|
|
2107
|
+
for (let i = 1; i < tokenLen; i++) {
|
|
2108
|
+
const f = currentFmt[srcPos + i] ?? 0;
|
|
2109
|
+
if (f !== runFmt) {
|
|
2110
|
+
desired.push({
|
|
2111
|
+
kind: "text",
|
|
2112
|
+
text: t.text.slice(runStart, i),
|
|
2113
|
+
format: runFmt
|
|
2114
|
+
});
|
|
2115
|
+
runStart = i;
|
|
2116
|
+
runFmt = f;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
desired.push({
|
|
2120
|
+
kind: "text",
|
|
2121
|
+
text: t.text.slice(runStart),
|
|
2122
|
+
format: runFmt
|
|
2123
|
+
});
|
|
2124
|
+
} else {
|
|
2125
|
+
desired.push({ kind: "text", text: t.text, format: 0 });
|
|
2126
|
+
}
|
|
2127
|
+
srcPos += tokenLen;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
if (nodesEqual(current, desired)) return false;
|
|
2131
|
+
const preservedLinkUrls = /* @__PURE__ */ new Map();
|
|
2132
|
+
for (const child of paragraph.getChildren()) {
|
|
2133
|
+
if ($isLinkTextNode(child)) {
|
|
2134
|
+
const label = child.getTextContent();
|
|
2135
|
+
const url = child.getUrl();
|
|
2136
|
+
if (label && url && !preservedLinkUrls.has(label)) {
|
|
2137
|
+
preservedLinkUrls.set(label, url);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const srcOffsets = getSelectionOffsetWithin(paragraph);
|
|
2142
|
+
for (const child of paragraph.getChildren()) child.remove();
|
|
2143
|
+
for (const node of desired) {
|
|
2144
|
+
if (node.kind === "token") {
|
|
2145
|
+
paragraph.append($createMarkdownTokenNode(node.text));
|
|
2146
|
+
} else if (node.kind === "link") {
|
|
2147
|
+
const t = $createLinkTextNode(node.text, node.url ?? "");
|
|
2148
|
+
if (node.format !== 0) t.setFormat(node.format);
|
|
2149
|
+
paragraph.append(t);
|
|
2150
|
+
} else {
|
|
2151
|
+
const preservedUrl = preservedLinkUrls.get(node.text);
|
|
2152
|
+
const t = preservedUrl ? $createLinkTextNode(node.text, preservedUrl) : lexical.$createTextNode(node.text);
|
|
2153
|
+
if (node.format !== 0) t.setFormat(node.format);
|
|
2154
|
+
paragraph.append(t);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (srcOffsets) {
|
|
2158
|
+
if (isLive) {
|
|
2159
|
+
const map = buildLiveOffsetMap(
|
|
2160
|
+
text,
|
|
2161
|
+
stashedMarkerForOffsetMap.length,
|
|
2162
|
+
inlineTokens,
|
|
2163
|
+
trailingDropForOffsetMap
|
|
2164
|
+
);
|
|
2165
|
+
setSelectionFromOffsets(paragraph, {
|
|
2166
|
+
anchor: map(srcOffsets.anchor),
|
|
2167
|
+
focus: map(srcOffsets.focus)
|
|
2168
|
+
});
|
|
2169
|
+
} else {
|
|
2170
|
+
setSelectionFromOffsets(paragraph, {
|
|
2171
|
+
anchor: srcOffsets.anchor + hybridPromoteShift,
|
|
2172
|
+
focus: srcOffsets.focus + hybridPromoteShift
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
function $restyleAllParagraphs(mode) {
|
|
2179
|
+
const root = lexical.$getRoot();
|
|
2180
|
+
const map = $computeBlockMap();
|
|
2181
|
+
for (const child of root.getChildren()) {
|
|
2182
|
+
if (!lexical.$isParagraphNode(child)) continue;
|
|
2183
|
+
const info = map.get(child.getKey()) ?? $detectBlockFor(child);
|
|
2184
|
+
$applyStyling(child, info, mode);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
function $autoEscapeClosedFence(prevKinds) {
|
|
2188
|
+
const map = $computeBlockMap();
|
|
2189
|
+
const root = lexical.$getRoot();
|
|
2190
|
+
for (const child of root.getChildren()) {
|
|
2191
|
+
if (!lexical.$isParagraphNode(child)) continue;
|
|
2192
|
+
const key = child.getKey();
|
|
2193
|
+
const info = map.get(key);
|
|
2194
|
+
if (!info) continue;
|
|
2195
|
+
const oldKind = prevKinds.get(key);
|
|
2196
|
+
if (info.kind === "code-fence-close" && oldKind !== "code-fence-close" && child.getNextSibling() === null) {
|
|
2197
|
+
const next = lexical.$createParagraphNode();
|
|
2198
|
+
child.insertAfter(next);
|
|
2199
|
+
next.select();
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
prevKinds.clear();
|
|
2203
|
+
for (const [k, v] of map) prevKinds.set(k, v.kind);
|
|
2204
|
+
}
|
|
2205
|
+
var BLOCK_ATTR = "data-md-block";
|
|
2206
|
+
var LANG_ATTR = "data-md-lang";
|
|
2207
|
+
function syncBlockAttributes(editor) {
|
|
2208
|
+
editor.getEditorState().read(() => {
|
|
2209
|
+
const root = editor.getRootElement();
|
|
2210
|
+
if (!root) return;
|
|
2211
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2212
|
+
const map = $computeBlockMap();
|
|
2213
|
+
for (const [key, info] of map) {
|
|
2214
|
+
const el = editor.getElementByKey(key);
|
|
2215
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
2216
|
+
seen.add(el);
|
|
2217
|
+
if (info.kind === "paragraph") {
|
|
2218
|
+
if (el.hasAttribute(BLOCK_ATTR)) el.removeAttribute(BLOCK_ATTR);
|
|
2219
|
+
} else {
|
|
2220
|
+
if (el.getAttribute(BLOCK_ATTR) !== info.kind) {
|
|
2221
|
+
el.setAttribute(BLOCK_ATTR, info.kind);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
if (info.kind === "code-fence-open" && info.lang) {
|
|
2225
|
+
if (el.getAttribute(LANG_ATTR) !== info.lang) {
|
|
2226
|
+
el.setAttribute(LANG_ATTR, info.lang);
|
|
2227
|
+
}
|
|
2228
|
+
} else if (el.hasAttribute(LANG_ATTR)) {
|
|
2229
|
+
el.removeAttribute(LANG_ATTR);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
const stale = root.querySelectorAll(`[${BLOCK_ATTR}]`);
|
|
2233
|
+
stale.forEach((el) => {
|
|
2234
|
+
if (!seen.has(el)) {
|
|
2235
|
+
el.removeAttribute(BLOCK_ATTR);
|
|
2236
|
+
el.removeAttribute(LANG_ATTR);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
var RESTYLE_TAG = "md-restyle";
|
|
2242
|
+
function MarkdownPlugin() {
|
|
2243
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2244
|
+
const { markdownMode } = useComposerContext();
|
|
2245
|
+
const prevKindsRef = react.useRef(/* @__PURE__ */ new Map());
|
|
2246
|
+
const modeRef = react.useRef(markdownMode);
|
|
2247
|
+
modeRef.current = markdownMode;
|
|
2248
|
+
react.useEffect(() => {
|
|
2249
|
+
const prevKinds = prevKindsRef.current;
|
|
2250
|
+
editor.update(
|
|
2251
|
+
() => {
|
|
2252
|
+
$restyleAllParagraphs(modeRef.current);
|
|
2253
|
+
const map = $computeBlockMap();
|
|
2254
|
+
prevKinds.clear();
|
|
2255
|
+
for (const [k, v] of map) prevKinds.set(k, v.kind);
|
|
2256
|
+
},
|
|
2257
|
+
{ tag: RESTYLE_TAG }
|
|
2258
|
+
);
|
|
2259
|
+
syncBlockAttributes(editor);
|
|
2260
|
+
let scheduled = false;
|
|
2261
|
+
const unregisterUpdate = editor.registerUpdateListener(
|
|
2262
|
+
({ tags, dirtyElements, dirtyLeaves }) => {
|
|
2263
|
+
syncBlockAttributes(editor);
|
|
2264
|
+
if (tags.has(RESTYLE_TAG)) return;
|
|
2265
|
+
if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
|
|
2266
|
+
if (scheduled) return;
|
|
2267
|
+
scheduled = true;
|
|
2268
|
+
queueMicrotask(() => {
|
|
2269
|
+
scheduled = false;
|
|
2270
|
+
editor.update(
|
|
2271
|
+
() => {
|
|
2272
|
+
$restyleAllParagraphs(modeRef.current);
|
|
2273
|
+
$autoEscapeClosedFence(prevKinds);
|
|
2274
|
+
},
|
|
2275
|
+
{ tag: RESTYLE_TAG }
|
|
2276
|
+
);
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
);
|
|
2280
|
+
const unregisterBackspace = editor.registerCommand(
|
|
2281
|
+
lexical.KEY_BACKSPACE_COMMAND,
|
|
2282
|
+
() => {
|
|
2283
|
+
if (modeRef.current !== "live") return false;
|
|
2284
|
+
const selection = lexical.$getSelection();
|
|
2285
|
+
if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
2286
|
+
return false;
|
|
2287
|
+
}
|
|
2288
|
+
if (selection.anchor.offset !== 0) return false;
|
|
2289
|
+
const anchor = selection.anchor.getNode();
|
|
2290
|
+
const top = anchor.getTopLevelElement();
|
|
2291
|
+
if (!top || !lexical.$isParagraphNode(top)) return false;
|
|
2292
|
+
if (!$isBlockParagraphNode(top)) return false;
|
|
2293
|
+
if (!top.hasBlockMarker()) return false;
|
|
2294
|
+
if (selection.anchor.type === "element") {
|
|
2295
|
+
if (selection.anchor.getNode().getKey() !== top.getKey()) {
|
|
2296
|
+
return false;
|
|
2297
|
+
}
|
|
2298
|
+
} else {
|
|
2299
|
+
const first = top.getFirstChild();
|
|
2300
|
+
if (!first || first.getKey() !== anchor.getKey()) return false;
|
|
2301
|
+
}
|
|
2302
|
+
editor.update(() => {
|
|
2303
|
+
const latest = top.getLatest();
|
|
2304
|
+
if ($isBlockParagraphNode(latest)) {
|
|
2305
|
+
latest.setBlockMarker("");
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
return true;
|
|
2309
|
+
},
|
|
2310
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2311
|
+
);
|
|
2312
|
+
return () => {
|
|
2313
|
+
unregisterUpdate();
|
|
2314
|
+
unregisterBackspace();
|
|
2315
|
+
};
|
|
2316
|
+
}, [editor, markdownMode]);
|
|
2317
|
+
return null;
|
|
2318
|
+
}
|
|
2319
|
+
function Portal({ children }) {
|
|
2320
|
+
const [mounted, setMounted] = react.useState(false);
|
|
2321
|
+
react.useEffect(() => {
|
|
2322
|
+
setMounted(true);
|
|
2323
|
+
}, []);
|
|
2324
|
+
if (!mounted) return null;
|
|
2325
|
+
return reactDom.createPortal(children, document.body);
|
|
2326
|
+
}
|
|
2327
|
+
function ImageLightbox({ src, alt, onClose }) {
|
|
2328
|
+
const { icons, tokenStyle } = useComposerContext();
|
|
2329
|
+
const { close: CloseIcon } = icons;
|
|
2330
|
+
react.useEffect(() => {
|
|
2331
|
+
const onKey = (e) => {
|
|
2332
|
+
if (e.key === "Escape") onClose();
|
|
2333
|
+
};
|
|
2334
|
+
document.addEventListener("keydown", onKey);
|
|
2335
|
+
const prev = document.body.style.overflow;
|
|
2336
|
+
document.body.style.overflow = "hidden";
|
|
2337
|
+
return () => {
|
|
2338
|
+
document.removeEventListener("keydown", onKey);
|
|
2339
|
+
document.body.style.overflow = prev;
|
|
2340
|
+
};
|
|
2341
|
+
}, [onClose]);
|
|
2342
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2343
|
+
"div",
|
|
2344
|
+
{
|
|
2345
|
+
role: "dialog",
|
|
2346
|
+
"aria-modal": "true",
|
|
2347
|
+
className: "fixed inset-0 z-50 flex items-center justify-center p-6",
|
|
2348
|
+
style: tokenStyle,
|
|
2349
|
+
children: [
|
|
2350
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2351
|
+
"div",
|
|
2352
|
+
{
|
|
2353
|
+
"aria-hidden": true,
|
|
2354
|
+
className: "absolute inset-0 bg-foreground/70 backdrop-blur-sm",
|
|
2355
|
+
onClick: onClose
|
|
2356
|
+
}
|
|
2357
|
+
),
|
|
2358
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2359
|
+
"button",
|
|
2360
|
+
{
|
|
2361
|
+
type: "button",
|
|
2362
|
+
onClick: onClose,
|
|
2363
|
+
"aria-label": "Close",
|
|
2364
|
+
className: "absolute end-5 top-5 flex h-9 w-9 items-center justify-center rounded-full bg-card text-foreground shadow-soft transition-colors hover:bg-accent",
|
|
2365
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-4 w-4" })
|
|
2366
|
+
}
|
|
2367
|
+
),
|
|
2368
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2369
|
+
"img",
|
|
2370
|
+
{
|
|
2371
|
+
src,
|
|
2372
|
+
alt,
|
|
2373
|
+
className: "relative max-h-[85vh] max-w-[85vw] rounded-lg object-contain shadow-xl"
|
|
2374
|
+
}
|
|
2375
|
+
)
|
|
2376
|
+
]
|
|
2377
|
+
}
|
|
2378
|
+
) });
|
|
2379
|
+
}
|
|
2380
|
+
var FENCE_OPEN_MERMAID = /^```mermaid(?:\s.*)?$/;
|
|
2381
|
+
var FENCE_CLOSE = /^```\s*$/;
|
|
2382
|
+
var mermaidPromise = null;
|
|
2383
|
+
var mermaidInitialized = false;
|
|
2384
|
+
var mermaidMissingWarned = false;
|
|
2385
|
+
async function loadMermaid() {
|
|
2386
|
+
if (!mermaidPromise) {
|
|
2387
|
+
mermaidPromise = import('mermaid').then((m) => m.default).catch((err) => {
|
|
2388
|
+
if (!mermaidMissingWarned) {
|
|
2389
|
+
mermaidMissingWarned = true;
|
|
2390
|
+
console.warn(
|
|
2391
|
+
"[composeai] Failed to load the `mermaid` package. Either `npm install mermaid` or pass a `renderDiagram` prop to <Composer /> to render diagrams yourself.",
|
|
2392
|
+
err
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
return null;
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
const mermaid = await mermaidPromise;
|
|
2399
|
+
if (mermaid && !mermaidInitialized) {
|
|
2400
|
+
mermaid.initialize({
|
|
2401
|
+
startOnLoad: false,
|
|
2402
|
+
theme: "default",
|
|
2403
|
+
securityLevel: "strict",
|
|
2404
|
+
fontFamily: "inherit"
|
|
2405
|
+
});
|
|
2406
|
+
mermaidInitialized = true;
|
|
2407
|
+
}
|
|
2408
|
+
return mermaid;
|
|
2409
|
+
}
|
|
2410
|
+
function svgToDataUri(svg) {
|
|
2411
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
2412
|
+
}
|
|
2413
|
+
function MermaidPlugin() {
|
|
2414
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2415
|
+
const { features, icons, renderDiagram, classNames, sx } = useComposerContext();
|
|
2416
|
+
const { sparkle: SparkleIcon } = icons;
|
|
2417
|
+
const [diagrams, setDiagrams] = react.useState([]);
|
|
2418
|
+
const keepSource = typeof features.mermaid === "object" ? features.mermaid.keepSource !== false : true;
|
|
2419
|
+
react.useEffect(() => {
|
|
2420
|
+
const sync = () => {
|
|
2421
|
+
editor.getEditorState().read(() => {
|
|
2422
|
+
const found = [];
|
|
2423
|
+
const root = lexical.$getRoot();
|
|
2424
|
+
const children = root.getChildren();
|
|
2425
|
+
let i = 0;
|
|
2426
|
+
while (i < children.length) {
|
|
2427
|
+
const opener = children[i];
|
|
2428
|
+
if (!lexical.$isParagraphNode(opener) || !FENCE_OPEN_MERMAID.test(opener.getTextContent())) {
|
|
2429
|
+
i++;
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const paragraphKeys = [opener.getKey()];
|
|
2433
|
+
const codeLines = [];
|
|
2434
|
+
let j = i + 1;
|
|
2435
|
+
while (j < children.length) {
|
|
2436
|
+
const next = children[j];
|
|
2437
|
+
if (!lexical.$isParagraphNode(next)) break;
|
|
2438
|
+
const text = next.getTextContent();
|
|
2439
|
+
paragraphKeys.push(next.getKey());
|
|
2440
|
+
if (FENCE_CLOSE.test(text)) {
|
|
2441
|
+
j++;
|
|
2442
|
+
break;
|
|
2443
|
+
}
|
|
2444
|
+
codeLines.push(text);
|
|
2445
|
+
j++;
|
|
2446
|
+
}
|
|
2447
|
+
found.push({
|
|
2448
|
+
id: opener.getKey(),
|
|
2449
|
+
code: codeLines.join("\n").trim(),
|
|
2450
|
+
paragraphKeys
|
|
2451
|
+
});
|
|
2452
|
+
i = j;
|
|
2453
|
+
}
|
|
2454
|
+
setDiagrams((prev) => {
|
|
2455
|
+
if (prev.length === found.length && prev.every(
|
|
2456
|
+
(d, idx) => d.id === found[idx].id && d.code === found[idx].code && d.paragraphKeys.length === found[idx].paragraphKeys.length && d.paragraphKeys.every((k, kk) => k === found[idx].paragraphKeys[kk])
|
|
2457
|
+
)) {
|
|
2458
|
+
return prev;
|
|
2459
|
+
}
|
|
2460
|
+
return found;
|
|
2461
|
+
});
|
|
2462
|
+
});
|
|
2463
|
+
};
|
|
2464
|
+
sync();
|
|
2465
|
+
return editor.registerUpdateListener(sync);
|
|
2466
|
+
}, [editor]);
|
|
2467
|
+
const hiddenKeysRef = react.useRef(/* @__PURE__ */ new Set());
|
|
2468
|
+
react.useEffect(() => {
|
|
2469
|
+
const currentKeys = /* @__PURE__ */ new Set();
|
|
2470
|
+
for (const d of diagrams) for (const k of d.paragraphKeys) currentKeys.add(k);
|
|
2471
|
+
if (keepSource) {
|
|
2472
|
+
for (const key of hiddenKeysRef.current) {
|
|
2473
|
+
const el = editor.getElementByKey(key);
|
|
2474
|
+
if (el) el.style.removeProperty("display");
|
|
2475
|
+
}
|
|
2476
|
+
hiddenKeysRef.current.clear();
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
for (const key of hiddenKeysRef.current) {
|
|
2480
|
+
if (!currentKeys.has(key)) {
|
|
2481
|
+
const el = editor.getElementByKey(key);
|
|
2482
|
+
if (el) el.style.removeProperty("display");
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
for (const key of currentKeys) {
|
|
2486
|
+
const el = editor.getElementByKey(key);
|
|
2487
|
+
if (el) el.style.display = "none";
|
|
2488
|
+
}
|
|
2489
|
+
hiddenKeysRef.current = currentKeys;
|
|
2490
|
+
}, [diagrams, keepSource, editor]);
|
|
2491
|
+
react.useEffect(() => {
|
|
2492
|
+
return () => {
|
|
2493
|
+
for (const key of hiddenKeysRef.current) {
|
|
2494
|
+
const el = editor.getElementByKey(key);
|
|
2495
|
+
if (el) el.style.removeProperty("display");
|
|
2496
|
+
}
|
|
2497
|
+
hiddenKeysRef.current.clear();
|
|
2498
|
+
};
|
|
2499
|
+
}, [editor]);
|
|
2500
|
+
if (diagrams.length === 0) return null;
|
|
2501
|
+
const preview = slotProps(
|
|
2502
|
+
"mermaidPreview",
|
|
2503
|
+
"border-t border-border/60 bg-muted/30 px-4 py-3",
|
|
2504
|
+
classNames,
|
|
2505
|
+
sx
|
|
2506
|
+
);
|
|
2507
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ...preview, children: [
|
|
2508
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1.5 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
2509
|
+
/* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { className: "h-3 w-3" }),
|
|
2510
|
+
"Diagram preview"
|
|
2511
|
+
] }),
|
|
2512
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 overflow-x-auto scrollbar-thin", children: diagrams.map((d) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2513
|
+
DiagramTile,
|
|
2514
|
+
{
|
|
2515
|
+
diagram: d,
|
|
2516
|
+
renderDiagram
|
|
2517
|
+
},
|
|
2518
|
+
d.id
|
|
2519
|
+
)) })
|
|
2520
|
+
] });
|
|
2521
|
+
}
|
|
2522
|
+
function DiagramTile({ diagram, renderDiagram }) {
|
|
2523
|
+
if (renderDiagram) {
|
|
2524
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ConsumerTile, { diagram, renderDiagram });
|
|
2525
|
+
}
|
|
2526
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MermaidTile, { diagram });
|
|
2527
|
+
}
|
|
2528
|
+
function ConsumerTile({
|
|
2529
|
+
diagram,
|
|
2530
|
+
renderDiagram
|
|
2531
|
+
}) {
|
|
2532
|
+
let content = null;
|
|
2533
|
+
try {
|
|
2534
|
+
content = renderDiagram({ code: diagram.code, language: "mermaid" });
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
console.error("[composeai] renderDiagram threw", err);
|
|
2537
|
+
content = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-24 w-40 items-center justify-center px-2 text-center text-[10px] text-destructive", children: err instanceof Error ? err.message.slice(0, 80) : "Render failed" });
|
|
2538
|
+
}
|
|
2539
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 overflow-hidden rounded-lg border border-border bg-card", children: content });
|
|
2540
|
+
}
|
|
2541
|
+
function MermaidTile({ diagram }) {
|
|
2542
|
+
const { icons } = useComposerContext();
|
|
2543
|
+
const { zoom: ZoomIcon } = icons;
|
|
2544
|
+
const [svg, setSvg] = react.useState(null);
|
|
2545
|
+
const [error, setError] = react.useState(null);
|
|
2546
|
+
const [zoom, setZoom] = react.useState(false);
|
|
2547
|
+
const [mermaidMissing, setMermaidMissing] = react.useState(false);
|
|
2548
|
+
const renderId = react.useMemo(
|
|
2549
|
+
() => `mermaid-${diagram.id}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2550
|
+
[diagram.id]
|
|
2551
|
+
);
|
|
2552
|
+
react.useEffect(() => {
|
|
2553
|
+
let cancelled = false;
|
|
2554
|
+
if (!diagram.code) {
|
|
2555
|
+
setSvg(null);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
loadMermaid().then((mermaid) => {
|
|
2559
|
+
if (cancelled) return null;
|
|
2560
|
+
if (!mermaid) {
|
|
2561
|
+
setMermaidMissing(true);
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
return mermaid.render(renderId, diagram.code);
|
|
2565
|
+
}).then((result) => {
|
|
2566
|
+
if (cancelled || !result) return;
|
|
2567
|
+
setSvg(result.svg);
|
|
2568
|
+
setError(null);
|
|
2569
|
+
}).catch((err) => {
|
|
2570
|
+
if (cancelled) return;
|
|
2571
|
+
const msg = err instanceof Error ? err.message : "Render failed";
|
|
2572
|
+
setError(msg);
|
|
2573
|
+
setSvg(null);
|
|
2574
|
+
});
|
|
2575
|
+
return () => {
|
|
2576
|
+
cancelled = true;
|
|
2577
|
+
};
|
|
2578
|
+
}, [diagram.code, renderId]);
|
|
2579
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2580
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2581
|
+
"button",
|
|
2582
|
+
{
|
|
2583
|
+
type: "button",
|
|
2584
|
+
onClick: () => svg && setZoom(true),
|
|
2585
|
+
"aria-label": "Zoom diagram",
|
|
2586
|
+
className: "group/dia relative shrink-0 overflow-hidden rounded-lg border border-border bg-card transition-colors hover:border-primary/40",
|
|
2587
|
+
children: svg ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2588
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2589
|
+
"div",
|
|
2590
|
+
{
|
|
2591
|
+
className: "h-24 w-40 [&_svg]:h-full [&_svg]:w-full",
|
|
2592
|
+
dangerouslySetInnerHTML: { __html: svg }
|
|
2593
|
+
}
|
|
2594
|
+
),
|
|
2595
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute end-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-foreground/70 text-background opacity-0 transition-opacity group-hover/dia:opacity-100", children: /* @__PURE__ */ jsxRuntime.jsx(ZoomIcon, { className: "h-3 w-3" }) })
|
|
2596
|
+
] }) : mermaidMissing ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid h-24 w-40 place-items-center px-3 text-center text-[11px] leading-snug text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
2597
|
+
"Install ",
|
|
2598
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "rounded bg-muted px-1 font-mono", children: "mermaid" }),
|
|
2599
|
+
" ",
|
|
2600
|
+
"or pass",
|
|
2601
|
+
" ",
|
|
2602
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "rounded bg-muted px-1 font-mono", children: "renderDiagram" })
|
|
2603
|
+
] }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid h-24 w-40 place-items-center px-2 text-center text-[10px] leading-snug text-destructive", children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: error.slice(0, 80) }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid h-24 w-40 place-items-center text-[10px] text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Rendering\u2026" }) })
|
|
2604
|
+
}
|
|
2605
|
+
),
|
|
2606
|
+
zoom && svg && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2607
|
+
ImageLightbox,
|
|
2608
|
+
{
|
|
2609
|
+
src: svgToDataUri(svg),
|
|
2610
|
+
alt: "Mermaid diagram",
|
|
2611
|
+
onClose: () => setZoom(false)
|
|
2612
|
+
}
|
|
2613
|
+
)
|
|
2614
|
+
] });
|
|
2615
|
+
}
|
|
2616
|
+
function SlashMenu({
|
|
2617
|
+
options,
|
|
2618
|
+
selectedIndex,
|
|
2619
|
+
isLoading = false,
|
|
2620
|
+
onSelect,
|
|
2621
|
+
onHover
|
|
2622
|
+
}) {
|
|
2623
|
+
const listRef = react.useRef(null);
|
|
2624
|
+
const { classNames, sx } = useComposerContext();
|
|
2625
|
+
react.useEffect(() => {
|
|
2626
|
+
const el = listRef.current?.querySelector(
|
|
2627
|
+
`[data-index="${selectedIndex}"]`
|
|
2628
|
+
);
|
|
2629
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
2630
|
+
}, [selectedIndex]);
|
|
2631
|
+
const grouped = options.reduce(
|
|
2632
|
+
(acc, item, index) => {
|
|
2633
|
+
const g = item.group ?? "Commands";
|
|
2634
|
+
if (!acc[g]) acc[g] = [];
|
|
2635
|
+
acc[g].push({ item, index });
|
|
2636
|
+
return acc;
|
|
2637
|
+
},
|
|
2638
|
+
{}
|
|
2639
|
+
);
|
|
2640
|
+
const menu = slotProps(
|
|
2641
|
+
"slashMenu",
|
|
2642
|
+
"z-50 w-72 origin-top animate-slide-up overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-soft",
|
|
2643
|
+
classNames,
|
|
2644
|
+
sx
|
|
2645
|
+
);
|
|
2646
|
+
const itemStyle = react.useMemo(() => resolveSx(sx?.slashItem), [sx]);
|
|
2647
|
+
const showSkeleton = isLoading && options.length === 0;
|
|
2648
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2649
|
+
"div",
|
|
2650
|
+
{
|
|
2651
|
+
"data-composer-popover": "open",
|
|
2652
|
+
role: "listbox",
|
|
2653
|
+
"aria-label": "Slash commands",
|
|
2654
|
+
"aria-busy": isLoading || void 0,
|
|
2655
|
+
...menu,
|
|
2656
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { ref: listRef, className: "max-h-72 overflow-y-auto scrollbar-thin py-1", children: [
|
|
2657
|
+
showSkeleton && /* @__PURE__ */ jsxRuntime.jsx(SlashSkeleton, { rows: 4 }),
|
|
2658
|
+
Object.entries(grouped).map(([group, entries]) => /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
2659
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: group }),
|
|
2660
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: entries.map(({ item, index }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2661
|
+
"li",
|
|
2662
|
+
{
|
|
2663
|
+
"data-index": index,
|
|
2664
|
+
role: "option",
|
|
2665
|
+
"aria-selected": selectedIndex === index,
|
|
2666
|
+
onMouseDown: (e) => {
|
|
2667
|
+
e.preventDefault();
|
|
2668
|
+
onSelect(index);
|
|
2669
|
+
},
|
|
2670
|
+
onMouseEnter: () => onHover(index),
|
|
2671
|
+
className: cn(
|
|
2672
|
+
"flex cursor-pointer items-center gap-2.5 px-2.5 py-1.5 text-sm",
|
|
2673
|
+
selectedIndex === index ? "bg-accent text-accent-foreground" : "text-foreground",
|
|
2674
|
+
classNames?.slashItem
|
|
2675
|
+
),
|
|
2676
|
+
style: itemStyle,
|
|
2677
|
+
children: [
|
|
2678
|
+
item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: item.icon }),
|
|
2679
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col leading-tight", children: [
|
|
2680
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium", children: item.label }),
|
|
2681
|
+
item.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: item.description })
|
|
2682
|
+
] }),
|
|
2683
|
+
item.shortcut && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ms-auto rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground", children: item.shortcut })
|
|
2684
|
+
]
|
|
2685
|
+
},
|
|
2686
|
+
item.id
|
|
2687
|
+
)) })
|
|
2688
|
+
] }, group))
|
|
2689
|
+
] })
|
|
2690
|
+
}
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
function SlashSkeleton({ rows = 4 }) {
|
|
2694
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { "aria-hidden": "true", className: "px-2.5 py-1.5", children: [
|
|
2695
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-0.5 pb-1.5 pt-0.5", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block h-2 w-16 animate-pulse rounded bg-muted/70" }) }),
|
|
2696
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2697
|
+
"li",
|
|
2698
|
+
{
|
|
2699
|
+
className: "flex items-center gap-2.5 rounded-md px-0 py-1.5",
|
|
2700
|
+
children: [
|
|
2701
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-7 w-7 shrink-0 animate-pulse rounded-md bg-muted" }),
|
|
2702
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
|
|
2703
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2704
|
+
"span",
|
|
2705
|
+
{
|
|
2706
|
+
className: "h-2.5 animate-pulse rounded bg-muted",
|
|
2707
|
+
style: { width: `${50 + i * 19 % 35}%` }
|
|
2708
|
+
}
|
|
2709
|
+
),
|
|
2710
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2711
|
+
"span",
|
|
2712
|
+
{
|
|
2713
|
+
className: "h-2 animate-pulse rounded bg-muted/70",
|
|
2714
|
+
style: { width: `${30 + i * 13 % 30}%` }
|
|
2715
|
+
}
|
|
2716
|
+
)
|
|
2717
|
+
] })
|
|
2718
|
+
]
|
|
2719
|
+
},
|
|
2720
|
+
i
|
|
2721
|
+
)) }),
|
|
2722
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading commands\u2026" })
|
|
2723
|
+
] });
|
|
2724
|
+
}
|
|
2725
|
+
var MARGIN = 8;
|
|
2726
|
+
function findComposerRoot(anchor) {
|
|
2727
|
+
const active = document.activeElement;
|
|
2728
|
+
if (active instanceof HTMLElement) {
|
|
2729
|
+
const root = active.closest("[data-composer-root]");
|
|
2730
|
+
if (root) return root;
|
|
2731
|
+
}
|
|
2732
|
+
if (anchor) {
|
|
2733
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
2734
|
+
const x = anchorRect.left + 1;
|
|
2735
|
+
const y = anchorRect.top + 1;
|
|
2736
|
+
const el = document.elementFromPoint(x, y);
|
|
2737
|
+
if (el instanceof HTMLElement) {
|
|
2738
|
+
const root = el.closest("[data-composer-root]");
|
|
2739
|
+
if (root) return root;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return document.querySelector("[data-composer-root]");
|
|
2743
|
+
}
|
|
2744
|
+
function rectFromDom(rect) {
|
|
2745
|
+
return {
|
|
2746
|
+
left: rect.left,
|
|
2747
|
+
right: rect.right,
|
|
2748
|
+
top: rect.top,
|
|
2749
|
+
bottom: rect.bottom,
|
|
2750
|
+
width: rect.width,
|
|
2751
|
+
height: rect.height
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
var WORD_OR_SPACE = /[A-Za-z0-9_\s\u0590-\u05FF\u0600-\u06FF\u4E00-\u9FFF]/;
|
|
2755
|
+
function isBlockRtl(node) {
|
|
2756
|
+
let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
2757
|
+
while (el) {
|
|
2758
|
+
const display = getComputedStyle(el).display;
|
|
2759
|
+
if (display && display !== "inline" && display !== "contents") {
|
|
2760
|
+
return getComputedStyle(el).direction === "rtl";
|
|
2761
|
+
}
|
|
2762
|
+
el = el.parentElement;
|
|
2763
|
+
}
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
function readTriggerInfo() {
|
|
2767
|
+
if (typeof window === "undefined") return null;
|
|
2768
|
+
const sel = window.getSelection?.();
|
|
2769
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
2770
|
+
const range = sel.getRangeAt(0);
|
|
2771
|
+
const node = range.startContainer;
|
|
2772
|
+
if (node.nodeType !== Node.TEXT_NODE) return null;
|
|
2773
|
+
const text = node.data;
|
|
2774
|
+
const cursorOffset = range.startOffset;
|
|
2775
|
+
let triggerOffset = -1;
|
|
2776
|
+
const lookbackStart = Math.max(0, cursorOffset - 64);
|
|
2777
|
+
for (let i = cursorOffset - 1; i >= lookbackStart; i--) {
|
|
2778
|
+
const c = text[i];
|
|
2779
|
+
if (WORD_OR_SPACE.test(c)) continue;
|
|
2780
|
+
triggerOffset = i;
|
|
2781
|
+
break;
|
|
2782
|
+
}
|
|
2783
|
+
if (triggerOffset < 0) return null;
|
|
2784
|
+
const triggerRange = document.createRange();
|
|
2785
|
+
try {
|
|
2786
|
+
triggerRange.setStart(node, triggerOffset);
|
|
2787
|
+
triggerRange.setEnd(node, triggerOffset + 1);
|
|
2788
|
+
} catch {
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
const rect = triggerRange.getBoundingClientRect();
|
|
2792
|
+
if (rect.height === 0 && rect.width === 0) return null;
|
|
2793
|
+
return {
|
|
2794
|
+
rect: rectFromDom(rect),
|
|
2795
|
+
rtl: isBlockRtl(node) ?? false
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
function readFallbackInfo(fallback, dirHint) {
|
|
2799
|
+
const sel = typeof window !== "undefined" ? window.getSelection?.() : null;
|
|
2800
|
+
let rect;
|
|
2801
|
+
if (sel && sel.rangeCount > 0) {
|
|
2802
|
+
const r = sel.getRangeAt(0).getBoundingClientRect();
|
|
2803
|
+
rect = r.height > 0 || r.width > 0 ? rectFromDom(r) : rectFromDom(fallback.getBoundingClientRect());
|
|
2804
|
+
const blockRtl = sel.rangeCount > 0 ? isBlockRtl(sel.getRangeAt(0).startContainer) : null;
|
|
2805
|
+
if (blockRtl !== null) return { rect, rtl: blockRtl };
|
|
2806
|
+
} else {
|
|
2807
|
+
rect = rectFromDom(fallback.getBoundingClientRect());
|
|
2808
|
+
}
|
|
2809
|
+
if (dirHint === "rtl") return { rect, rtl: true };
|
|
2810
|
+
if (dirHint === "ltr") return { rect, rtl: false };
|
|
2811
|
+
const root = fallback.closest("[data-composer-root]") ?? document.querySelector("[data-composer-root]");
|
|
2812
|
+
const editor = root?.querySelector(".composer-editor");
|
|
2813
|
+
const probe = editor ?? root ?? fallback;
|
|
2814
|
+
return { rect, rtl: getComputedStyle(probe).direction === "rtl" };
|
|
2815
|
+
}
|
|
2816
|
+
function SmartPopover({ children, gap = 6 }) {
|
|
2817
|
+
const ref = react.useRef(null);
|
|
2818
|
+
const [placement, setPlacement] = react.useState(null);
|
|
2819
|
+
const { tokenStyle, dir } = useComposerContext();
|
|
2820
|
+
react.useLayoutEffect(() => {
|
|
2821
|
+
const el = ref.current;
|
|
2822
|
+
if (!el) return;
|
|
2823
|
+
const anchor = el.parentElement;
|
|
2824
|
+
if (!anchor) return;
|
|
2825
|
+
const update = () => {
|
|
2826
|
+
const info = readTriggerInfo() ?? readFallbackInfo(anchor, dir);
|
|
2827
|
+
const triggerRect = info.rect;
|
|
2828
|
+
const menuHeight = el.offsetHeight;
|
|
2829
|
+
const menuWidth = el.offsetWidth;
|
|
2830
|
+
const viewportH = window.innerHeight;
|
|
2831
|
+
const viewportW = window.innerWidth;
|
|
2832
|
+
const baseLeft = info.rtl ? triggerRect.right - menuWidth : triggerRect.left;
|
|
2833
|
+
let left = baseLeft;
|
|
2834
|
+
if (left + menuWidth + MARGIN > viewportW) {
|
|
2835
|
+
left = viewportW - menuWidth - MARGIN;
|
|
2836
|
+
}
|
|
2837
|
+
if (left < MARGIN) left = MARGIN;
|
|
2838
|
+
const spaceBelow = viewportH - triggerRect.bottom;
|
|
2839
|
+
const spaceAbove = triggerRect.top;
|
|
2840
|
+
const wantsAbove = spaceBelow < menuHeight + MARGIN && spaceAbove > spaceBelow;
|
|
2841
|
+
let next;
|
|
2842
|
+
if (wantsAbove) {
|
|
2843
|
+
const composerRoot = findComposerRoot(anchor);
|
|
2844
|
+
const topEdge = composerRoot ? composerRoot.getBoundingClientRect().top : triggerRect.top;
|
|
2845
|
+
next = {
|
|
2846
|
+
left,
|
|
2847
|
+
bottom: Math.max(MARGIN, viewportH - topEdge + gap),
|
|
2848
|
+
rtl: info.rtl
|
|
2849
|
+
};
|
|
2850
|
+
} else {
|
|
2851
|
+
next = { left, top: triggerRect.bottom + gap, rtl: info.rtl };
|
|
2852
|
+
}
|
|
2853
|
+
setPlacement(
|
|
2854
|
+
(prev) => prev && prev.left === next.left && prev.top === next.top && prev.bottom === next.bottom && prev.rtl === next.rtl ? prev : next
|
|
2855
|
+
);
|
|
2856
|
+
};
|
|
2857
|
+
update();
|
|
2858
|
+
const ro = new ResizeObserver(update);
|
|
2859
|
+
ro.observe(el);
|
|
2860
|
+
ro.observe(anchor);
|
|
2861
|
+
document.addEventListener("selectionchange", update);
|
|
2862
|
+
window.addEventListener("resize", update);
|
|
2863
|
+
window.addEventListener("scroll", update, true);
|
|
2864
|
+
return () => {
|
|
2865
|
+
ro.disconnect();
|
|
2866
|
+
document.removeEventListener("selectionchange", update);
|
|
2867
|
+
window.removeEventListener("resize", update);
|
|
2868
|
+
window.removeEventListener("scroll", update, true);
|
|
2869
|
+
};
|
|
2870
|
+
}, [children, gap, dir]);
|
|
2871
|
+
const style = placement === null ? {
|
|
2872
|
+
position: "fixed",
|
|
2873
|
+
left: 0,
|
|
2874
|
+
top: 0,
|
|
2875
|
+
visibility: "hidden",
|
|
2876
|
+
zIndex: 60
|
|
2877
|
+
} : {
|
|
2878
|
+
position: "fixed",
|
|
2879
|
+
left: placement.left,
|
|
2880
|
+
top: placement.top,
|
|
2881
|
+
bottom: placement.bottom,
|
|
2882
|
+
zIndex: 60
|
|
2883
|
+
};
|
|
2884
|
+
const mergedStyle = tokenStyle ? { ...tokenStyle, ...style } : style;
|
|
2885
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2886
|
+
"div",
|
|
2887
|
+
{
|
|
2888
|
+
ref,
|
|
2889
|
+
"data-composer-popover-placement": placement?.bottom !== void 0 ? "above-composer" : "below",
|
|
2890
|
+
dir: placement?.rtl ? "rtl" : "ltr",
|
|
2891
|
+
style: mergedStyle,
|
|
2892
|
+
children
|
|
2893
|
+
}
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
function useOutsideClickDismiss(editor, enabled) {
|
|
2897
|
+
react.useEffect(() => {
|
|
2898
|
+
if (!enabled) return;
|
|
2899
|
+
const dismiss = () => {
|
|
2900
|
+
const event = new KeyboardEvent("keydown", { key: "Escape" });
|
|
2901
|
+
editor.dispatchCommand(lexical.KEY_ESCAPE_COMMAND, event);
|
|
2902
|
+
};
|
|
2903
|
+
const isOutside = (target) => {
|
|
2904
|
+
if (!(target instanceof Node)) return false;
|
|
2905
|
+
const popover = document.querySelector(
|
|
2906
|
+
"[data-composer-popover-placement]"
|
|
2907
|
+
);
|
|
2908
|
+
if (!popover) return false;
|
|
2909
|
+
if (popover.contains(target)) return false;
|
|
2910
|
+
const el = target instanceof Element ? target : target.parentElement;
|
|
2911
|
+
if (!el) return true;
|
|
2912
|
+
if (el.closest("[data-composer-root]")) return false;
|
|
2913
|
+
return true;
|
|
2914
|
+
};
|
|
2915
|
+
const onPointer = (e) => {
|
|
2916
|
+
const target = e instanceof TouchEvent ? e.touches[0]?.target ?? null : e.target;
|
|
2917
|
+
if (isOutside(target)) dismiss();
|
|
2918
|
+
};
|
|
2919
|
+
document.addEventListener("mousedown", onPointer, true);
|
|
2920
|
+
document.addEventListener("touchstart", onPointer, true);
|
|
2921
|
+
return () => {
|
|
2922
|
+
document.removeEventListener("mousedown", onPointer, true);
|
|
2923
|
+
document.removeEventListener("touchstart", onPointer, true);
|
|
2924
|
+
};
|
|
2925
|
+
}, [editor, enabled]);
|
|
2926
|
+
}
|
|
2927
|
+
var SlashOption = class extends LexicalTypeaheadMenuPlugin.MenuOption {
|
|
2928
|
+
command;
|
|
2929
|
+
constructor(command) {
|
|
2930
|
+
super(command.id);
|
|
2931
|
+
this.command = command;
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
function isSyncItems(items) {
|
|
2935
|
+
return Array.isArray(items);
|
|
2936
|
+
}
|
|
2937
|
+
function SlashCommandPlugin({ config, onSubmit }) {
|
|
2938
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2939
|
+
const { closeMenusOnOutsideClick } = useComposerContext();
|
|
2940
|
+
const [query, setQuery] = react.useState("");
|
|
2941
|
+
const [asyncItems, setAsyncItems] = react.useState(null);
|
|
2942
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
2943
|
+
!isSyncItems(config.items)
|
|
2944
|
+
);
|
|
2945
|
+
useOutsideClickDismiss(editor, closeMenusOnOutsideClick);
|
|
2946
|
+
const triggerFn = LexicalTypeaheadMenuPlugin.useBasicTypeaheadTriggerMatch(
|
|
2947
|
+
config.trigger ?? "/",
|
|
2948
|
+
{ minLength: 0, maxLength: 32, allowWhitespace: false }
|
|
2949
|
+
);
|
|
2950
|
+
react.useEffect(() => {
|
|
2951
|
+
if (isSyncItems(config.items)) {
|
|
2952
|
+
setIsLoading(false);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
let cancelled = false;
|
|
2956
|
+
setIsLoading(true);
|
|
2957
|
+
Promise.resolve(config.items(query)).then((res) => {
|
|
2958
|
+
if (cancelled) return;
|
|
2959
|
+
setAsyncItems(res);
|
|
2960
|
+
setIsLoading(false);
|
|
2961
|
+
});
|
|
2962
|
+
return () => {
|
|
2963
|
+
cancelled = true;
|
|
2964
|
+
};
|
|
2965
|
+
}, [query, config.items]);
|
|
2966
|
+
const allItems = react.useMemo(() => {
|
|
2967
|
+
return isSyncItems(config.items) ? config.items : asyncItems ?? [];
|
|
2968
|
+
}, [config.items, asyncItems]);
|
|
2969
|
+
const options = react.useMemo(() => {
|
|
2970
|
+
const q = query.trim().toLowerCase();
|
|
2971
|
+
const max = config.maxItems ?? 8;
|
|
2972
|
+
const filtered = q ? allItems.filter((it) => {
|
|
2973
|
+
const hay = `${it.label} ${it.description ?? ""} ${it.group ?? ""}`.toLowerCase();
|
|
2974
|
+
return hay.includes(q);
|
|
2975
|
+
}) : allItems;
|
|
2976
|
+
return filtered.slice(0, max).map((c) => new SlashOption(c));
|
|
2977
|
+
}, [allItems, query, config.maxItems]);
|
|
2978
|
+
const onSelectOption = react.useCallback(
|
|
2979
|
+
(selectedOption, nodeToReplace, closeMenu, matchingString) => {
|
|
2980
|
+
editor.update(() => {
|
|
2981
|
+
if (nodeToReplace) nodeToReplace.remove();
|
|
2982
|
+
});
|
|
2983
|
+
const ctx = {
|
|
2984
|
+
insertText: (text) => {
|
|
2985
|
+
editor.update(() => {
|
|
2986
|
+
const sel = lexical.$getSelection();
|
|
2987
|
+
if (lexical.$isRangeSelection(sel)) sel.insertText(text);
|
|
2988
|
+
});
|
|
2989
|
+
},
|
|
2990
|
+
insertMarkdown: (md) => {
|
|
2991
|
+
editor.update(() => {
|
|
2992
|
+
const sel = lexical.$getSelection();
|
|
2993
|
+
if (lexical.$isRangeSelection(sel)) sel.insertText(md);
|
|
2994
|
+
});
|
|
2995
|
+
},
|
|
2996
|
+
cancel: () => closeMenu(),
|
|
2997
|
+
submit: () => onSubmit()
|
|
2998
|
+
};
|
|
2999
|
+
selectedOption.command.onSelect?.(ctx);
|
|
3000
|
+
closeMenu();
|
|
3001
|
+
},
|
|
3002
|
+
[editor, onSubmit]
|
|
3003
|
+
);
|
|
3004
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3005
|
+
LexicalTypeaheadMenuPlugin.LexicalTypeaheadMenuPlugin,
|
|
3006
|
+
{
|
|
3007
|
+
onQueryChange: (s) => setQuery(s ?? ""),
|
|
3008
|
+
onSelectOption,
|
|
3009
|
+
triggerFn,
|
|
3010
|
+
options,
|
|
3011
|
+
menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
|
3012
|
+
if (!anchorElementRef.current) return null;
|
|
3013
|
+
if (options.length === 0 && !isLoading) return null;
|
|
3014
|
+
return reactDom.createPortal(
|
|
3015
|
+
/* @__PURE__ */ jsxRuntime.jsx(SmartPopover, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3016
|
+
SlashMenu,
|
|
3017
|
+
{
|
|
3018
|
+
options: options.map((o) => o.command),
|
|
3019
|
+
selectedIndex: selectedIndex ?? 0,
|
|
3020
|
+
isLoading,
|
|
3021
|
+
onSelect: (index) => selectOptionAndCleanUp(options[index]),
|
|
3022
|
+
onHover: (index) => setHighlightedIndex(index)
|
|
3023
|
+
}
|
|
3024
|
+
) }),
|
|
3025
|
+
anchorElementRef.current
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
function Avatar({ src, alt, size = 28, className }) {
|
|
3032
|
+
const [errored, setErrored] = react.useState(false);
|
|
3033
|
+
const showImage = src && !errored;
|
|
3034
|
+
const initial = (alt || "?").slice(0, 1).toUpperCase();
|
|
3035
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3036
|
+
"span",
|
|
3037
|
+
{
|
|
3038
|
+
className: "inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-primary/10 text-xs font-semibold text-primary" + (className ? ` ${className}` : ""),
|
|
3039
|
+
style: { width: size, height: size },
|
|
3040
|
+
children: showImage ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3041
|
+
"img",
|
|
3042
|
+
{
|
|
3043
|
+
src,
|
|
3044
|
+
alt,
|
|
3045
|
+
className: "h-full w-full object-cover",
|
|
3046
|
+
onError: () => setErrored(true)
|
|
3047
|
+
}
|
|
3048
|
+
) : initial
|
|
3049
|
+
}
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
function MentionMenu({
|
|
3053
|
+
options,
|
|
3054
|
+
selectedIndex,
|
|
3055
|
+
isLoading = false,
|
|
3056
|
+
onSelect,
|
|
3057
|
+
onHover
|
|
3058
|
+
}) {
|
|
3059
|
+
const listRef = react.useRef(null);
|
|
3060
|
+
const { classNames, sx } = useComposerContext();
|
|
3061
|
+
react.useEffect(() => {
|
|
3062
|
+
const el = listRef.current?.querySelector(
|
|
3063
|
+
`[data-index="${selectedIndex}"]`
|
|
3064
|
+
);
|
|
3065
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
3066
|
+
}, [selectedIndex]);
|
|
3067
|
+
const menu = slotProps(
|
|
3068
|
+
"mentionMenu",
|
|
3069
|
+
"z-50 w-64 origin-top animate-slide-up overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-soft",
|
|
3070
|
+
classNames,
|
|
3071
|
+
sx
|
|
3072
|
+
);
|
|
3073
|
+
const itemStyle = react.useMemo(() => resolveSx(sx?.mentionItem), [sx]);
|
|
3074
|
+
const showSkeleton = isLoading && options.length === 0;
|
|
3075
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3076
|
+
"div",
|
|
3077
|
+
{
|
|
3078
|
+
"data-composer-popover": "open",
|
|
3079
|
+
role: "listbox",
|
|
3080
|
+
"aria-label": "Mentions",
|
|
3081
|
+
"aria-busy": isLoading || void 0,
|
|
3082
|
+
...menu,
|
|
3083
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { ref: listRef, className: "max-h-72 overflow-y-auto scrollbar-thin py-1", children: [
|
|
3084
|
+
showSkeleton ? /* @__PURE__ */ jsxRuntime.jsx(MentionSkeleton, { rows: 3 }) : null,
|
|
3085
|
+
options.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3086
|
+
"li",
|
|
3087
|
+
{
|
|
3088
|
+
"data-index": index,
|
|
3089
|
+
role: "option",
|
|
3090
|
+
"aria-selected": selectedIndex === index,
|
|
3091
|
+
onMouseDown: (e) => {
|
|
3092
|
+
e.preventDefault();
|
|
3093
|
+
onSelect(index);
|
|
3094
|
+
},
|
|
3095
|
+
onMouseEnter: () => onHover(index),
|
|
3096
|
+
className: cn(
|
|
3097
|
+
"flex cursor-pointer items-center gap-2.5 px-2.5 py-1.5 text-sm",
|
|
3098
|
+
selectedIndex === index ? "bg-accent text-accent-foreground" : "text-foreground",
|
|
3099
|
+
classNames?.mentionItem
|
|
3100
|
+
),
|
|
3101
|
+
style: itemStyle,
|
|
3102
|
+
children: [
|
|
3103
|
+
item.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: item.avatarUrl, alt: item.label }) : item.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary", children: item.icon }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary", children: item.label.slice(0, 1).toUpperCase() }),
|
|
3104
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col leading-tight", children: [
|
|
3105
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium", children: item.label }),
|
|
3106
|
+
item.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: item.description })
|
|
3107
|
+
] })
|
|
3108
|
+
]
|
|
3109
|
+
},
|
|
3110
|
+
item.id
|
|
3111
|
+
))
|
|
3112
|
+
] })
|
|
3113
|
+
}
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
function MentionSkeleton({ rows = 3 }) {
|
|
3117
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { "aria-hidden": "true", className: "px-2.5 py-1.5", children: [
|
|
3118
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3119
|
+
"li",
|
|
3120
|
+
{
|
|
3121
|
+
className: "flex items-center gap-2.5 rounded-md px-0 py-1.5",
|
|
3122
|
+
children: [
|
|
3123
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-7 w-7 shrink-0 animate-pulse rounded-full bg-muted" }),
|
|
3124
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
|
|
3125
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3126
|
+
"span",
|
|
3127
|
+
{
|
|
3128
|
+
className: "h-2.5 animate-pulse rounded bg-muted",
|
|
3129
|
+
style: { width: `${60 + i * 17 % 30}%` }
|
|
3130
|
+
}
|
|
3131
|
+
),
|
|
3132
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3133
|
+
"span",
|
|
3134
|
+
{
|
|
3135
|
+
className: "h-2 animate-pulse rounded bg-muted/70",
|
|
3136
|
+
style: { width: `${35 + i * 23 % 25}%` }
|
|
3137
|
+
}
|
|
3138
|
+
)
|
|
3139
|
+
] })
|
|
3140
|
+
]
|
|
3141
|
+
},
|
|
3142
|
+
i
|
|
3143
|
+
)) }),
|
|
3144
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading suggestions\u2026" })
|
|
3145
|
+
] });
|
|
3146
|
+
}
|
|
3147
|
+
var MentionOption = class extends LexicalTypeaheadMenuPlugin.MenuOption {
|
|
3148
|
+
item;
|
|
3149
|
+
constructor(item) {
|
|
3150
|
+
super(item.id);
|
|
3151
|
+
this.item = item;
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
function isSyncItems2(items) {
|
|
3155
|
+
return Array.isArray(items);
|
|
3156
|
+
}
|
|
3157
|
+
function MentionPlugin({ config }) {
|
|
3158
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
3159
|
+
const { closeMenusOnOutsideClick } = useComposerContext();
|
|
3160
|
+
const [query, setQuery] = react.useState("");
|
|
3161
|
+
const [asyncItems, setAsyncItems] = react.useState(null);
|
|
3162
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
3163
|
+
!isSyncItems2(config.items)
|
|
3164
|
+
);
|
|
3165
|
+
useOutsideClickDismiss(editor, closeMenusOnOutsideClick);
|
|
3166
|
+
const trigger = config.trigger ?? "@";
|
|
3167
|
+
const triggerFn = LexicalTypeaheadMenuPlugin.useBasicTypeaheadTriggerMatch(trigger, {
|
|
3168
|
+
minLength: 0,
|
|
3169
|
+
maxLength: 32,
|
|
3170
|
+
allowWhitespace: false
|
|
3171
|
+
});
|
|
3172
|
+
react.useEffect(() => {
|
|
3173
|
+
if (isSyncItems2(config.items)) {
|
|
3174
|
+
setIsLoading(false);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
let cancelled = false;
|
|
3178
|
+
setIsLoading(true);
|
|
3179
|
+
Promise.resolve(config.items(query)).then((res) => {
|
|
3180
|
+
if (cancelled) return;
|
|
3181
|
+
setAsyncItems(res);
|
|
3182
|
+
setIsLoading(false);
|
|
3183
|
+
});
|
|
3184
|
+
return () => {
|
|
3185
|
+
cancelled = true;
|
|
3186
|
+
};
|
|
3187
|
+
}, [query, config.items]);
|
|
3188
|
+
const allItems = react.useMemo(() => {
|
|
3189
|
+
return isSyncItems2(config.items) ? config.items : asyncItems ?? [];
|
|
3190
|
+
}, [config.items, asyncItems]);
|
|
3191
|
+
const options = react.useMemo(() => {
|
|
3192
|
+
const q = query.trim().toLowerCase();
|
|
3193
|
+
const max = config.maxItems ?? 8;
|
|
3194
|
+
const filtered = q ? allItems.filter(
|
|
3195
|
+
(it) => `${it.label} ${it.description ?? ""}`.toLowerCase().includes(q)
|
|
3196
|
+
) : allItems;
|
|
3197
|
+
return filtered.slice(0, max).map((c) => new MentionOption(c));
|
|
3198
|
+
}, [allItems, query, config.maxItems]);
|
|
3199
|
+
const onSelectOption = react.useCallback(
|
|
3200
|
+
(selectedOption, nodeToReplace, closeMenu) => {
|
|
3201
|
+
editor.update(() => {
|
|
3202
|
+
const chip = $createMentionNode(selectedOption.item.id, trigger);
|
|
3203
|
+
chip.append(lexical.$createTextNode(selectedOption.item.label));
|
|
3204
|
+
if (nodeToReplace) {
|
|
3205
|
+
nodeToReplace.replace(chip);
|
|
3206
|
+
} else {
|
|
3207
|
+
const sel = lexical.$getSelection();
|
|
3208
|
+
if (lexical.$isRangeSelection(sel)) sel.insertNodes([chip]);
|
|
3209
|
+
}
|
|
3210
|
+
const space = lexical.$createTextNode(" ");
|
|
3211
|
+
chip.insertAfter(space);
|
|
3212
|
+
space.select();
|
|
3213
|
+
});
|
|
3214
|
+
closeMenu();
|
|
3215
|
+
},
|
|
3216
|
+
[editor, trigger]
|
|
3217
|
+
);
|
|
3218
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3219
|
+
LexicalTypeaheadMenuPlugin.LexicalTypeaheadMenuPlugin,
|
|
3220
|
+
{
|
|
3221
|
+
onQueryChange: (s) => setQuery(s ?? ""),
|
|
3222
|
+
onSelectOption,
|
|
3223
|
+
triggerFn,
|
|
3224
|
+
options,
|
|
3225
|
+
menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
|
3226
|
+
if (!anchorElementRef.current) return null;
|
|
3227
|
+
if (options.length === 0 && !isLoading) return null;
|
|
3228
|
+
return reactDom.createPortal(
|
|
3229
|
+
/* @__PURE__ */ jsxRuntime.jsx(SmartPopover, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3230
|
+
MentionMenu,
|
|
3231
|
+
{
|
|
3232
|
+
options: options.map((o) => o.item),
|
|
3233
|
+
selectedIndex: selectedIndex ?? 0,
|
|
3234
|
+
isLoading,
|
|
3235
|
+
onSelect: (index) => selectOptionAndCleanUp(options[index]),
|
|
3236
|
+
onHover: (index) => setHighlightedIndex(index)
|
|
3237
|
+
}
|
|
3238
|
+
) }),
|
|
3239
|
+
anchorElementRef.current
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
function resolveConfig(config) {
|
|
3246
|
+
if (Array.isArray(config)) {
|
|
3247
|
+
return { suggestions: config, caseSensitive: false, minLength: 1 };
|
|
3248
|
+
}
|
|
3249
|
+
return {
|
|
3250
|
+
suggestions: config.suggestions,
|
|
3251
|
+
caseSensitive: !!config.caseSensitive,
|
|
3252
|
+
// `1` (not `0`) so the ghost never appears on an empty editor — the
|
|
3253
|
+
// placeholder handles that, and showing a ghost there would feel like
|
|
3254
|
+
// the composer is auto-typing on its own.
|
|
3255
|
+
minLength: Math.max(1, config.minLength ?? 1)
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
function findGhost(typed, suggestions, caseSensitive) {
|
|
3259
|
+
if (typed.length === 0) return null;
|
|
3260
|
+
const needle = caseSensitive ? typed : typed.toLowerCase();
|
|
3261
|
+
for (const candidate of suggestions) {
|
|
3262
|
+
if (candidate.length <= typed.length) continue;
|
|
3263
|
+
const hay = caseSensitive ? candidate : candidate.toLowerCase();
|
|
3264
|
+
if (hay.startsWith(needle)) {
|
|
3265
|
+
return candidate.slice(typed.length);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
return null;
|
|
3269
|
+
}
|
|
3270
|
+
function $isCaretAtDocumentEnd() {
|
|
3271
|
+
const sel = lexical.$getSelection();
|
|
3272
|
+
if (!lexical.$isRangeSelection(sel) || !sel.isCollapsed()) return false;
|
|
3273
|
+
const root = lexical.$getRoot();
|
|
3274
|
+
const last = root.getLastDescendant();
|
|
3275
|
+
if (last === null) {
|
|
3276
|
+
return sel.focus.key === root.getKey();
|
|
3277
|
+
}
|
|
3278
|
+
if (sel.focus.key !== last.getKey()) return false;
|
|
3279
|
+
const size = "getTextContentSize" in last && typeof last.getTextContentSize === "function" ? last.getTextContentSize() : last.getTextContent().length;
|
|
3280
|
+
return sel.focus.offset === size;
|
|
3281
|
+
}
|
|
3282
|
+
function GhostedAutoCompletePlugin({ config }) {
|
|
3283
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
3284
|
+
const { multiline } = useComposerContext();
|
|
3285
|
+
const resolved = react.useMemo(() => resolveConfig(config), [config]);
|
|
3286
|
+
const [ghost, setGhost] = react.useState(null);
|
|
3287
|
+
const ghostRef = react.useRef(null);
|
|
3288
|
+
ghostRef.current = ghost;
|
|
3289
|
+
react.useEffect(() => {
|
|
3290
|
+
const compute = () => {
|
|
3291
|
+
editor.getEditorState().read(() => {
|
|
3292
|
+
if (!$isCaretAtDocumentEnd()) {
|
|
3293
|
+
setGhost(null);
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
const typed = lexical.$getRoot().getTextContent();
|
|
3297
|
+
if (typed.length < resolved.minLength) {
|
|
3298
|
+
setGhost(null);
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const remainder = findGhost(
|
|
3302
|
+
typed,
|
|
3303
|
+
resolved.suggestions,
|
|
3304
|
+
resolved.caseSensitive
|
|
3305
|
+
);
|
|
3306
|
+
if (!remainder) {
|
|
3307
|
+
setGhost(null);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
setGhost(
|
|
3311
|
+
(prev) => prev && prev.typed === typed && prev.remainder === remainder ? prev : { typed, remainder }
|
|
3312
|
+
);
|
|
3313
|
+
});
|
|
3314
|
+
};
|
|
3315
|
+
compute();
|
|
3316
|
+
return editor.registerUpdateListener(compute);
|
|
3317
|
+
}, [editor, resolved]);
|
|
3318
|
+
const acceptGhost = react.useCallback(() => {
|
|
3319
|
+
const current = ghostRef.current;
|
|
3320
|
+
if (!current) return false;
|
|
3321
|
+
editor.update(() => {
|
|
3322
|
+
const sel = lexical.$getSelection();
|
|
3323
|
+
if (lexical.$isRangeSelection(sel)) {
|
|
3324
|
+
sel.insertText(current.remainder);
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
setGhost(null);
|
|
3328
|
+
return true;
|
|
3329
|
+
}, [editor]);
|
|
3330
|
+
react.useEffect(() => {
|
|
3331
|
+
return editor.registerCommand(
|
|
3332
|
+
lexical.KEY_TAB_COMMAND,
|
|
3333
|
+
(event) => {
|
|
3334
|
+
if (!ghostRef.current) return false;
|
|
3335
|
+
event?.preventDefault();
|
|
3336
|
+
return acceptGhost();
|
|
3337
|
+
},
|
|
3338
|
+
lexical.COMMAND_PRIORITY_HIGH
|
|
3339
|
+
);
|
|
3340
|
+
}, [editor, acceptGhost]);
|
|
3341
|
+
react.useEffect(() => {
|
|
3342
|
+
return editor.registerCommand(
|
|
3343
|
+
lexical.KEY_ESCAPE_COMMAND,
|
|
3344
|
+
() => {
|
|
3345
|
+
if (!ghostRef.current) return false;
|
|
3346
|
+
setGhost(null);
|
|
3347
|
+
return true;
|
|
3348
|
+
},
|
|
3349
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
3350
|
+
);
|
|
3351
|
+
}, [editor]);
|
|
3352
|
+
if (!ghost) return null;
|
|
3353
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3354
|
+
GhostOverlay,
|
|
3355
|
+
{
|
|
3356
|
+
typed: ghost.typed,
|
|
3357
|
+
remainder: ghost.remainder,
|
|
3358
|
+
multiline
|
|
3359
|
+
}
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
function GhostOverlay({ typed, remainder, multiline }) {
|
|
3363
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
3364
|
+
const [container, setContainer] = react.useState(null);
|
|
3365
|
+
react.useEffect(() => {
|
|
3366
|
+
const root = editor.getRootElement();
|
|
3367
|
+
const block = root?.closest(".composer-editor-block") ?? null;
|
|
3368
|
+
setContainer(block);
|
|
3369
|
+
}, [editor]);
|
|
3370
|
+
if (!container) return null;
|
|
3371
|
+
const paddingClass = multiline ? "composer-ghost-overlay--multiline px-5 py-3.5" : "composer-ghost-overlay--inline px-2 leading-9";
|
|
3372
|
+
return reactDom.createPortal(
|
|
3373
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3374
|
+
"div",
|
|
3375
|
+
{
|
|
3376
|
+
"aria-hidden": true,
|
|
3377
|
+
"data-composer-ghost": "",
|
|
3378
|
+
className: `composer-ghost-overlay pointer-events-none absolute inset-0 select-none ${paddingClass}`,
|
|
3379
|
+
children: [
|
|
3380
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "composer-ghost-overlay-typed", "aria-hidden": true, children: typed }),
|
|
3381
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "composer-ghost-suggestion", children: remainder })
|
|
3382
|
+
]
|
|
3383
|
+
}
|
|
3384
|
+
),
|
|
3385
|
+
container
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
function formatBytes(bytes) {
|
|
3389
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3390
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
3391
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3392
|
+
}
|
|
3393
|
+
function AttachmentChip({ attachment, onRemove, onZoom }) {
|
|
3394
|
+
const { icons, classNames, sx } = useComposerContext();
|
|
3395
|
+
const {
|
|
3396
|
+
file: FileIcon,
|
|
3397
|
+
audio: AudioIcon,
|
|
3398
|
+
close: CloseIcon,
|
|
3399
|
+
zoom: ZoomIcon,
|
|
3400
|
+
spinner: SpinnerIcon,
|
|
3401
|
+
warning: WarningIcon
|
|
3402
|
+
} = icons;
|
|
3403
|
+
const isImage = attachment.kind === "image" && !!attachment.previewUrl;
|
|
3404
|
+
const isUploading = attachment.status === "uploading";
|
|
3405
|
+
const isFailed = attachment.status === "failed";
|
|
3406
|
+
const titleText = isFailed && attachment.error ? `${attachment.name} \u2014 ${attachment.error}` : attachment.name;
|
|
3407
|
+
if (isImage) {
|
|
3408
|
+
const chip2 = slotProps(
|
|
3409
|
+
"attachmentChip",
|
|
3410
|
+
"group/chip relative h-16 w-16 overflow-hidden rounded-xl border border-border bg-muted",
|
|
3411
|
+
classNames,
|
|
3412
|
+
sx
|
|
3413
|
+
);
|
|
3414
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ...chip2, title: titleText, children: [
|
|
3415
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3416
|
+
"img",
|
|
3417
|
+
{
|
|
3418
|
+
src: attachment.previewUrl,
|
|
3419
|
+
alt: attachment.name,
|
|
3420
|
+
className: "h-full w-full object-cover"
|
|
3421
|
+
}
|
|
3422
|
+
),
|
|
3423
|
+
isUploading && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3424
|
+
"div",
|
|
3425
|
+
{
|
|
3426
|
+
"aria-label": "Uploading",
|
|
3427
|
+
className: "absolute inset-0 grid place-items-center bg-foreground/50",
|
|
3428
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(SpinnerIcon, { className: "h-5 w-5 animate-spin text-background" })
|
|
3429
|
+
}
|
|
3430
|
+
),
|
|
3431
|
+
isFailed && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3432
|
+
"div",
|
|
3433
|
+
{
|
|
3434
|
+
"aria-label": "Upload failed",
|
|
3435
|
+
className: "absolute inset-0 grid place-items-center bg-destructive/55",
|
|
3436
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(WarningIcon, { className: "h-5 w-5 text-destructive-foreground" })
|
|
3437
|
+
}
|
|
3438
|
+
),
|
|
3439
|
+
!isUploading && !isFailed && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3440
|
+
"button",
|
|
3441
|
+
{
|
|
3442
|
+
type: "button",
|
|
3443
|
+
onClick: onZoom,
|
|
3444
|
+
"aria-label": `Zoom ${attachment.name}`,
|
|
3445
|
+
className: "absolute inset-0 flex items-center justify-center bg-foreground/40 opacity-0 transition-opacity group-hover/chip:opacity-100",
|
|
3446
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ZoomIcon, { className: "h-4 w-4 text-background" })
|
|
3447
|
+
}
|
|
3448
|
+
),
|
|
3449
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3450
|
+
"button",
|
|
3451
|
+
{
|
|
3452
|
+
type: "button",
|
|
3453
|
+
onClick: onRemove,
|
|
3454
|
+
"aria-label": `Remove ${attachment.name}`,
|
|
3455
|
+
className: "absolute end-1 top-1 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-foreground text-background transition-opacity " + (isUploading || isFailed ? "opacity-100" : "opacity-0 group-hover/chip:opacity-100"),
|
|
3456
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-3 w-3", strokeWidth: 2.5 })
|
|
3457
|
+
}
|
|
3458
|
+
)
|
|
3459
|
+
] });
|
|
3460
|
+
}
|
|
3461
|
+
const KindIcon = attachment.kind === "audio" ? AudioIcon : FileIcon;
|
|
3462
|
+
const chip = slotProps(
|
|
3463
|
+
"attachmentChip",
|
|
3464
|
+
"group/chip flex items-center gap-2 rounded-xl border bg-card ps-2 pe-1 py-1.5 " + (isFailed ? "border-destructive/60" : "border-border"),
|
|
3465
|
+
classNames,
|
|
3466
|
+
sx
|
|
3467
|
+
);
|
|
3468
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ...chip, title: titleText, children: [
|
|
3469
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3470
|
+
"span",
|
|
3471
|
+
{
|
|
3472
|
+
className: "flex h-8 w-8 items-center justify-center rounded-md " + (isFailed ? "bg-destructive/15 text-destructive" : "bg-muted text-muted-foreground"),
|
|
3473
|
+
children: isUploading ? /* @__PURE__ */ jsxRuntime.jsx(SpinnerIcon, { className: "h-4 w-4 animate-spin" }) : isFailed ? /* @__PURE__ */ jsxRuntime.jsx(WarningIcon, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(KindIcon, { className: "h-4 w-4" })
|
|
3474
|
+
}
|
|
3475
|
+
),
|
|
3476
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-col", children: [
|
|
3477
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "max-w-[160px] truncate text-xs font-medium leading-tight", children: attachment.name }),
|
|
3478
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3479
|
+
"span",
|
|
3480
|
+
{
|
|
3481
|
+
className: "text-[10px] " + (isFailed ? "text-destructive" : "text-muted-foreground"),
|
|
3482
|
+
children: isUploading ? "Uploading\u2026" : isFailed ? attachment.error || "Upload failed" : formatBytes(attachment.size)
|
|
3483
|
+
}
|
|
3484
|
+
)
|
|
3485
|
+
] }),
|
|
3486
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3487
|
+
"button",
|
|
3488
|
+
{
|
|
3489
|
+
type: "button",
|
|
3490
|
+
onClick: onRemove,
|
|
3491
|
+
"aria-label": `Remove ${attachment.name}`,
|
|
3492
|
+
className: "flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
3493
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-3.5 w-3.5" })
|
|
3494
|
+
}
|
|
3495
|
+
)
|
|
3496
|
+
] });
|
|
3497
|
+
}
|
|
3498
|
+
function AttachmentTray() {
|
|
3499
|
+
const { attachments, removeAttachment, classNames, sx } = useComposerContext();
|
|
3500
|
+
const [zoom, setZoom] = react.useState(null);
|
|
3501
|
+
if (attachments.length === 0) return null;
|
|
3502
|
+
const tray = slotProps(
|
|
3503
|
+
"attachmentTray",
|
|
3504
|
+
"flex flex-wrap gap-2 px-4 pt-3",
|
|
3505
|
+
classNames,
|
|
3506
|
+
sx
|
|
3507
|
+
);
|
|
3508
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3509
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ...tray, children: attachments.map((att) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3510
|
+
AttachmentChip,
|
|
3511
|
+
{
|
|
3512
|
+
attachment: att,
|
|
3513
|
+
onRemove: () => removeAttachment(att.id),
|
|
3514
|
+
onZoom: att.kind === "image" ? () => setZoom(att) : void 0
|
|
3515
|
+
},
|
|
3516
|
+
att.id
|
|
3517
|
+
)) }),
|
|
3518
|
+
zoom && zoom.previewUrl && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3519
|
+
ImageLightbox,
|
|
3520
|
+
{
|
|
3521
|
+
src: zoom.previewUrl,
|
|
3522
|
+
alt: zoom.name,
|
|
3523
|
+
onClose: () => setZoom(null)
|
|
3524
|
+
}
|
|
3525
|
+
)
|
|
3526
|
+
] });
|
|
3527
|
+
}
|
|
3528
|
+
var sideClasses = {
|
|
3529
|
+
top: "bottom-full left-1/2 mb-2 -translate-x-1/2",
|
|
3530
|
+
bottom: "top-full left-1/2 mt-2 -translate-x-1/2",
|
|
3531
|
+
left: "end-full top-1/2 me-2 -translate-y-1/2",
|
|
3532
|
+
right: "start-full top-1/2 ms-2 -translate-y-1/2"
|
|
3533
|
+
};
|
|
3534
|
+
function Tooltip({
|
|
3535
|
+
children,
|
|
3536
|
+
content,
|
|
3537
|
+
side = "top",
|
|
3538
|
+
delay = 150,
|
|
3539
|
+
className
|
|
3540
|
+
}) {
|
|
3541
|
+
const [open, setOpen] = react.useState(false);
|
|
3542
|
+
const id = react.useId();
|
|
3543
|
+
const timerRef = react.useRef(null);
|
|
3544
|
+
const show = () => {
|
|
3545
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3546
|
+
timerRef.current = setTimeout(() => setOpen(true), delay);
|
|
3547
|
+
};
|
|
3548
|
+
const hide = () => {
|
|
3549
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3550
|
+
setOpen(false);
|
|
3551
|
+
};
|
|
3552
|
+
const trigger = react.isValidElement(children) ? react.cloneElement(children, {
|
|
3553
|
+
"aria-describedby": id,
|
|
3554
|
+
onMouseEnter: show,
|
|
3555
|
+
onMouseLeave: hide,
|
|
3556
|
+
onFocus: show,
|
|
3557
|
+
onBlur: hide
|
|
3558
|
+
}) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3559
|
+
"span",
|
|
3560
|
+
{
|
|
3561
|
+
tabIndex: 0,
|
|
3562
|
+
"aria-describedby": id,
|
|
3563
|
+
onMouseEnter: show,
|
|
3564
|
+
onMouseLeave: hide,
|
|
3565
|
+
onFocus: show,
|
|
3566
|
+
onBlur: hide,
|
|
3567
|
+
children
|
|
3568
|
+
}
|
|
3569
|
+
);
|
|
3570
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative inline-flex", children: [
|
|
3571
|
+
trigger,
|
|
3572
|
+
open && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3573
|
+
"span",
|
|
3574
|
+
{
|
|
3575
|
+
id,
|
|
3576
|
+
role: "tooltip",
|
|
3577
|
+
className: cn(
|
|
3578
|
+
"absolute z-50 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-md",
|
|
3579
|
+
sideClasses[side],
|
|
3580
|
+
className
|
|
3581
|
+
),
|
|
3582
|
+
children: content
|
|
3583
|
+
}
|
|
3584
|
+
)
|
|
3585
|
+
] });
|
|
3586
|
+
}
|
|
3587
|
+
function getSpeechRecognition() {
|
|
3588
|
+
const w = window;
|
|
3589
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
3590
|
+
}
|
|
3591
|
+
function formatSeconds(s) {
|
|
3592
|
+
const mm = Math.floor(s / 60);
|
|
3593
|
+
const ss = s % 60;
|
|
3594
|
+
return `${mm}:${ss.toString().padStart(2, "0")}`;
|
|
3595
|
+
}
|
|
3596
|
+
function VoiceButton() {
|
|
3597
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
3598
|
+
const { addFiles, icons, classNames } = useComposerContext();
|
|
3599
|
+
const { voice: VoiceIcon, voiceRecording: VoiceRecordingIcon } = icons;
|
|
3600
|
+
const [state, setState] = react.useState("idle");
|
|
3601
|
+
const [elapsed, setElapsed] = react.useState(0);
|
|
3602
|
+
const recognitionRef = react.useRef(null);
|
|
3603
|
+
const mediaRef = react.useRef(null);
|
|
3604
|
+
const chunksRef = react.useRef([]);
|
|
3605
|
+
const startedAtRef = react.useRef(0);
|
|
3606
|
+
const tickerRef = react.useRef(null);
|
|
3607
|
+
react.useEffect(() => {
|
|
3608
|
+
return () => {
|
|
3609
|
+
try {
|
|
3610
|
+
recognitionRef.current?.stop();
|
|
3611
|
+
} catch {
|
|
3612
|
+
}
|
|
3613
|
+
try {
|
|
3614
|
+
mediaRef.current?.stop();
|
|
3615
|
+
} catch {
|
|
3616
|
+
}
|
|
3617
|
+
if (tickerRef.current) window.clearInterval(tickerRef.current);
|
|
3618
|
+
};
|
|
3619
|
+
}, []);
|
|
3620
|
+
const startTicker = () => {
|
|
3621
|
+
startedAtRef.current = Date.now();
|
|
3622
|
+
setElapsed(0);
|
|
3623
|
+
if (tickerRef.current) window.clearInterval(tickerRef.current);
|
|
3624
|
+
tickerRef.current = window.setInterval(() => {
|
|
3625
|
+
setElapsed(Math.floor((Date.now() - startedAtRef.current) / 1e3));
|
|
3626
|
+
}, 500);
|
|
3627
|
+
};
|
|
3628
|
+
const stopTicker = () => {
|
|
3629
|
+
if (tickerRef.current) {
|
|
3630
|
+
window.clearInterval(tickerRef.current);
|
|
3631
|
+
tickerRef.current = null;
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
const insertText = react.useCallback(
|
|
3635
|
+
(text) => {
|
|
3636
|
+
editor.update(() => {
|
|
3637
|
+
const sel = lexical.$getSelection();
|
|
3638
|
+
if (lexical.$isRangeSelection(sel)) {
|
|
3639
|
+
sel.insertText(text);
|
|
3640
|
+
}
|
|
3641
|
+
});
|
|
3642
|
+
},
|
|
3643
|
+
[editor]
|
|
3644
|
+
);
|
|
3645
|
+
const startSpeech = (Recognition) => {
|
|
3646
|
+
const rec = new Recognition();
|
|
3647
|
+
rec.continuous = false;
|
|
3648
|
+
rec.interimResults = true;
|
|
3649
|
+
rec.lang = navigator.language || "en-US";
|
|
3650
|
+
let finalBuffer = "";
|
|
3651
|
+
rec.onresult = (event) => {
|
|
3652
|
+
let interim = "";
|
|
3653
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
3654
|
+
const result = event.results[i];
|
|
3655
|
+
const transcript = result[0].transcript;
|
|
3656
|
+
if (result.isFinal) {
|
|
3657
|
+
finalBuffer += transcript;
|
|
3658
|
+
} else {
|
|
3659
|
+
interim += transcript;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
if (finalBuffer.trim().length > 0) {
|
|
3663
|
+
insertText(finalBuffer + (interim ? "" : " "));
|
|
3664
|
+
finalBuffer = "";
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
rec.onerror = () => {
|
|
3668
|
+
stopTicker();
|
|
3669
|
+
setState("idle");
|
|
3670
|
+
};
|
|
3671
|
+
rec.onend = () => {
|
|
3672
|
+
stopTicker();
|
|
3673
|
+
setState("idle");
|
|
3674
|
+
};
|
|
3675
|
+
recognitionRef.current = rec;
|
|
3676
|
+
setState("recording");
|
|
3677
|
+
startTicker();
|
|
3678
|
+
rec.start();
|
|
3679
|
+
};
|
|
3680
|
+
const startMediaRecorder = async () => {
|
|
3681
|
+
try {
|
|
3682
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
3683
|
+
const rec = new MediaRecorder(stream);
|
|
3684
|
+
chunksRef.current = [];
|
|
3685
|
+
rec.ondataavailable = (e) => {
|
|
3686
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
3687
|
+
};
|
|
3688
|
+
rec.onstop = () => {
|
|
3689
|
+
const blob = new Blob(chunksRef.current, { type: "audio/webm" });
|
|
3690
|
+
const file = new File([blob], `voice-${Date.now()}.webm`, {
|
|
3691
|
+
type: "audio/webm"
|
|
3692
|
+
});
|
|
3693
|
+
addFiles([file]);
|
|
3694
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
3695
|
+
stopTicker();
|
|
3696
|
+
setState("idle");
|
|
3697
|
+
};
|
|
3698
|
+
mediaRef.current = rec;
|
|
3699
|
+
rec.start();
|
|
3700
|
+
setState("recording");
|
|
3701
|
+
startTicker();
|
|
3702
|
+
} catch {
|
|
3703
|
+
stopTicker();
|
|
3704
|
+
setState("idle");
|
|
3705
|
+
}
|
|
3706
|
+
};
|
|
3707
|
+
const start = async () => {
|
|
3708
|
+
setState("starting");
|
|
3709
|
+
const Recognition = getSpeechRecognition();
|
|
3710
|
+
if (Recognition) {
|
|
3711
|
+
try {
|
|
3712
|
+
startSpeech(Recognition);
|
|
3713
|
+
return;
|
|
3714
|
+
} catch {
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
await startMediaRecorder();
|
|
3718
|
+
};
|
|
3719
|
+
const stop = () => {
|
|
3720
|
+
setState("transcribing");
|
|
3721
|
+
try {
|
|
3722
|
+
recognitionRef.current?.stop();
|
|
3723
|
+
} catch {
|
|
3724
|
+
}
|
|
3725
|
+
try {
|
|
3726
|
+
mediaRef.current?.stop();
|
|
3727
|
+
} catch {
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
const isRecording = state === "recording" || state === "starting";
|
|
3731
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
3732
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3733
|
+
Tooltip,
|
|
3734
|
+
{
|
|
3735
|
+
content: isRecording ? "Stop recording" : "Voice input",
|
|
3736
|
+
side: "top",
|
|
3737
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3738
|
+
"button",
|
|
3739
|
+
{
|
|
3740
|
+
type: "button",
|
|
3741
|
+
"aria-label": isRecording ? "Stop voice recording" : "Start voice input",
|
|
3742
|
+
"aria-pressed": isRecording,
|
|
3743
|
+
onClick: () => isRecording ? stop() : void start(),
|
|
3744
|
+
className: cn(
|
|
3745
|
+
"flex h-8 w-8 items-center justify-center rounded-full transition-colors",
|
|
3746
|
+
isRecording ? "bg-destructive/10 text-destructive ring-1 ring-destructive/40" : "text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
3747
|
+
classNames?.toolbarButton
|
|
3748
|
+
),
|
|
3749
|
+
children: state === "transcribing" ? /* @__PURE__ */ jsxRuntime.jsx(VoiceRecordingIcon, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(VoiceIcon, { className: cn("h-4 w-4", isRecording && "animate-pulse") })
|
|
3750
|
+
}
|
|
3751
|
+
)
|
|
3752
|
+
}
|
|
3753
|
+
),
|
|
3754
|
+
isRecording && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1 rounded-full bg-destructive/10 px-2 py-0.5 text-[11px] font-mono font-medium tabular-nums text-destructive", children: [
|
|
3755
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1.5 w-1.5 animate-pulse rounded-full bg-destructive" }),
|
|
3756
|
+
formatSeconds(elapsed)
|
|
3757
|
+
] })
|
|
3758
|
+
] });
|
|
3759
|
+
}
|
|
3760
|
+
function AttachmentTypePicker({
|
|
3761
|
+
types,
|
|
3762
|
+
addFiles,
|
|
3763
|
+
triggerClassName,
|
|
3764
|
+
triggerStyle,
|
|
3765
|
+
TriggerIcon
|
|
3766
|
+
}) {
|
|
3767
|
+
const { closeMenusOnOutsideClick } = useComposerContext();
|
|
3768
|
+
const [open, setOpen] = react.useState(false);
|
|
3769
|
+
const [activeIndex, setActiveIndex] = react.useState(0);
|
|
3770
|
+
const triggerRef = react.useRef(null);
|
|
3771
|
+
const popoverRef = react.useRef(null);
|
|
3772
|
+
const fileInputRef = react.useRef(null);
|
|
3773
|
+
const itemRefs = react.useRef([]);
|
|
3774
|
+
const menuId = react.useId();
|
|
3775
|
+
const close = react.useCallback(() => setOpen(false), []);
|
|
3776
|
+
const triggerPicker = react.useCallback(
|
|
3777
|
+
(type) => {
|
|
3778
|
+
const input = fileInputRef.current;
|
|
3779
|
+
if (!input) return;
|
|
3780
|
+
input.accept = type.accept;
|
|
3781
|
+
input.click();
|
|
3782
|
+
},
|
|
3783
|
+
[]
|
|
3784
|
+
);
|
|
3785
|
+
const pick = react.useCallback(
|
|
3786
|
+
(index) => {
|
|
3787
|
+
const type = types[index];
|
|
3788
|
+
if (!type) return;
|
|
3789
|
+
close();
|
|
3790
|
+
triggerRef.current?.focus();
|
|
3791
|
+
triggerPicker(type);
|
|
3792
|
+
},
|
|
3793
|
+
[types, close, triggerPicker]
|
|
3794
|
+
);
|
|
3795
|
+
react.useEffect(() => {
|
|
3796
|
+
if (!open || !closeMenusOnOutsideClick) return;
|
|
3797
|
+
const onPointerDown = (event) => {
|
|
3798
|
+
const target = event.target;
|
|
3799
|
+
if (!target) return;
|
|
3800
|
+
if (popoverRef.current?.contains(target)) return;
|
|
3801
|
+
if (triggerRef.current?.contains(target)) return;
|
|
3802
|
+
close();
|
|
3803
|
+
};
|
|
3804
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
3805
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
3806
|
+
}, [open, closeMenusOnOutsideClick, close]);
|
|
3807
|
+
react.useEffect(() => {
|
|
3808
|
+
if (!open) return;
|
|
3809
|
+
const onKeyDown = (event) => {
|
|
3810
|
+
if (event.key === "Escape") {
|
|
3811
|
+
event.preventDefault();
|
|
3812
|
+
close();
|
|
3813
|
+
triggerRef.current?.focus();
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
if (event.key === "ArrowDown") {
|
|
3817
|
+
event.preventDefault();
|
|
3818
|
+
setActiveIndex((i) => (i + 1) % types.length);
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
if (event.key === "ArrowUp") {
|
|
3822
|
+
event.preventDefault();
|
|
3823
|
+
setActiveIndex((i) => (i - 1 + types.length) % types.length);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
if (event.key === "Home") {
|
|
3827
|
+
event.preventDefault();
|
|
3828
|
+
setActiveIndex(0);
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
if (event.key === "End") {
|
|
3832
|
+
event.preventDefault();
|
|
3833
|
+
setActiveIndex(types.length - 1);
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
3837
|
+
if (popoverRef.current?.contains(document.activeElement)) {
|
|
3838
|
+
event.preventDefault();
|
|
3839
|
+
pick(activeIndex);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
};
|
|
3843
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3844
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
3845
|
+
}, [open, types.length, activeIndex, pick, close]);
|
|
3846
|
+
react.useEffect(() => {
|
|
3847
|
+
if (!open) return;
|
|
3848
|
+
itemRefs.current[activeIndex]?.focus();
|
|
3849
|
+
}, [open, activeIndex]);
|
|
3850
|
+
react.useEffect(() => {
|
|
3851
|
+
if (open) setActiveIndex(0);
|
|
3852
|
+
}, [open]);
|
|
3853
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
3854
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3855
|
+
"input",
|
|
3856
|
+
{
|
|
3857
|
+
ref: fileInputRef,
|
|
3858
|
+
type: "file",
|
|
3859
|
+
multiple: true,
|
|
3860
|
+
hidden: true,
|
|
3861
|
+
onChange: (e) => {
|
|
3862
|
+
const files = e.target.files;
|
|
3863
|
+
if (files && files.length > 0) addFiles(Array.from(files));
|
|
3864
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
),
|
|
3868
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3869
|
+
"button",
|
|
3870
|
+
{
|
|
3871
|
+
ref: triggerRef,
|
|
3872
|
+
type: "button",
|
|
3873
|
+
"aria-label": "Attach file",
|
|
3874
|
+
"aria-haspopup": "menu",
|
|
3875
|
+
"aria-expanded": open,
|
|
3876
|
+
"aria-controls": open ? menuId : void 0,
|
|
3877
|
+
onClick: () => setOpen((o) => !o),
|
|
3878
|
+
className: triggerClassName,
|
|
3879
|
+
style: triggerStyle,
|
|
3880
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(TriggerIcon, { className: "h-4 w-4" })
|
|
3881
|
+
}
|
|
3882
|
+
),
|
|
3883
|
+
open && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3884
|
+
"div",
|
|
3885
|
+
{
|
|
3886
|
+
ref: popoverRef,
|
|
3887
|
+
id: menuId,
|
|
3888
|
+
role: "menu",
|
|
3889
|
+
"aria-label": "Attachment types",
|
|
3890
|
+
"data-composer-popover": "open",
|
|
3891
|
+
className: cn(
|
|
3892
|
+
"composer-popover-in absolute bottom-full start-0 z-30 mb-2 min-w-[200px] overflow-hidden",
|
|
3893
|
+
"rounded-xl border border-border bg-popover p-1 text-popover-foreground",
|
|
3894
|
+
"shadow-soft"
|
|
3895
|
+
),
|
|
3896
|
+
children: types.map((type, index) => {
|
|
3897
|
+
const active = index === activeIndex;
|
|
3898
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3899
|
+
"button",
|
|
3900
|
+
{
|
|
3901
|
+
ref: (el) => {
|
|
3902
|
+
itemRefs.current[index] = el;
|
|
3903
|
+
},
|
|
3904
|
+
role: "menuitem",
|
|
3905
|
+
type: "button",
|
|
3906
|
+
tabIndex: active ? 0 : -1,
|
|
3907
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
3908
|
+
onClick: () => pick(index),
|
|
3909
|
+
className: cn(
|
|
3910
|
+
"flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-start text-sm transition-colors",
|
|
3911
|
+
active ? "bg-accent text-accent-foreground" : "text-foreground hover:bg-accent/60"
|
|
3912
|
+
),
|
|
3913
|
+
children: [
|
|
3914
|
+
type.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center text-muted-foreground", children: type.icon }) : null,
|
|
3915
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: type.label }),
|
|
3916
|
+
type.description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 font-mono text-[11px] text-muted-foreground", children: type.description }) : null
|
|
3917
|
+
]
|
|
3918
|
+
},
|
|
3919
|
+
type.id
|
|
3920
|
+
);
|
|
3921
|
+
})
|
|
3922
|
+
}
|
|
3923
|
+
)
|
|
3924
|
+
] });
|
|
3925
|
+
}
|
|
3926
|
+
var TOOLBAR_BTN_BASE = "flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground";
|
|
3927
|
+
function Toolbar({ extras }) {
|
|
3928
|
+
const {
|
|
3929
|
+
features,
|
|
3930
|
+
attachmentsConfig,
|
|
3931
|
+
addFiles,
|
|
3932
|
+
webEnabled,
|
|
3933
|
+
toggleWeb,
|
|
3934
|
+
icons,
|
|
3935
|
+
classNames,
|
|
3936
|
+
sx
|
|
3937
|
+
} = useComposerContext();
|
|
3938
|
+
const { attach: AttachIcon, image: ImageIcon, web: WebIcon } = icons;
|
|
3939
|
+
const fileInputRef = react.useRef(null);
|
|
3940
|
+
const imageInputRef = react.useRef(null);
|
|
3941
|
+
const attachmentsEnabled = !!features.attachments;
|
|
3942
|
+
const showFileBtn = attachmentsEnabled && attachmentsConfig.file !== false;
|
|
3943
|
+
const showImageBtn = attachmentsEnabled && attachmentsConfig.image !== false;
|
|
3944
|
+
const hasTypePicker = showFileBtn && Array.isArray(attachmentsConfig.types) && attachmentsConfig.types.length > 0;
|
|
3945
|
+
const toolbar = slotProps("toolbar", "flex items-center gap-1", classNames, sx);
|
|
3946
|
+
const toolbarBtn = slotProps("toolbarButton", TOOLBAR_BTN_BASE, classNames, sx);
|
|
3947
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ...toolbar, children: [
|
|
3948
|
+
showFileBtn && !hasTypePicker && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3949
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3950
|
+
"input",
|
|
3951
|
+
{
|
|
3952
|
+
ref: fileInputRef,
|
|
3953
|
+
type: "file",
|
|
3954
|
+
multiple: true,
|
|
3955
|
+
accept: attachmentsConfig.accept,
|
|
3956
|
+
hidden: true,
|
|
3957
|
+
onChange: (e) => {
|
|
3958
|
+
const files = e.target.files;
|
|
3959
|
+
if (files) addFiles(Array.from(files));
|
|
3960
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
),
|
|
3964
|
+
/* @__PURE__ */ jsxRuntime.jsx(Tooltip, { content: "Attach file", side: "top", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3965
|
+
"button",
|
|
3966
|
+
{
|
|
3967
|
+
type: "button",
|
|
3968
|
+
"aria-label": "Attach file",
|
|
3969
|
+
onClick: () => fileInputRef.current?.click(),
|
|
3970
|
+
...toolbarBtn,
|
|
3971
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(AttachIcon, { className: "h-4 w-4" })
|
|
3972
|
+
}
|
|
3973
|
+
) })
|
|
3974
|
+
] }),
|
|
3975
|
+
hasTypePicker && attachmentsConfig.types && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3976
|
+
AttachmentTypePicker,
|
|
3977
|
+
{
|
|
3978
|
+
types: attachmentsConfig.types,
|
|
3979
|
+
addFiles,
|
|
3980
|
+
triggerClassName: toolbarBtn.className ?? "",
|
|
3981
|
+
triggerStyle: toolbarBtn.style,
|
|
3982
|
+
TriggerIcon: AttachIcon
|
|
3983
|
+
}
|
|
3984
|
+
),
|
|
3985
|
+
showImageBtn && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3986
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3987
|
+
"input",
|
|
3988
|
+
{
|
|
3989
|
+
ref: imageInputRef,
|
|
3990
|
+
type: "file",
|
|
3991
|
+
multiple: true,
|
|
3992
|
+
accept: "image/*",
|
|
3993
|
+
hidden: true,
|
|
3994
|
+
onChange: (e) => {
|
|
3995
|
+
const files = e.target.files;
|
|
3996
|
+
if (files) addFiles(Array.from(files));
|
|
3997
|
+
if (imageInputRef.current) imageInputRef.current.value = "";
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
),
|
|
4001
|
+
/* @__PURE__ */ jsxRuntime.jsx(Tooltip, { content: "Add image", side: "top", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4002
|
+
"button",
|
|
4003
|
+
{
|
|
4004
|
+
type: "button",
|
|
4005
|
+
"aria-label": "Add image",
|
|
4006
|
+
onClick: () => imageInputRef.current?.click(),
|
|
4007
|
+
...toolbarBtn,
|
|
4008
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ImageIcon, { className: "h-4 w-4" })
|
|
4009
|
+
}
|
|
4010
|
+
) })
|
|
4011
|
+
] }),
|
|
4012
|
+
features.voice && /* @__PURE__ */ jsxRuntime.jsx(VoiceButton, {}),
|
|
4013
|
+
features.web && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4014
|
+
"button",
|
|
4015
|
+
{
|
|
4016
|
+
type: "button",
|
|
4017
|
+
onClick: toggleWeb,
|
|
4018
|
+
"aria-pressed": webEnabled,
|
|
4019
|
+
className: cn(
|
|
4020
|
+
"ms-0.5 inline-flex h-8 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors",
|
|
4021
|
+
webEnabled ? "bg-primary/10 text-primary" : "text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
4022
|
+
classNames?.toolbarButton
|
|
4023
|
+
),
|
|
4024
|
+
children: [
|
|
4025
|
+
/* @__PURE__ */ jsxRuntime.jsx(WebIcon, { className: "h-3.5 w-3.5" }),
|
|
4026
|
+
"Web"
|
|
4027
|
+
]
|
|
4028
|
+
}
|
|
4029
|
+
),
|
|
4030
|
+
extras
|
|
4031
|
+
] });
|
|
4032
|
+
}
|
|
4033
|
+
function SendButton({ canSend, isStreaming, onSend, onStop }) {
|
|
4034
|
+
const { icons, classNames, sx, slots } = useComposerContext();
|
|
4035
|
+
const { send: SendIcon, stop: StopIcon } = icons;
|
|
4036
|
+
if (isStreaming) {
|
|
4037
|
+
const stop = slotProps(
|
|
4038
|
+
"stopButton",
|
|
4039
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-full bg-foreground text-background transition-transform hover:scale-105",
|
|
4040
|
+
classNames,
|
|
4041
|
+
sx
|
|
4042
|
+
);
|
|
4043
|
+
if (slots.stopButton) {
|
|
4044
|
+
const Slot = slots.stopButton;
|
|
4045
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4046
|
+
Slot,
|
|
4047
|
+
{
|
|
4048
|
+
onStop: onStop ?? noop,
|
|
4049
|
+
className: stop.className,
|
|
4050
|
+
style: stop.style
|
|
4051
|
+
}
|
|
4052
|
+
);
|
|
4053
|
+
}
|
|
4054
|
+
return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onStop, "aria-label": "Stop generating", ...stop, children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5 fill-current" }) });
|
|
4055
|
+
}
|
|
4056
|
+
const send = slotProps(
|
|
4057
|
+
"sendButton",
|
|
4058
|
+
[
|
|
4059
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-full transition-all",
|
|
4060
|
+
canSend ? "bg-foreground text-background shadow-sm hover:scale-105" : "bg-muted text-muted-foreground/60"
|
|
4061
|
+
],
|
|
4062
|
+
classNames,
|
|
4063
|
+
sx
|
|
4064
|
+
);
|
|
4065
|
+
if (slots.sendButton) {
|
|
4066
|
+
const Slot = slots.sendButton;
|
|
4067
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4068
|
+
Slot,
|
|
4069
|
+
{
|
|
4070
|
+
canSend,
|
|
4071
|
+
onSend,
|
|
4072
|
+
className: send.className,
|
|
4073
|
+
style: send.style
|
|
4074
|
+
}
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4078
|
+
"button",
|
|
4079
|
+
{
|
|
4080
|
+
type: "button",
|
|
4081
|
+
onClick: onSend,
|
|
4082
|
+
disabled: !canSend,
|
|
4083
|
+
"aria-label": "Send message",
|
|
4084
|
+
...send,
|
|
4085
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(SendIcon, { className: "h-4 w-4", strokeWidth: 2.5 })
|
|
4086
|
+
}
|
|
4087
|
+
);
|
|
4088
|
+
}
|
|
4089
|
+
function noop() {
|
|
4090
|
+
}
|
|
4091
|
+
function Key({ children }) {
|
|
4092
|
+
return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-border bg-card px-1 py-0.5 font-mono text-[10px]", children });
|
|
4093
|
+
}
|
|
4094
|
+
function formatShortcut(spec) {
|
|
4095
|
+
const parts = spec.split("+").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
4096
|
+
if (parts.length === 0) return null;
|
|
4097
|
+
const labels = [];
|
|
4098
|
+
for (const p of parts) {
|
|
4099
|
+
switch (p) {
|
|
4100
|
+
case "mod":
|
|
4101
|
+
labels.push("\u2318/Ctrl");
|
|
4102
|
+
break;
|
|
4103
|
+
case "cmd":
|
|
4104
|
+
case "command":
|
|
4105
|
+
case "meta":
|
|
4106
|
+
case "win":
|
|
4107
|
+
case "super":
|
|
4108
|
+
labels.push("\u2318");
|
|
4109
|
+
break;
|
|
4110
|
+
case "ctrl":
|
|
4111
|
+
case "control":
|
|
4112
|
+
labels.push("Ctrl");
|
|
4113
|
+
break;
|
|
4114
|
+
case "alt":
|
|
4115
|
+
case "option":
|
|
4116
|
+
labels.push("Alt");
|
|
4117
|
+
break;
|
|
4118
|
+
case "shift":
|
|
4119
|
+
labels.push("Shift");
|
|
4120
|
+
break;
|
|
4121
|
+
default:
|
|
4122
|
+
labels.push(p.length === 1 ? p.toUpperCase() : p);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
return labels.join(" + ");
|
|
4126
|
+
}
|
|
4127
|
+
function HintBar({ hint }) {
|
|
4128
|
+
const {
|
|
4129
|
+
multiline,
|
|
4130
|
+
submitOnEnter,
|
|
4131
|
+
smartNewline,
|
|
4132
|
+
focusShortcut,
|
|
4133
|
+
classNames,
|
|
4134
|
+
sx
|
|
4135
|
+
} = useComposerContext();
|
|
4136
|
+
const defaultShortcuts = react.useMemo(() => {
|
|
4137
|
+
if (!multiline) {
|
|
4138
|
+
if (!submitOnEnter) return null;
|
|
4139
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4140
|
+
"Press ",
|
|
4141
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "Enter" }),
|
|
4142
|
+
" to send."
|
|
4143
|
+
] });
|
|
4144
|
+
}
|
|
4145
|
+
if (smartNewline && submitOnEnter) {
|
|
4146
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4147
|
+
"Press ",
|
|
4148
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "Enter" }),
|
|
4149
|
+
" to send a single line,",
|
|
4150
|
+
" ",
|
|
4151
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "\u2318/Ctrl + Enter" }),
|
|
4152
|
+
" to send once you've started a new line."
|
|
4153
|
+
] });
|
|
4154
|
+
}
|
|
4155
|
+
if (!submitOnEnter) {
|
|
4156
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4157
|
+
"Press ",
|
|
4158
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "\u2318/Ctrl + Enter" }),
|
|
4159
|
+
" to send, ",
|
|
4160
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "Enter" }),
|
|
4161
|
+
" for newline."
|
|
4162
|
+
] });
|
|
4163
|
+
}
|
|
4164
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4165
|
+
"Press ",
|
|
4166
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "Enter" }),
|
|
4167
|
+
" to send, ",
|
|
4168
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: "Shift + Enter" }),
|
|
4169
|
+
" for newline."
|
|
4170
|
+
] });
|
|
4171
|
+
}, [multiline, submitOnEnter, smartNewline]);
|
|
4172
|
+
const focusHint = react.useMemo(() => {
|
|
4173
|
+
if (!focusShortcut) return null;
|
|
4174
|
+
const label = formatShortcut(focusShortcut);
|
|
4175
|
+
if (!label) return null;
|
|
4176
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4177
|
+
" ",
|
|
4178
|
+
/* @__PURE__ */ jsxRuntime.jsx(Key, { children: label }),
|
|
4179
|
+
" to jump back here."
|
|
4180
|
+
] });
|
|
4181
|
+
}, [focusShortcut]);
|
|
4182
|
+
if (!hint) return null;
|
|
4183
|
+
const hintProps = slotProps(
|
|
4184
|
+
"hint",
|
|
4185
|
+
"text-center text-[11px] text-muted-foreground",
|
|
4186
|
+
classNames,
|
|
4187
|
+
sx
|
|
4188
|
+
);
|
|
4189
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { ...hintProps, children: hint === true ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4190
|
+
"AI can make mistakes \u2014 verify important info.",
|
|
4191
|
+
defaultShortcuts ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden sm:inline", children: [
|
|
4192
|
+
" ",
|
|
4193
|
+
defaultShortcuts
|
|
4194
|
+
] }) : null,
|
|
4195
|
+
focusHint ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: focusHint }) : null
|
|
4196
|
+
] }) : hint });
|
|
4197
|
+
}
|
|
4198
|
+
var DEFAULT_MAX = 3;
|
|
4199
|
+
var HARD_CAP = 5;
|
|
4200
|
+
function pickDisplay(items, maxToShow, randomize) {
|
|
4201
|
+
const cleaned = items.filter((s) => typeof s === "string" && s.length > 0);
|
|
4202
|
+
if (cleaned.length === 0) return [];
|
|
4203
|
+
const max = Math.min(Math.max(1, maxToShow ?? DEFAULT_MAX), HARD_CAP);
|
|
4204
|
+
if (cleaned.length <= max) return cleaned;
|
|
4205
|
+
if (randomize === false) return cleaned.slice(0, max);
|
|
4206
|
+
const arr = [...cleaned];
|
|
4207
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
4208
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
4209
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
4210
|
+
}
|
|
4211
|
+
return arr.slice(0, max);
|
|
4212
|
+
}
|
|
4213
|
+
function QuickPrompts({ prompts }) {
|
|
4214
|
+
const { runPrompt, icons } = useComposerContext();
|
|
4215
|
+
const { sparkle: SparkleIcon } = icons;
|
|
4216
|
+
const display = react.useMemo(
|
|
4217
|
+
() => pickDisplay(prompts.items, prompts.maxToShow, prompts.randomize),
|
|
4218
|
+
[prompts.items, prompts.maxToShow, prompts.randomize]
|
|
4219
|
+
);
|
|
4220
|
+
if (display.length === 0) return null;
|
|
4221
|
+
const behavior = prompts.behavior ?? "sendValue";
|
|
4222
|
+
const handleClick = (prompt) => {
|
|
4223
|
+
prompts.onSelect?.(prompt);
|
|
4224
|
+
runPrompt(prompt, behavior);
|
|
4225
|
+
};
|
|
4226
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4227
|
+
"div",
|
|
4228
|
+
{
|
|
4229
|
+
role: "group",
|
|
4230
|
+
"aria-label": "Quick prompts",
|
|
4231
|
+
className: "flex flex-wrap items-center gap-2 px-1 pb-1",
|
|
4232
|
+
children: display.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4233
|
+
"button",
|
|
4234
|
+
{
|
|
4235
|
+
type: "button",
|
|
4236
|
+
onClick: () => handleClick(p),
|
|
4237
|
+
title: p,
|
|
4238
|
+
className: cn(
|
|
4239
|
+
"group inline-flex max-w-full items-center gap-1.5 rounded-full",
|
|
4240
|
+
"border border-border bg-card/60 px-3 py-1.5 text-xs",
|
|
4241
|
+
"text-muted-foreground backdrop-blur transition-all",
|
|
4242
|
+
"hover:-translate-y-px hover:border-primary/40 hover:bg-card",
|
|
4243
|
+
"hover:text-foreground hover:shadow-sm",
|
|
4244
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
4245
|
+
),
|
|
4246
|
+
children: [
|
|
4247
|
+
/* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { className: "h-3 w-3 shrink-0 text-primary opacity-70 group-hover:opacity-100" }),
|
|
4248
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", style: { maxWidth: "32ch" }, children: p })
|
|
4249
|
+
]
|
|
4250
|
+
},
|
|
4251
|
+
p
|
|
4252
|
+
))
|
|
4253
|
+
}
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
function useComposerHandle(ref, onSubmit) {
|
|
4257
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
4258
|
+
const { addFiles } = useComposerContext();
|
|
4259
|
+
react.useEffect(() => {
|
|
4260
|
+
if (!ref) return;
|
|
4261
|
+
const handle = {
|
|
4262
|
+
focus: () => editor.focus(),
|
|
4263
|
+
clear: () => {
|
|
4264
|
+
editor.update(() => {
|
|
4265
|
+
const root = lexical.$getRoot();
|
|
4266
|
+
root.clear();
|
|
4267
|
+
root.append(lexical.$createParagraphNode());
|
|
4268
|
+
});
|
|
4269
|
+
},
|
|
4270
|
+
insert: (text) => {
|
|
4271
|
+
editor.update(() => {
|
|
4272
|
+
const root = lexical.$getRoot();
|
|
4273
|
+
const isEmpty = root.getChildrenSize() === 0 || root.getChildrenSize() === 1 && root.getFirstChild()?.getTextContent() === "";
|
|
4274
|
+
if (isEmpty) {
|
|
4275
|
+
$seedInitialValue(text);
|
|
4276
|
+
} else {
|
|
4277
|
+
$insertTextWithParagraphBreaks(text);
|
|
4278
|
+
}
|
|
4279
|
+
});
|
|
4280
|
+
},
|
|
4281
|
+
submit: () => onSubmit(),
|
|
4282
|
+
addAttachments: (files) => addFiles(files)
|
|
4283
|
+
};
|
|
4284
|
+
if (typeof ref === "function") {
|
|
4285
|
+
ref(handle);
|
|
4286
|
+
return () => ref(null);
|
|
4287
|
+
}
|
|
4288
|
+
ref.current = handle;
|
|
4289
|
+
return () => {
|
|
4290
|
+
ref.current = null;
|
|
4291
|
+
};
|
|
4292
|
+
}, [editor, ref, onSubmit, addFiles]);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
// src/internal/shortcut.ts
|
|
4296
|
+
var MODIFIERS = /* @__PURE__ */ new Set([
|
|
4297
|
+
"mod",
|
|
4298
|
+
"cmd",
|
|
4299
|
+
"command",
|
|
4300
|
+
"meta",
|
|
4301
|
+
"win",
|
|
4302
|
+
"super",
|
|
4303
|
+
"ctrl",
|
|
4304
|
+
"control",
|
|
4305
|
+
"alt",
|
|
4306
|
+
"option",
|
|
4307
|
+
"shift"
|
|
4308
|
+
]);
|
|
4309
|
+
function isMac() {
|
|
4310
|
+
if (typeof navigator === "undefined") return false;
|
|
4311
|
+
const platform = navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent;
|
|
4312
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
4313
|
+
}
|
|
4314
|
+
function parseShortcut(spec) {
|
|
4315
|
+
const segments = spec.split("+").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
4316
|
+
if (segments.length === 0) return null;
|
|
4317
|
+
const mac = isMac();
|
|
4318
|
+
let mod = false;
|
|
4319
|
+
let altMod = false;
|
|
4320
|
+
let shift = false;
|
|
4321
|
+
let alt = false;
|
|
4322
|
+
let key = null;
|
|
4323
|
+
for (const seg of segments) {
|
|
4324
|
+
if (MODIFIERS.has(seg)) {
|
|
4325
|
+
switch (seg) {
|
|
4326
|
+
case "mod":
|
|
4327
|
+
mod = true;
|
|
4328
|
+
break;
|
|
4329
|
+
case "cmd":
|
|
4330
|
+
case "command":
|
|
4331
|
+
case "meta":
|
|
4332
|
+
case "win":
|
|
4333
|
+
case "super":
|
|
4334
|
+
if (mac) mod = true;
|
|
4335
|
+
else altMod = true;
|
|
4336
|
+
break;
|
|
4337
|
+
case "ctrl":
|
|
4338
|
+
case "control":
|
|
4339
|
+
if (mac) altMod = true;
|
|
4340
|
+
else mod = true;
|
|
4341
|
+
break;
|
|
4342
|
+
case "alt":
|
|
4343
|
+
case "option":
|
|
4344
|
+
alt = true;
|
|
4345
|
+
break;
|
|
4346
|
+
case "shift":
|
|
4347
|
+
shift = true;
|
|
4348
|
+
break;
|
|
4349
|
+
}
|
|
4350
|
+
} else {
|
|
4351
|
+
key = seg;
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
if (!key) return null;
|
|
4355
|
+
return { mod, altMod, shift, alt, key };
|
|
4356
|
+
}
|
|
4357
|
+
function matchesShortcut(parsed, event) {
|
|
4358
|
+
const mac = isMac();
|
|
4359
|
+
const platformMod = mac ? event.metaKey : event.ctrlKey;
|
|
4360
|
+
const otherMod = mac ? event.ctrlKey : event.metaKey;
|
|
4361
|
+
if (parsed.mod !== platformMod) return false;
|
|
4362
|
+
if (parsed.altMod !== otherMod) return false;
|
|
4363
|
+
if (parsed.shift !== event.shiftKey) return false;
|
|
4364
|
+
if (parsed.alt !== event.altKey) return false;
|
|
4365
|
+
const eventKey = event.key.length === 1 ? event.key.toLowerCase() : event.key.toLowerCase();
|
|
4366
|
+
return eventKey === parsed.key;
|
|
4367
|
+
}
|
|
4368
|
+
var Composer = react.forwardRef(function Composer2(props, ref) {
|
|
4369
|
+
const {
|
|
4370
|
+
placeholder = "Send a message\u2026",
|
|
4371
|
+
onSend,
|
|
4372
|
+
onStop,
|
|
4373
|
+
isStreaming,
|
|
4374
|
+
autoFocus,
|
|
4375
|
+
refocusOnSubmit = true,
|
|
4376
|
+
focusShortcut = "mod+/",
|
|
4377
|
+
initialValue,
|
|
4378
|
+
className,
|
|
4379
|
+
classNames,
|
|
4380
|
+
sx,
|
|
4381
|
+
style,
|
|
4382
|
+
tokens,
|
|
4383
|
+
color,
|
|
4384
|
+
hint = true,
|
|
4385
|
+
features,
|
|
4386
|
+
toolbarExtras,
|
|
4387
|
+
closeMenusOnOutsideClick = true,
|
|
4388
|
+
mode = "markdown",
|
|
4389
|
+
multiline = true,
|
|
4390
|
+
submitOnEnter = true,
|
|
4391
|
+
smartNewline = true,
|
|
4392
|
+
icons,
|
|
4393
|
+
slots,
|
|
4394
|
+
renderDiagram,
|
|
4395
|
+
prompts,
|
|
4396
|
+
attachmentOptions,
|
|
4397
|
+
dir
|
|
4398
|
+
} = props;
|
|
4399
|
+
const tokenStyle = react.useMemo(() => {
|
|
4400
|
+
const derived = color ? deriveColorTokens(color) : null;
|
|
4401
|
+
if (!derived && !tokens) return void 0;
|
|
4402
|
+
return tokensToStyle({ ...derived, ...tokens });
|
|
4403
|
+
}, [color, tokens]);
|
|
4404
|
+
const root = slotProps("root", "space-y-2", classNames, sx);
|
|
4405
|
+
const rootStyle = react.useMemo(
|
|
4406
|
+
() => ({ ...tokenStyle, ...root.style, ...style }),
|
|
4407
|
+
[tokenStyle, root.style, style]
|
|
4408
|
+
);
|
|
4409
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4410
|
+
ComposerProvider,
|
|
4411
|
+
{
|
|
4412
|
+
features,
|
|
4413
|
+
isStreaming,
|
|
4414
|
+
closeMenusOnOutsideClick,
|
|
4415
|
+
attachmentOptions,
|
|
4416
|
+
mode,
|
|
4417
|
+
multiline,
|
|
4418
|
+
submitOnEnter,
|
|
4419
|
+
smartNewline,
|
|
4420
|
+
focusShortcut,
|
|
4421
|
+
icons,
|
|
4422
|
+
slots,
|
|
4423
|
+
renderDiagram,
|
|
4424
|
+
dir,
|
|
4425
|
+
classNames,
|
|
4426
|
+
sx,
|
|
4427
|
+
tokenStyle,
|
|
4428
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4429
|
+
"div",
|
|
4430
|
+
{
|
|
4431
|
+
dir,
|
|
4432
|
+
className: cn(root.className, className),
|
|
4433
|
+
style: Object.keys(rootStyle).length ? rootStyle : void 0,
|
|
4434
|
+
children: [
|
|
4435
|
+
prompts && prompts.items.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(QuickPrompts, { prompts }) : null,
|
|
4436
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4437
|
+
ComposerCard,
|
|
4438
|
+
{
|
|
4439
|
+
placeholder,
|
|
4440
|
+
initialValue,
|
|
4441
|
+
handleRef: ref,
|
|
4442
|
+
onSend,
|
|
4443
|
+
onStop,
|
|
4444
|
+
autoFocus,
|
|
4445
|
+
refocusOnSubmit,
|
|
4446
|
+
focusShortcut,
|
|
4447
|
+
isStreaming: !!isStreaming,
|
|
4448
|
+
toolbarExtras,
|
|
4449
|
+
mode,
|
|
4450
|
+
multiline
|
|
4451
|
+
}
|
|
4452
|
+
),
|
|
4453
|
+
/* @__PURE__ */ jsxRuntime.jsx(HintBar, { hint })
|
|
4454
|
+
]
|
|
4455
|
+
}
|
|
4456
|
+
)
|
|
4457
|
+
}
|
|
4458
|
+
);
|
|
4459
|
+
});
|
|
4460
|
+
var BLOCK_PARAGRAPH_REPLACEMENT = {
|
|
4461
|
+
replace: lexical.ParagraphNode,
|
|
4462
|
+
with: () => new BlockParagraphNode(),
|
|
4463
|
+
withKlass: BlockParagraphNode
|
|
4464
|
+
};
|
|
4465
|
+
var RICH_NODES = [
|
|
4466
|
+
MentionNode,
|
|
4467
|
+
MarkdownTokenNode,
|
|
4468
|
+
BlockParagraphNode,
|
|
4469
|
+
LinkTextNode,
|
|
4470
|
+
BLOCK_PARAGRAPH_REPLACEMENT
|
|
4471
|
+
];
|
|
4472
|
+
var PLAIN_NODES = [MentionNode];
|
|
4473
|
+
function ComposerCard({
|
|
4474
|
+
placeholder,
|
|
4475
|
+
initialValue,
|
|
4476
|
+
handleRef,
|
|
4477
|
+
onSend,
|
|
4478
|
+
onStop,
|
|
4479
|
+
autoFocus,
|
|
4480
|
+
refocusOnSubmit,
|
|
4481
|
+
focusShortcut,
|
|
4482
|
+
isStreaming,
|
|
4483
|
+
toolbarExtras,
|
|
4484
|
+
mode,
|
|
4485
|
+
multiline
|
|
4486
|
+
}) {
|
|
4487
|
+
const { webEnabled, isDraggingFiles, classNames, sx } = useComposerContext();
|
|
4488
|
+
const card = slotProps(
|
|
4489
|
+
"card",
|
|
4490
|
+
[
|
|
4491
|
+
"group relative border border-border bg-card shadow-soft transition-all focus-within:border-primary/40 focus-within:shadow-glow",
|
|
4492
|
+
webEnabled && "ring-1 ring-primary/20",
|
|
4493
|
+
isDraggingFiles && "ring-2 ring-primary/60"
|
|
4494
|
+
],
|
|
4495
|
+
classNames,
|
|
4496
|
+
sx
|
|
4497
|
+
);
|
|
4498
|
+
const initialConfig = react.useMemo(
|
|
4499
|
+
() => ({
|
|
4500
|
+
namespace: "composeai",
|
|
4501
|
+
theme: composerTheme,
|
|
4502
|
+
onError: (error) => {
|
|
4503
|
+
console.error("[Composer]", error);
|
|
4504
|
+
},
|
|
4505
|
+
nodes: mode === "markdown" ? RICH_NODES : PLAIN_NODES,
|
|
4506
|
+
editorState: initialValue ? void 0 : null
|
|
4507
|
+
}),
|
|
4508
|
+
[mode, initialValue]
|
|
4509
|
+
);
|
|
4510
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4511
|
+
"div",
|
|
4512
|
+
{
|
|
4513
|
+
"data-composer-root": "",
|
|
4514
|
+
"data-composer-inline": multiline ? void 0 : "",
|
|
4515
|
+
...card,
|
|
4516
|
+
children: [
|
|
4517
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4518
|
+
"div",
|
|
4519
|
+
{
|
|
4520
|
+
"aria-hidden": true,
|
|
4521
|
+
"data-composer-overlay": "",
|
|
4522
|
+
className: "pointer-events-none absolute inset-0 opacity-0 transition-opacity group-focus-within:opacity-100",
|
|
4523
|
+
style: {
|
|
4524
|
+
background: "linear-gradient(135deg, hsl(var(--primary)/0.08) 0%, transparent 40%, hsl(var(--primary)/0.06) 100%)"
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
),
|
|
4528
|
+
isDraggingFiles && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4529
|
+
"div",
|
|
4530
|
+
{
|
|
4531
|
+
"aria-hidden": true,
|
|
4532
|
+
"data-composer-overlay": "",
|
|
4533
|
+
className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-primary/5 text-sm font-medium text-primary backdrop-blur-[1px]",
|
|
4534
|
+
children: "Drop to attach"
|
|
4535
|
+
}
|
|
4536
|
+
),
|
|
4537
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalComposer.LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4538
|
+
ComposerInner,
|
|
4539
|
+
{
|
|
4540
|
+
placeholder,
|
|
4541
|
+
mode,
|
|
4542
|
+
multiline,
|
|
4543
|
+
handleRef,
|
|
4544
|
+
onSend,
|
|
4545
|
+
onStop,
|
|
4546
|
+
autoFocus,
|
|
4547
|
+
refocusOnSubmit,
|
|
4548
|
+
focusShortcut,
|
|
4549
|
+
isStreaming,
|
|
4550
|
+
toolbarExtras,
|
|
4551
|
+
initialValue
|
|
4552
|
+
}
|
|
4553
|
+
) })
|
|
4554
|
+
]
|
|
4555
|
+
}
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
function ComposerInner({
|
|
4559
|
+
placeholder,
|
|
4560
|
+
mode,
|
|
4561
|
+
multiline,
|
|
4562
|
+
handleRef,
|
|
4563
|
+
onSend,
|
|
4564
|
+
onStop,
|
|
4565
|
+
autoFocus,
|
|
4566
|
+
refocusOnSubmit,
|
|
4567
|
+
focusShortcut,
|
|
4568
|
+
isStreaming,
|
|
4569
|
+
toolbarExtras,
|
|
4570
|
+
initialValue
|
|
4571
|
+
}) {
|
|
4572
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
4573
|
+
const {
|
|
4574
|
+
features,
|
|
4575
|
+
attachments,
|
|
4576
|
+
clearAttachments,
|
|
4577
|
+
registerRunPrompt,
|
|
4578
|
+
attachmentOptions
|
|
4579
|
+
} = useComposerContext();
|
|
4580
|
+
const canSendOnlyAttachment = attachmentOptions.canSendOnlyAttachment !== false;
|
|
4581
|
+
const hasUploadingAttachment = attachments.some((a) => a.status === "uploading");
|
|
4582
|
+
const hasFailedAttachment = attachments.some((a) => a.status === "failed");
|
|
4583
|
+
const uploadsBlocking = hasUploadingAttachment || hasFailedAttachment;
|
|
4584
|
+
const markdownEnabled = mode === "markdown" && features.markdown;
|
|
4585
|
+
const [hasText, setHasText] = react.useState(
|
|
4586
|
+
!!initialValue && initialValue.trim().length > 0
|
|
4587
|
+
);
|
|
4588
|
+
const onSendRef = react.useRef(onSend);
|
|
4589
|
+
onSendRef.current = onSend;
|
|
4590
|
+
const refocusOnSubmitRef = react.useRef(refocusOnSubmit);
|
|
4591
|
+
refocusOnSubmitRef.current = refocusOnSubmit;
|
|
4592
|
+
const submit = react.useCallback(() => {
|
|
4593
|
+
if (isStreaming) return;
|
|
4594
|
+
if (uploadsBlocking) return;
|
|
4595
|
+
let payload = null;
|
|
4596
|
+
editor.getEditorState().read(() => {
|
|
4597
|
+
const { text, mentions } = collectPlainAndMentions(editor);
|
|
4598
|
+
const markdown = toMarkdown(editor);
|
|
4599
|
+
const trimmed = text.trim();
|
|
4600
|
+
if (!trimmed) {
|
|
4601
|
+
if (attachments.length === 0) return;
|
|
4602
|
+
if (!canSendOnlyAttachment) return;
|
|
4603
|
+
}
|
|
4604
|
+
payload = {
|
|
4605
|
+
text: trimmed,
|
|
4606
|
+
markdown,
|
|
4607
|
+
attachments: [...attachments],
|
|
4608
|
+
mentions
|
|
4609
|
+
};
|
|
4610
|
+
});
|
|
4611
|
+
if (!payload) return;
|
|
4612
|
+
onSendRef.current?.(payload);
|
|
4613
|
+
editor.update(
|
|
4614
|
+
() => {
|
|
4615
|
+
const root = lexical.$getRoot();
|
|
4616
|
+
root.clear();
|
|
4617
|
+
root.append(lexical.$createParagraphNode());
|
|
4618
|
+
},
|
|
4619
|
+
{
|
|
4620
|
+
onUpdate: () => {
|
|
4621
|
+
if (refocusOnSubmitRef.current) {
|
|
4622
|
+
focusEditor(editor);
|
|
4623
|
+
} else {
|
|
4624
|
+
const root = editor.getRootElement();
|
|
4625
|
+
if (root) root.blur();
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
);
|
|
4630
|
+
clearAttachments();
|
|
4631
|
+
}, [
|
|
4632
|
+
editor,
|
|
4633
|
+
attachments,
|
|
4634
|
+
clearAttachments,
|
|
4635
|
+
isStreaming,
|
|
4636
|
+
canSendOnlyAttachment,
|
|
4637
|
+
uploadsBlocking
|
|
4638
|
+
]);
|
|
4639
|
+
useComposerHandle(handleRef, submit);
|
|
4640
|
+
react.useEffect(() => {
|
|
4641
|
+
if (!focusShortcut) return;
|
|
4642
|
+
const parsed = parseShortcut(focusShortcut);
|
|
4643
|
+
if (!parsed) return;
|
|
4644
|
+
const onKey = (e) => {
|
|
4645
|
+
if (!matchesShortcut(parsed, e)) return;
|
|
4646
|
+
if (e.defaultPrevented) return;
|
|
4647
|
+
e.preventDefault();
|
|
4648
|
+
focusEditor(editor);
|
|
4649
|
+
};
|
|
4650
|
+
window.addEventListener("keydown", onKey);
|
|
4651
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
4652
|
+
}, [editor, focusShortcut]);
|
|
4653
|
+
const seededRef = react.useRef(false);
|
|
4654
|
+
react.useEffect(() => {
|
|
4655
|
+
if (seededRef.current) return;
|
|
4656
|
+
seededRef.current = true;
|
|
4657
|
+
if (!initialValue) return;
|
|
4658
|
+
editor.update(() => {
|
|
4659
|
+
$seedInitialValue(initialValue);
|
|
4660
|
+
});
|
|
4661
|
+
}, [editor, initialValue]);
|
|
4662
|
+
react.useEffect(() => {
|
|
4663
|
+
return editor.registerUpdateListener(() => {
|
|
4664
|
+
editor.getEditorState().read(() => {
|
|
4665
|
+
const text = lexical.$getRoot().getTextContent().trim();
|
|
4666
|
+
setHasText(text.length > 0);
|
|
4667
|
+
});
|
|
4668
|
+
});
|
|
4669
|
+
}, [editor]);
|
|
4670
|
+
react.useEffect(() => {
|
|
4671
|
+
return registerRunPrompt((prompt, behavior) => {
|
|
4672
|
+
editor.update(() => {
|
|
4673
|
+
$seedInitialValue(prompt);
|
|
4674
|
+
});
|
|
4675
|
+
if (behavior === "sendValue") {
|
|
4676
|
+
queueMicrotask(() => submit());
|
|
4677
|
+
} else {
|
|
4678
|
+
focusEditor(editor);
|
|
4679
|
+
}
|
|
4680
|
+
});
|
|
4681
|
+
}, [editor, registerRunPrompt, submit]);
|
|
4682
|
+
const toolbarSlot = /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { extras: toolbarExtras });
|
|
4683
|
+
const sendButtonSlot = /* @__PURE__ */ jsxRuntime.jsx(
|
|
4684
|
+
SendButton,
|
|
4685
|
+
{
|
|
4686
|
+
canSend: (
|
|
4687
|
+
// Same gate as `submit`, kept in sync so the disabled state is
|
|
4688
|
+
// never out of step with what would actually happen on click.
|
|
4689
|
+
!uploadsBlocking && (hasText || attachments.length > 0 && canSendOnlyAttachment)
|
|
4690
|
+
),
|
|
4691
|
+
isStreaming,
|
|
4692
|
+
onSend: submit,
|
|
4693
|
+
onStop
|
|
4694
|
+
}
|
|
4695
|
+
);
|
|
4696
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4697
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4698
|
+
EditorShell,
|
|
4699
|
+
{
|
|
4700
|
+
placeholder,
|
|
4701
|
+
mode,
|
|
4702
|
+
multiline,
|
|
4703
|
+
header: /* @__PURE__ */ jsxRuntime.jsx(AttachmentTray, {}),
|
|
4704
|
+
toolbar: toolbarSlot,
|
|
4705
|
+
sendButton: sendButtonSlot,
|
|
4706
|
+
footer: multiline ? /* @__PURE__ */ jsxRuntime.jsx(MermaidSlot, {}) : null
|
|
4707
|
+
}
|
|
4708
|
+
),
|
|
4709
|
+
/* @__PURE__ */ jsxRuntime.jsx(KeyboardPlugin, { onSubmit: submit }),
|
|
4710
|
+
/* @__PURE__ */ jsxRuntime.jsx(AutoFocusPlugin, { enabled: !!autoFocus }),
|
|
4711
|
+
/* @__PURE__ */ jsxRuntime.jsx(PasteDropPlugin, {}),
|
|
4712
|
+
markdownEnabled && /* @__PURE__ */ jsxRuntime.jsx(MarkdownPlugin, {}),
|
|
4713
|
+
features.slashCommands && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4714
|
+
SlashCommandPlugin,
|
|
4715
|
+
{
|
|
4716
|
+
config: features.slashCommands,
|
|
4717
|
+
onSubmit: submit
|
|
4718
|
+
}
|
|
4719
|
+
),
|
|
4720
|
+
features.mentions && /* @__PURE__ */ jsxRuntime.jsx(MentionPlugin, { config: features.mentions }),
|
|
4721
|
+
features.ghostedAutoComplete && /* @__PURE__ */ jsxRuntime.jsx(GhostedAutoCompletePlugin, { config: features.ghostedAutoComplete })
|
|
4722
|
+
] });
|
|
4723
|
+
}
|
|
4724
|
+
function MermaidSlot() {
|
|
4725
|
+
const { features, mode } = useComposerContext();
|
|
4726
|
+
if (mode !== "markdown" || !features.mermaid) return null;
|
|
4727
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MermaidPlugin, {});
|
|
4728
|
+
}
|
|
4729
|
+
function SuggestionRow({ items, onSelect, className }) {
|
|
4730
|
+
const { icons } = useComposerContext();
|
|
4731
|
+
const { sparkle: SparkleIcon } = icons;
|
|
4732
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex flex-wrap justify-center gap-2", className), children: items.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4733
|
+
"button",
|
|
4734
|
+
{
|
|
4735
|
+
type: "button",
|
|
4736
|
+
onClick: () => onSelect(s),
|
|
4737
|
+
className: "group inline-flex items-center gap-1.5 rounded-full border border-border bg-card/60 px-3.5 py-1.5 text-xs text-muted-foreground backdrop-blur transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-card hover:text-foreground hover:shadow-sm",
|
|
4738
|
+
children: [
|
|
4739
|
+
/* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { className: "h-3 w-3 text-primary opacity-70 group-hover:opacity-100" }),
|
|
4740
|
+
s
|
|
4741
|
+
]
|
|
4742
|
+
},
|
|
4743
|
+
s
|
|
4744
|
+
)) });
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
exports.Composer = Composer;
|
|
4748
|
+
exports.SuggestionRow = SuggestionRow;
|
|
4749
|
+
//# sourceMappingURL=index.cjs.map
|
|
4750
|
+
//# sourceMappingURL=index.cjs.map
|