@tensor_1/react 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +102 -1
  2. package/dist/components/Comments.d.ts +18 -2
  3. package/dist/components/Comments.js +37 -12
  4. package/dist/components/EditorIcons.d.ts +39 -0
  5. package/dist/components/EditorIcons.js +51 -0
  6. package/dist/components/EmojiGlyph.d.ts +10 -0
  7. package/dist/components/EmojiGlyph.js +18 -0
  8. package/dist/components/EmojiPicker.d.ts +60 -0
  9. package/dist/components/EmojiPicker.js +277 -0
  10. package/dist/components/Markdown.d.ts +36 -0
  11. package/dist/components/Markdown.js +104 -0
  12. package/dist/components/ReactionBar.d.ts +57 -0
  13. package/dist/components/ReactionBar.js +81 -0
  14. package/dist/components/RelationshipGraph.js +23 -4
  15. package/dist/components/RichTextEditor.d.ts +128 -0
  16. package/dist/components/RichTextEditor.js +954 -0
  17. package/dist/components/SaveStatus.d.ts +32 -0
  18. package/dist/components/SaveStatus.js +21 -0
  19. package/dist/components/Tabs.d.ts +10 -3
  20. package/dist/components/Tabs.js +5 -3
  21. package/dist/emoji-image-WQLh8ZQP.d.cts +128 -0
  22. package/dist/emoji-image.d.ts +25 -0
  23. package/dist/emoji-image.js +80 -0
  24. package/dist/emoji-model.d.ts +66 -0
  25. package/dist/emoji-model.js +108 -0
  26. package/dist/emoji.cjs +871 -0
  27. package/dist/emoji.d.cts +57 -0
  28. package/dist/emoji.d.ts +6 -0
  29. package/dist/emoji.js +18 -0
  30. package/dist/index.cjs +587 -519
  31. package/dist/index.d.cts +59 -64
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/markdown-model.d.ts +75 -0
  35. package/dist/markdown-model.js +438 -0
  36. package/dist/markdown.cjs +585 -0
  37. package/dist/markdown.d.cts +115 -0
  38. package/dist/markdown.d.ts +4 -0
  39. package/dist/markdown.js +9 -0
  40. package/dist/overlay-position-CEGSz6Z2.d.cts +59 -0
  41. package/dist/rich-text/emoji-items.d.ts +14 -0
  42. package/dist/rich-text/emoji-items.js +7 -0
  43. package/dist/rich-text/pipeline.d.ts +91 -0
  44. package/dist/rich-text/pipeline.js +379 -0
  45. package/dist/rich-text/round-trip.d.ts +24 -0
  46. package/dist/rich-text/round-trip.js +11 -0
  47. package/dist/rich-text.cjs +2578 -0
  48. package/dist/rich-text.d.cts +148 -0
  49. package/dist/rich-text.d.ts +8 -0
  50. package/dist/rich-text.js +10 -0
  51. package/dist/url-policy.d.ts +9 -0
  52. package/dist/url-policy.js +11 -0
  53. package/package.json +82 -1
@@ -0,0 +1,2578 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/rich-text.ts
31
+ var rich_text_exports = {};
32
+ __export(rich_text_exports, {
33
+ DEFAULT_RICH_TEXT_LABELS: () => DEFAULT_RICH_TEXT_LABELS,
34
+ RichTextEditor: () => RichTextEditor,
35
+ RichTextEditorFallback: () => RichTextEditorFallback,
36
+ emojiImageFileName: () => emojiImageFileName,
37
+ roundTripMarkdown: () => roundTripMarkdown
38
+ });
39
+ module.exports = __toCommonJS(rich_text_exports);
40
+
41
+ // src/components/RichTextEditor.tsx
42
+ var import_react7 = require("react");
43
+ var import_core2 = require("@tiptap/core");
44
+ var import_react8 = require("@tiptap/react");
45
+ var import_markdown2 = require("@tiptap/markdown");
46
+ var import_extensions = require("@tiptap/extensions");
47
+ var import_extension_image2 = require("@tiptap/extension-image");
48
+ var import_extension_emoji2 = require("@tiptap/extension-emoji");
49
+ var import_suggestion = require("@tiptap/suggestion");
50
+ var import_state = require("@tiptap/pm/state");
51
+ var import_view = require("@tiptap/pm/view");
52
+
53
+ // src/cx.ts
54
+ function cx(...args) {
55
+ return args.filter(Boolean).join(" ");
56
+ }
57
+
58
+ // src/overlay-position.ts
59
+ var OPPOSITE = { top: "bottom", bottom: "top", left: "right", right: "left" };
60
+ function parsePlacement(p) {
61
+ const [side, align] = p.split("-");
62
+ return { side, align: align ?? "center" };
63
+ }
64
+ var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
65
+ function spaceOn(side, a, vp, offset) {
66
+ switch (side) {
67
+ case "top":
68
+ return a.y - offset;
69
+ case "bottom":
70
+ return vp.height - (a.y + a.height) - offset;
71
+ case "left":
72
+ return a.x - offset;
73
+ case "right":
74
+ return vp.width - (a.x + a.width) - offset;
75
+ }
76
+ }
77
+ function fitsOn(side, a, f, vp, offset, padding) {
78
+ const need = side === "top" || side === "bottom" ? f.height : f.width;
79
+ return spaceOn(side, a, vp, offset) >= need + padding;
80
+ }
81
+ function computePosition(anchor, floating, viewport, options = {}) {
82
+ const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true } = options;
83
+ const { side: preferred, align } = parsePlacement(placement);
84
+ let side = preferred;
85
+ if (flip && !fitsOn(side, anchor, floating, viewport, offset, padding)) {
86
+ const opp = OPPOSITE[side];
87
+ if (fitsOn(opp, anchor, floating, viewport, offset, padding) || spaceOn(opp, anchor, viewport, offset) > spaceOn(side, anchor, viewport, offset)) {
88
+ side = opp;
89
+ }
90
+ }
91
+ let x, y;
92
+ const horizontal = side === "top" || side === "bottom";
93
+ if (side === "top") y = anchor.y - floating.height - offset;
94
+ else if (side === "bottom") y = anchor.y + anchor.height + offset;
95
+ else if (side === "left") x = anchor.x - floating.width - offset;
96
+ else x = anchor.x + anchor.width + offset;
97
+ if (horizontal) {
98
+ const start = anchor.x;
99
+ const end = anchor.x + anchor.width - floating.width;
100
+ const center = anchor.x + (anchor.width - floating.width) / 2;
101
+ x = align === "start" ? start : align === "end" ? end : center;
102
+ if (shift) x = clamp(x, padding, Math.max(padding, viewport.width - floating.width - padding));
103
+ } else {
104
+ const start = anchor.y;
105
+ const end = anchor.y + anchor.height - floating.height;
106
+ const center = anchor.y + (anchor.height - floating.height) / 2;
107
+ y = align === "start" ? start : align === "end" ? end : center;
108
+ if (shift) y = clamp(y, padding, Math.max(padding, viewport.height - floating.height - padding));
109
+ }
110
+ return {
111
+ x,
112
+ y,
113
+ placement: align === "center" ? side : `${side}-${align}`,
114
+ side,
115
+ align
116
+ };
117
+ }
118
+
119
+ // src/emoji-model.ts
120
+ var COMPONENT_GROUP_KEY = "component";
121
+ var capitalize = (s) => s ? s[0].toLocaleUpperCase() + s.slice(1) : s;
122
+ function buildEmojiData(locale, compact, messages) {
123
+ const sourceGroups = [...messages.groups].sort((a, b) => a.order - b.order);
124
+ const groups = [];
125
+ const groupIndex = /* @__PURE__ */ new Map();
126
+ for (const g of sourceGroups) {
127
+ if (g.key === COMPONENT_GROUP_KEY) continue;
128
+ groupIndex.set(g.order, groups.length);
129
+ groups.push({ key: g.key, name: capitalize(g.message) });
130
+ }
131
+ const emojis2 = [];
132
+ const names = /* @__PURE__ */ new Map();
133
+ const sorted = [...compact].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
134
+ for (const item of sorted) {
135
+ if (item.group === void 0) continue;
136
+ const group = groupIndex.get(item.group);
137
+ if (group === void 0) continue;
138
+ const entry = { emoji: item.unicode, name: item.label, tags: item.tags ?? [], group };
139
+ emojis2.push(entry);
140
+ names.set(item.unicode, item.label);
141
+ names.set(item.unicode.replace(/️/g, ""), item.label);
142
+ }
143
+ return { locale, groups, emojis: emojis2, names };
144
+ }
145
+ var cache = /* @__PURE__ */ new Map();
146
+ var loadEmojiData = (locale) => {
147
+ let pending = cache.get(locale);
148
+ if (!pending) {
149
+ const files = locale === "de" ? Promise.all([import("emojibase-data/de/compact.json"), import("emojibase-data/de/messages.json")]) : Promise.all([import("emojibase-data/en/compact.json"), import("emojibase-data/en/messages.json")]);
150
+ pending = files.then(
151
+ ([compact, messages]) => buildEmojiData(
152
+ locale,
153
+ compact.default ?? compact,
154
+ messages.default ?? messages
155
+ )
156
+ );
157
+ pending.catch(() => cache.delete(locale));
158
+ cache.set(locale, pending);
159
+ }
160
+ return pending;
161
+ };
162
+ var fold = (s) => s.toLocaleLowerCase().normalize("NFD").replace(/\p{M}/gu, "");
163
+ function searchEmoji(data, query, limit = 64) {
164
+ const q = fold(query.trim());
165
+ if (!q) return [];
166
+ const prefix = [];
167
+ const contains = [];
168
+ for (const entry of data.emojis) {
169
+ const name = fold(entry.name);
170
+ const words = [...name.split(/[\s:,-]+/), ...entry.tags.map(fold)];
171
+ if (words.some((w) => w.startsWith(q)) || name.startsWith(q)) prefix.push(entry);
172
+ else if (name.includes(q)) contains.push(entry);
173
+ if (prefix.length >= limit) break;
174
+ }
175
+ return [...prefix, ...contains].slice(0, limit);
176
+ }
177
+ var DEFAULT_RECENT_KEY = "uix-emoji-recent";
178
+ var RECENT_MAX = 16;
179
+ function readRecentEmoji(key = DEFAULT_RECENT_KEY) {
180
+ try {
181
+ const raw = globalThis.localStorage?.getItem(key);
182
+ const list = raw ? JSON.parse(raw) : [];
183
+ return Array.isArray(list) ? list.filter((e) => typeof e === "string").slice(0, RECENT_MAX) : [];
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+ function pushRecentEmoji(emoji, key = DEFAULT_RECENT_KEY) {
189
+ const next = [emoji, ...readRecentEmoji(key).filter((e) => e !== emoji)].slice(0, RECENT_MAX);
190
+ try {
191
+ globalThis.localStorage?.setItem(key, JSON.stringify(next));
192
+ } catch {
193
+ }
194
+ return next;
195
+ }
196
+ function emojiGridMove(index, key, count, columns) {
197
+ if (count === 0) return null;
198
+ switch (key) {
199
+ case "ArrowRight":
200
+ return Math.min(index + 1, count - 1);
201
+ case "ArrowLeft":
202
+ return Math.max(index - 1, 0);
203
+ case "ArrowDown":
204
+ return Math.min(index + columns, count - 1);
205
+ case "ArrowUp":
206
+ return index - columns < 0 ? null : index - columns;
207
+ case "Home":
208
+ return 0;
209
+ case "End":
210
+ return count - 1;
211
+ default:
212
+ return null;
213
+ }
214
+ }
215
+ function formatLabel(template, values) {
216
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
217
+ }
218
+
219
+ // src/emoji-image.ts
220
+ var warned = /* @__PURE__ */ new Set();
221
+ var devWarn = (message) => {
222
+ if (warned.has(message) || typeof process === "undefined" || process.env.NODE_ENV === "production") return;
223
+ warned.add(message);
224
+ console.warn(message);
225
+ };
226
+ function normalizeEmojiImageBaseUrl(value) {
227
+ if (value === void 0 || value === "") return void 0;
228
+ const sameOrigin = /^(?:\/(?![/\\])|\.\/)/.test(value) && !/[\\\s]/.test(value) && !/^[^/?#]*:/.test(value);
229
+ if (!sameOrigin) {
230
+ devWarn(`uix: emojiImageBaseUrl "${value}" was ignored \u2014 only same-origin paths starting with "/" or "./" are allowed.`);
231
+ return void 0;
232
+ }
233
+ return value.replace(/\/+$/, "") || "/";
234
+ }
235
+ function emojiImageFileName(emoji) {
236
+ return `${Array.from(emoji, (ch) => ch.codePointAt(0).toString(16)).join("-")}.png`;
237
+ }
238
+ function emojiImageSrc(base, emoji) {
239
+ return `${base === "/" ? "" : base}/${emojiImageFileName(emoji)}`;
240
+ }
241
+ var support = /* @__PURE__ */ new Map();
242
+ var context;
243
+ var SIZE = 20;
244
+ function canRenderEmoji(emoji) {
245
+ const cached = support.get(emoji);
246
+ if (cached !== void 0) return cached;
247
+ if (typeof document === "undefined") return true;
248
+ if (context === void 0) {
249
+ try {
250
+ const canvas = document.createElement("canvas");
251
+ canvas.width = SIZE;
252
+ canvas.height = SIZE;
253
+ context = typeof canvas.getContext === "function" ? canvas.getContext("2d", { willReadFrequently: true }) : null;
254
+ } catch {
255
+ context = null;
256
+ }
257
+ }
258
+ if (!context) return true;
259
+ let result = true;
260
+ try {
261
+ context.clearRect(0, 0, SIZE, SIZE);
262
+ context.fillStyle = "#000";
263
+ context.textBaseline = "top";
264
+ context.font = `${SIZE - 4}px sans-serif`;
265
+ context.fillText(emoji, 0, 0);
266
+ const { data } = context.getImageData(0, 0, SIZE, SIZE);
267
+ result = false;
268
+ for (let i = 0; i < data.length; i += 4) {
269
+ const [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
270
+ if (a > 0 && (Math.abs(r - g) > 12 || Math.abs(g - b) > 12)) {
271
+ result = true;
272
+ break;
273
+ }
274
+ }
275
+ } catch {
276
+ result = true;
277
+ }
278
+ support.set(emoji, result);
279
+ return result;
280
+ }
281
+ function emojiPattern() {
282
+ try {
283
+ return new RegExp("\\p{RGI_Emoji}", "gv");
284
+ } catch {
285
+ return /\p{Regional_Indicator}{2}|(?:\p{Extended_Pictographic}|[#*0-9]️?⃣)(?:️|\p{Emoji_Modifier})*(?:‍\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})*)*/gu;
286
+ }
287
+ }
288
+
289
+ // src/rich-text/emoji-items.ts
290
+ var import_extension_emoji = require("@tiptap/extension-emoji");
291
+ var OFFLINE_EMOJI_ITEMS = Object.freeze(
292
+ import_extension_emoji.emojis.filter((item) => typeof item.emoji === "string" && item.emoji !== "").map(({ fallbackImage: _remote, ...item }) => item).filter((item) => !/https?:|\/\//i.test(JSON.stringify(item)))
293
+ );
294
+
295
+ // src/components/EmojiGlyph.tsx
296
+ var import_react = require("react");
297
+ var import_jsx_runtime = require("react/jsx-runtime");
298
+ function useEmojiRenderer(emojiImageBaseUrl) {
299
+ const base = (0, import_react.useMemo)(() => normalizeEmojiImageBaseUrl(emojiImageBaseUrl), [emojiImageBaseUrl]);
300
+ const [mounted, setMounted] = (0, import_react.useState)(false);
301
+ (0, import_react.useEffect)(() => {
302
+ if (base) setMounted(true);
303
+ }, [base]);
304
+ return (0, import_react.useCallback)((emoji) => {
305
+ if (!base || !mounted || canRenderEmoji(emoji)) return emoji;
306
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { className: "uix-emoji-img", src: emojiImageSrc(base, emoji), alt: emoji, draggable: false, decoding: "async" });
307
+ }, [base, mounted]);
308
+ }
309
+
310
+ // src/rich-text/pipeline.ts
311
+ var import_core = require("@tiptap/core");
312
+ var import_markdown = require("@tiptap/markdown");
313
+ var import_extension_list = require("@tiptap/extension-list");
314
+ var import_extension_table = require("@tiptap/extension-table");
315
+ var import_extension_image = require("@tiptap/extension-image");
316
+ var import_starter_kit = require("@tiptap/starter-kit");
317
+ var import_marked = require("marked");
318
+
319
+ // src/url-policy.ts
320
+ var ALLOWED_START = /^(https?:|mailto:|\/|#|\.\/|\.\.\/)/i;
321
+ var CONTROL_OR_BACKSLASH = /[\x00-\x1f\x7f\\]/;
322
+ var PROTOCOL_RELATIVE = /^[/\\]{2}/;
323
+ function defaultIsSafeUrl(url) {
324
+ const candidate = url.replace(/[\t\n\r]/g, "").trim();
325
+ if (CONTROL_OR_BACKSLASH.test(candidate) || PROTOCOL_RELATIVE.test(candidate)) return false;
326
+ return ALLOWED_START.test(candidate);
327
+ }
328
+
329
+ // src/rich-text/pipeline.ts
330
+ var indentContinuation = (h) => {
331
+ let first = true;
332
+ return {
333
+ ...h,
334
+ renderChildren: (nodes) => {
335
+ const out = h.renderChildren(nodes);
336
+ if (!first) return out;
337
+ first = false;
338
+ return out.split("\n").map((line, n) => n > 0 && line ? h.indent(line) : line).join("\n");
339
+ }
340
+ };
341
+ };
342
+ var listItemRender = import_extension_list.ListItem.config.renderMarkdown;
343
+ var UixListItem = import_extension_list.ListItem.extend({
344
+ renderMarkdown: (node, h, ctx) => listItemRender(node, indentContinuation(h), ctx)
345
+ });
346
+ var UixTaskItem = import_extension_list.TaskItem.extend({
347
+ renderMarkdown: (node, h) => {
348
+ const prefix = `- [${node.attrs?.checked ? "x" : " "}] `;
349
+ const [first, ...rest] = node.content ?? [];
350
+ if (first?.type !== "paragraph" || !(first.content ?? []).some((c) => c.type === "hardBreak")) {
351
+ return (0, import_core.renderNestedMarkdownContent)(node, h, prefix);
352
+ }
353
+ const lines = [[]];
354
+ for (const child of first.content ?? []) {
355
+ if (child.type === "hardBreak") lines.push([]);
356
+ else lines[lines.length - 1].push(child);
357
+ }
358
+ const paragraphs = lines.filter((line) => line.length > 0).map((content) => ({ type: "paragraph", content }));
359
+ return (0, import_core.renderNestedMarkdownContent)({ ...node, content: [...paragraphs, ...rest] }, h, prefix);
360
+ }
361
+ });
362
+ function createSchemaExtensions(options = {}) {
363
+ const isSafeUrl = options.isSafeUrl ?? defaultIsSafeUrl;
364
+ return [
365
+ import_starter_kit.StarterKit.configure({
366
+ underline: false,
367
+ listItem: false,
368
+ heading: { levels: [...options.headingLevels ?? [1, 2, 3]] },
369
+ link: {
370
+ openOnClick: false,
371
+ autolink: true,
372
+ linkOnPaste: true,
373
+ defaultProtocol: "https",
374
+ // Unsafe links never become marks: pasted or typed, they stay text.
375
+ isAllowedUri: (url) => isSafeUrl(url),
376
+ shouldAutoLink: (url) => isSafeUrl(url),
377
+ HTMLAttributes: { rel: "noopener noreferrer nofollow", target: null }
378
+ }
379
+ }),
380
+ UixListItem,
381
+ import_extension_list.TaskList,
382
+ UixTaskItem.configure({ nested: true }),
383
+ import_extension_table.TableKit.configure({ table: { resizable: false } }),
384
+ options.image ?? import_extension_image.Image.configure({ inline: true, allowBase64: false })
385
+ ];
386
+ }
387
+ var ENTITY_AFTER_AMP = /^(?:#\d+;|#x[\da-f]+;|[a-z][a-z\d]*;)/i;
388
+ var TAG_START = /^[a-z/!?]$/i;
389
+ var ASCII_PUNCT = /[!-/:-@[-`{-~]/;
390
+ var LIST_MARKER = /^(?:\d+|[ivxlcdmIVXLCDM]+|[a-zA-Z]{1,2})$/;
391
+ var BACKSLASH = String.fromCharCode(92);
392
+ function encodeText(text, options = {}) {
393
+ let out = "";
394
+ const chars = Array.from(text);
395
+ let lineStart = 0;
396
+ let inLead = options.lineStart ?? true;
397
+ for (let i = 0; i < chars.length; i += 1) {
398
+ const ch = chars[i];
399
+ const prev = chars[i - 1] ?? "";
400
+ const next = chars[i + 1] ?? "";
401
+ if (ch === "\n") {
402
+ out += ch;
403
+ lineStart = i + 1;
404
+ inLead = true;
405
+ continue;
406
+ }
407
+ if (inLead && (ch === " " || ch === " ")) {
408
+ continue;
409
+ }
410
+ const atStart = inLead;
411
+ inLead = false;
412
+ const lineRest = chars.slice(i).join("").split("\n")[0];
413
+ const endOrSpace = next === "" || next === "\n" || /\s/u.test(next);
414
+ const markerBefore = !atStart && LIST_MARKER.test(out.slice(out.lastIndexOf("\n") + 1));
415
+ if (
416
+ // `#` always: marked ends a list item at a line starting with `#`, space or not.
417
+ atStart && (ch === "#" || /[-+*]/.test(ch) && endOrSpace) || atStart && /[-=*_]/.test(ch) && new RegExp(`^(?:\\${ch}[ \\t]*)+$`).test(lineRest) || markerBefore && /[.)]/.test(ch) && endOrSpace
418
+ ) {
419
+ out += BACKSLASH + ch;
420
+ continue;
421
+ }
422
+ if (ch === "#" && options.headingEnd && (prev === "" || /\s/u.test(prev)) && /^#+$/.test(lineRest)) {
423
+ out += BACKSLASH + ch;
424
+ continue;
425
+ }
426
+ switch (ch) {
427
+ case BACKSLASH:
428
+ out += ASCII_PUNCT.test(next) || next === "" ? BACKSLASH + BACKSLASH : BACKSLASH;
429
+ break;
430
+ case "`":
431
+ case "[":
432
+ case "]":
433
+ case "~":
434
+ out += BACKSLASH + ch;
435
+ break;
436
+ case "|":
437
+ out += options.inTable ? BACKSLASH + ch : ch;
438
+ break;
439
+ case "&":
440
+ out += ENTITY_AFTER_AMP.test(chars.slice(i + 1, i + 40).join("")) ? "&amp;" : ch;
441
+ break;
442
+ case "<":
443
+ out += TAG_START.test(next) ? "&lt;" : ch;
444
+ break;
445
+ case "!":
446
+ out += next === "" && options.beforeLink ? BACKSLASH + ch : ch;
447
+ break;
448
+ case "*":
449
+ case "_": {
450
+ const spaced = (prev === "" || /\s/u.test(prev)) && (next === "" || /\s/u.test(next));
451
+ const intraword = ch === "_" && /[\p{L}\p{N}]/u.test(prev) && /[\p{L}\p{N}]/u.test(next);
452
+ out += spaced || intraword ? ch : BACKSLASH + ch;
453
+ break;
454
+ }
455
+ case ">":
456
+ out += atStart ? BACKSLASH + ch : ch;
457
+ break;
458
+ default:
459
+ out += ch;
460
+ }
461
+ }
462
+ return out;
463
+ }
464
+ function createMarked() {
465
+ return new import_marked.Marked({
466
+ gfm: true,
467
+ tokenizer: {
468
+ html: () => void 0,
469
+ // `<br>` is the one tag read: GFM tables have no other way to hold a line break,
470
+ // and Tiptap writes hard breaks in cells that way.
471
+ tag: (src) => {
472
+ const br = /^<br\s*\/?>/i.exec(src);
473
+ return br ? { type: "br", raw: br[0] } : void 0;
474
+ }
475
+ }
476
+ });
477
+ }
478
+ var hasLink = (node) => (node?.marks ?? []).some((m) => (typeof m === "string" ? m : m.type) === "link");
479
+ function patchManager(manager) {
480
+ const internals = manager;
481
+ internals.encodeTextForMarkdown = function encode(text, node, parentNode) {
482
+ const inCode = parentNode?.type != null && internals.codeTypes.has(parentNode.type) || (node.marks ?? []).some((m) => internals.codeTypes.has(typeof m === "string" ? m : m.type));
483
+ if (inCode) return text;
484
+ const siblings = parentNode?.content ?? [];
485
+ const index = siblings.indexOf(node);
486
+ const previous = index > 0 ? siblings[index - 1] : void 0;
487
+ const next = index >= 0 ? siblings[index + 1] : void 0;
488
+ return encodeText(text, {
489
+ inTable: !!internals.uixInTable,
490
+ beforeLink: hasLink(next) && !hasLink(node),
491
+ // Unknown position (node not found among its siblings): assume a line start, the safe side.
492
+ lineStart: index <= 0 || previous?.type === "hardBreak",
493
+ headingEnd: parentNode?.type === "heading" && (index < 0 || index === siblings.length - 1)
494
+ });
495
+ };
496
+ return manager;
497
+ }
498
+ function serializeNode(manager, node) {
499
+ const internals = manager;
500
+ internals.uixInTable = node.type === "table";
501
+ try {
502
+ return manager.serialize({ type: "doc", content: [node] }).replace(/^\n+|\n+$/g, "");
503
+ } finally {
504
+ internals.uixInTable = false;
505
+ }
506
+ }
507
+ function createManager(extensions) {
508
+ return patchManager(new import_markdown.MarkdownManager({ marked: createMarked(), extensions }));
509
+ }
510
+ var SEPARATOR_TOKENS = /* @__PURE__ */ new Set(["space", "def"]);
511
+ var nodeKey = (node) => JSON.stringify(node);
512
+ var normalize = (schema, content) => {
513
+ if (content.length === 0) return [];
514
+ const blocks = [];
515
+ for (const node of content) {
516
+ if (node.type && schema.nodes[node.type]?.isInline) {
517
+ const last = blocks[blocks.length - 1];
518
+ if (last?.type === "paragraph" && last.wrapped) last.content.push(node);
519
+ else blocks.push({ type: "paragraph", content: [node], wrapped: true });
520
+ } else {
521
+ blocks.push(node);
522
+ }
523
+ }
524
+ for (const block of blocks) delete block.wrapped;
525
+ const doc = schema.nodeFromJSON({ type: "doc", content: blocks });
526
+ doc.check();
527
+ return doc.toJSON().content ?? [];
528
+ };
529
+ function readMarkdown(source, manager, schema) {
530
+ const normalized = source.replace(/\r\n?/g, "\n");
531
+ const lexed = manager.instance.lexer(normalized);
532
+ const origin = [];
533
+ for (let i = 0, o = 0; i <= normalized.length; i += 1, o += 1) {
534
+ origin.push(o);
535
+ if (source[o] === "\r" && source[o + 1] === "\n") o += 1;
536
+ }
537
+ let cursor = 0;
538
+ const tokens = lexed.map((t) => {
539
+ const start = origin[cursor];
540
+ cursor += t.raw.length;
541
+ return { type: t.type, raw: source.slice(start, origin[cursor]) };
542
+ });
543
+ const joined = lexed.map((t) => t.raw).join("") === normalized ? tokens.map((t) => t.raw).join("") : null;
544
+ const blocks = [];
545
+ const eol = /\r\n/.test(source) && !/(^|[^\r])\n/.test(source) ? "\r\n" : "\n";
546
+ let lead = "";
547
+ let defs = [];
548
+ const keepInLead = (raw) => {
549
+ lead += raw;
550
+ const text = raw.replace(/\s+$/u, "").replace(/^\s+/u, "");
551
+ if (text) defs.push(text);
552
+ };
553
+ const pushBlock = (raw) => {
554
+ let nodes;
555
+ try {
556
+ nodes = normalize(schema, manager.parse(raw).content ?? []);
557
+ } catch {
558
+ nodes = normalize(schema, [{ type: "paragraph", content: [{ type: "text", text: raw }] }]);
559
+ }
560
+ if (nodes.length === 0) {
561
+ keepInLead(raw);
562
+ return;
563
+ }
564
+ blocks.push({ raw, lead, nodes, keys: nodes.map(nodeKey), defs });
565
+ lead = "";
566
+ defs = [];
567
+ };
568
+ if (joined !== source) {
569
+ const core = source.replace(/\s+$/u, "");
570
+ lead = source.slice(0, source.length - source.trimStart().length);
571
+ if (core.trim()) pushBlock(core.trimStart());
572
+ const tail = source.slice(core.length);
573
+ return { source, blocks, tail, tailDefs: [], eol, doc: { type: "doc", content: blocks.flatMap((b) => b.nodes) } };
574
+ }
575
+ for (const token of tokens) {
576
+ if (SEPARATOR_TOKENS.has(token.type) || token.raw.trim() === "") {
577
+ if (token.type === "def") keepInLead(token.raw);
578
+ else lead += token.raw;
579
+ continue;
580
+ }
581
+ const leading = /^(?:[ \t]*\r?\n)*/.exec(token.raw)[0];
582
+ lead += leading;
583
+ const body = token.raw.slice(leading.length);
584
+ const core = body.replace(/\s+$/u, "");
585
+ pushBlock(core);
586
+ lead += body.slice(core.length);
587
+ }
588
+ return {
589
+ source,
590
+ blocks,
591
+ tail: lead,
592
+ tailDefs: defs,
593
+ eol,
594
+ doc: { type: "doc", content: blocks.flatMap((b) => b.nodes) }
595
+ };
596
+ }
597
+ var isEmptyParagraph = (node) => node.type === "paragraph" && !(node.content ?? []).some((c) => c.type !== "text" || (c.text ?? "").trim() !== "");
598
+ var matchesAt = (children, at, block) => block.keys.every((key, k) => children[at + k] === key);
599
+ var LOOKAHEAD = 8;
600
+ function writeMarkdown(doc, origin, manager) {
601
+ const content = [...doc.content ?? []];
602
+ while (content.length > 0 && isEmptyParagraph(content[content.length - 1])) content.pop();
603
+ if (content.length === 0) {
604
+ return origin.blocks.length === 0 ? origin.source : "";
605
+ }
606
+ const keys = content.map(nodeKey);
607
+ const { blocks, eol } = origin;
608
+ const pieces = [];
609
+ let i = 0;
610
+ let j = 0;
611
+ while (i < content.length) {
612
+ if (j < blocks.length && matchesAt(keys, i, blocks[j])) {
613
+ pieces.push({ text: blocks[j].raw, slot: j, edited: false });
614
+ i += blocks[j].keys.length;
615
+ j += 1;
616
+ continue;
617
+ }
618
+ let skip = 0;
619
+ for (let d = 1; d <= LOOKAHEAD && j + d < blocks.length; d += 1) {
620
+ if (matchesAt(keys, i, blocks[j + d])) {
621
+ skip = d;
622
+ break;
623
+ }
624
+ }
625
+ let insert = 0;
626
+ if (j < blocks.length) {
627
+ for (let d = 1; d <= LOOKAHEAD && i + d < content.length; d += 1) {
628
+ if (matchesAt(keys, i + d, blocks[j])) {
629
+ insert = d;
630
+ break;
631
+ }
632
+ }
633
+ }
634
+ if (skip && (!insert || skip <= insert)) {
635
+ j += skip;
636
+ continue;
637
+ }
638
+ let text = serializeNode(manager, content[i]);
639
+ if (eol !== "\n") text = text.replace(/\n/g, eol);
640
+ const inPlace = !insert && j < blocks.length && blocks[j].keys.length === 1;
641
+ pieces.push(inPlace ? { text, slot: j, edited: true } : { text, edited: true });
642
+ i += 1;
643
+ if (!insert && j < blocks.length) j += 1;
644
+ }
645
+ const blank = eol + eol;
646
+ const emitted = /* @__PURE__ */ new Set();
647
+ let out = "";
648
+ pieces.forEach((piece, n) => {
649
+ const prev = pieces[n - 1];
650
+ const slot = piece.slot;
651
+ const lead = slot !== void 0 ? blocks[slot].lead : "";
652
+ if (n === 0) {
653
+ if (slot === 0) {
654
+ out += lead;
655
+ emitted.add(0);
656
+ }
657
+ } else if (slot !== void 0 && prev?.slot === slot - 1 && // Next to a re-serialized block, only a separator with a blank line is safe.
658
+ (!(piece.edited || prev.edited) || /\n[ \t]*\r?\n/.test(lead))) {
659
+ out += lead;
660
+ emitted.add(slot);
661
+ } else {
662
+ out += blank;
663
+ }
664
+ out += piece.text;
665
+ });
666
+ const last = pieces[pieces.length - 1];
667
+ const tailKept = last.slot === blocks.length - 1;
668
+ const lost = [
669
+ ...blocks.flatMap((block, n) => emitted.has(n) ? [] : block.defs),
670
+ ...tailKept ? [] : origin.tailDefs
671
+ ];
672
+ if (lost.length) out += blank + lost.join(eol);
673
+ if (tailKept) out += origin.tail;
674
+ else if (/\n$/.test(origin.tail)) out += eol;
675
+ return out;
676
+ }
677
+ function createPipeline(options = {}) {
678
+ const extensions = createSchemaExtensions(options);
679
+ const schema = (0, import_core.getSchema)(extensions);
680
+ const manager = createManager(extensions);
681
+ return {
682
+ extensions,
683
+ schema,
684
+ manager,
685
+ read: (source) => readMarkdown(source, manager, schema),
686
+ write: (doc, origin) => writeMarkdown(doc, origin, manager)
687
+ };
688
+ }
689
+
690
+ // src/components/Composer.tsx
691
+ var import_react2 = require("react");
692
+ var import_jsx_runtime2 = require("react/jsx-runtime");
693
+ function Composer({ children, className, label = "Add a comment", ...props }) {
694
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx("uix-composer", className), ...props, children: import_react2.Children.map(children, (child) => {
695
+ if (!(0, import_react2.isValidElement)(child) || child.type !== "textarea") return child;
696
+ const p = child.props;
697
+ return p["aria-label"] || p["aria-labelledby"] || p.id ? child : (0, import_react2.cloneElement)(child, { "aria-label": label });
698
+ }) });
699
+ }
700
+ function ComposerBar({ children, className, ...props }) {
701
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx("uix-composer__bar", className), ...props, children });
702
+ }
703
+
704
+ // src/components/EditorIcons.tsx
705
+ var import_jsx_runtime3 = require("react/jsx-runtime");
706
+ var PATHS = {
707
+ heading1: "M4 12h8M4 18V6M12 18V6M17 12l3-2v8",
708
+ heading2: "M4 12h8M4 18V6M12 18V6M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1",
709
+ heading3: "M4 12h8M4 18V6M12 18V6M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2",
710
+ bold: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8",
711
+ italic: "M19 4h-9M14 20H5M15 4 9 20",
712
+ strike: "M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6M4 12h16",
713
+ code: "m16 18 6-6-6-6M8 6l-6 6 6 6",
714
+ bulletList: "M3 12h.01M3 18h.01M3 6h.01M8 12h13M8 18h13M8 6h13",
715
+ orderedList: "M10 12h11M10 18h11M10 6h11M4 10h2M4 6h1v4M6 18H4c0-1 2-2 2-3s-1-1.5-2-1",
716
+ taskList: "m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8",
717
+ blockquote: "M17 6H3M21 12H8M21 18H8M3 12v6",
718
+ codeBlock: "m10 9-3 3 3 3M14 15l3-3-3-3M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z",
719
+ horizontalRule: "M5 12h14",
720
+ link: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
721
+ table: "M12 3v18M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2ZM3 9h18M3 15h18",
722
+ tableAddRow: "M4 3h16a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM12 15v6M9 18h6",
723
+ tableAddColumn: "M4 3h6a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM18 9v6M15 12h6",
724
+ tableDeleteRow: "M4 3h16a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM9 18h6",
725
+ tableDeleteColumn: "M4 3h6a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM15 12h6",
726
+ trash: "M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",
727
+ image: "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2ZM9 7a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM21 15l-3.09-3.09a2 2 0 0 0-2.82 0L6 21",
728
+ emoji: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01",
729
+ addReaction: "M22 11v1a10 10 0 1 1-9-10M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01M16 5h6M19 2v6",
730
+ undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
731
+ redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13",
732
+ sourceMode: "M10 12.5 8 15l2 2.5M14 12.5l2 2.5-2 2.5M14 2v4a2 2 0 0 0 2 2h4M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",
733
+ richMode: "M14 2v4a2 2 0 0 0 2 2h4M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7ZM8 13h8M8 17h5",
734
+ clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM12 6v6l4 2"
735
+ };
736
+ function EditorIcon({ name, ...props }) {
737
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
738
+ "svg",
739
+ {
740
+ viewBox: "0 0 24 24",
741
+ fill: "none",
742
+ stroke: "currentColor",
743
+ strokeWidth: 2,
744
+ strokeLinecap: "round",
745
+ strokeLinejoin: "round",
746
+ "aria-hidden": "true",
747
+ focusable: "false",
748
+ ...props,
749
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: PATHS[name] })
750
+ }
751
+ );
752
+ }
753
+
754
+ // src/components/EmojiPicker.tsx
755
+ var import_react5 = require("react");
756
+
757
+ // src/components/Popover.tsx
758
+ var import_react4 = require("react");
759
+
760
+ // src/hooks/useAnchoredPosition.ts
761
+ var import_react3 = require("react");
762
+ var useIsomorphicLayoutEffect = typeof document !== "undefined" ? import_react3.useLayoutEffect : import_react3.useEffect;
763
+ var resolve = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
764
+ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
765
+ const opts = (0, import_react3.useRef)({ placement, offset, padding });
766
+ opts.current = { placement, offset, padding };
767
+ const reposition = (0, import_react3.useCallback)(() => {
768
+ const anchorEl = resolve(anchor);
769
+ const floating = floatingRef.current;
770
+ if (!anchorEl || !floating) return;
771
+ const s = floating.style;
772
+ s.position = "fixed";
773
+ s.inset = "auto";
774
+ s.margin = "0";
775
+ const a = anchorEl.getBoundingClientRect();
776
+ const { placement: p, offset: o, padding: pad } = opts.current;
777
+ const pos = computePosition(
778
+ { x: a.x, y: a.y, width: a.width, height: a.height },
779
+ { width: floating.offsetWidth, height: floating.offsetHeight },
780
+ { width: window.innerWidth, height: window.innerHeight },
781
+ { placement: p, offset: o, padding: pad }
782
+ );
783
+ s.left = `${Math.round(pos.x)}px`;
784
+ s.top = `${Math.round(pos.y)}px`;
785
+ floating.dataset.placement = pos.placement;
786
+ }, [anchor, floatingRef]);
787
+ useIsomorphicLayoutEffect(() => {
788
+ if (!open) return;
789
+ reposition();
790
+ const onScroll = () => reposition();
791
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
792
+ window.addEventListener("resize", onScroll);
793
+ return () => {
794
+ window.removeEventListener("scroll", onScroll, true);
795
+ window.removeEventListener("resize", onScroll);
796
+ };
797
+ }, [open, reposition]);
798
+ return reposition;
799
+ }
800
+
801
+ // src/components/Popover.tsx
802
+ var import_jsx_runtime4 = require("react/jsx-runtime");
803
+ function Popover({
804
+ popover = "auto",
805
+ anchor,
806
+ placement = "bottom-start",
807
+ offset = 6,
808
+ className,
809
+ children,
810
+ ...props
811
+ }) {
812
+ const ref = (0, import_react4.useRef)(null);
813
+ const [open, setOpen] = (0, import_react4.useState)(false);
814
+ const popoverAttr = { popover };
815
+ const reposition = useAnchoredPosition(anchor ?? null, ref, {
816
+ open: open && !!anchor,
817
+ placement,
818
+ offset
819
+ });
820
+ (0, import_react4.useEffect)(() => {
821
+ const el = ref.current;
822
+ if (!el || !anchor) return;
823
+ const sync = () => {
824
+ const isOpen = el.matches(":popover-open");
825
+ if (isOpen) reposition();
826
+ setOpen(isOpen);
827
+ };
828
+ el.addEventListener("toggle", sync);
829
+ return () => el.removeEventListener("toggle", sync);
830
+ }, [anchor, reposition]);
831
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
832
+ }
833
+
834
+ // src/components/EmojiPicker.tsx
835
+ var import_jsx_runtime5 = require("react/jsx-runtime");
836
+ var DEFAULT_EMOJI_PICKER_LABELS = {
837
+ dialog: "Choose an emoji",
838
+ search: "Search emoji",
839
+ recent: "Recently used",
840
+ quickPicks: "Frequently used",
841
+ categories: "Emoji categories",
842
+ noResults: "No emoji found",
843
+ loading: "Loading emoji\u2026",
844
+ loadFailed: "Emoji could not be loaded."
845
+ };
846
+ var EMOJI_GRID_COLUMNS = 8;
847
+ function EmojiPicker({
848
+ onSelect,
849
+ trigger,
850
+ labels: labelOverrides,
851
+ locale = "en",
852
+ quickPicks,
853
+ loadData = loadEmojiData,
854
+ recentStorageKey = DEFAULT_RECENT_KEY,
855
+ placement = "bottom-start",
856
+ onOpenChange,
857
+ className,
858
+ emojiImageBaseUrl
859
+ }) {
860
+ const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
861
+ const labels = { ...DEFAULT_EMOJI_PICKER_LABELS, ...labelOverrides };
862
+ const id = (0, import_react5.useId)();
863
+ const popoverId = `${id}-picker`;
864
+ const anchorRef = (0, import_react5.useRef)(null);
865
+ const searchRef = (0, import_react5.useRef)(null);
866
+ const gridRef = (0, import_react5.useRef)(null);
867
+ const pointerWasOpen = (0, import_react5.useRef)(null);
868
+ const [open, setOpen] = (0, import_react5.useState)(false);
869
+ const [data, setData] = (0, import_react5.useState)(null);
870
+ const [failed, setFailed] = (0, import_react5.useState)(false);
871
+ const [query, setQuery] = (0, import_react5.useState)("");
872
+ const [recent, setRecent] = (0, import_react5.useState)([]);
873
+ const [active, setActive] = (0, import_react5.useState)(0);
874
+ const popoverEl = () => typeof document === "undefined" ? null : document.getElementById(popoverId);
875
+ const isOpen = (el) => {
876
+ try {
877
+ return !!el?.matches(":popover-open");
878
+ } catch {
879
+ return false;
880
+ }
881
+ };
882
+ (0, import_react5.useEffect)(() => {
883
+ const el = popoverEl();
884
+ if (!el) return;
885
+ const onToggle = (event) => {
886
+ const { newState } = event;
887
+ const nowOpen = newState ? newState === "open" : isOpen(el);
888
+ setOpen(nowOpen);
889
+ onOpenChange?.(nowOpen);
890
+ if (nowOpen) {
891
+ setRecent(readRecentEmoji(recentStorageKey));
892
+ setQuery("");
893
+ setActive(0);
894
+ requestAnimationFrame(() => searchRef.current?.focus());
895
+ } else if (el.contains(document.activeElement) || document.activeElement === document.body) {
896
+ focusTrigger();
897
+ }
898
+ };
899
+ el.addEventListener("toggle", onToggle);
900
+ return () => el.removeEventListener("toggle", onToggle);
901
+ }, [popoverId, recentStorageKey, onOpenChange]);
902
+ (0, import_react5.useEffect)(() => {
903
+ if (!open || data?.locale === locale) return;
904
+ let live = true;
905
+ setFailed(false);
906
+ loadData(locale).then(
907
+ (loaded) => {
908
+ if (live) setData(loaded);
909
+ },
910
+ () => {
911
+ if (live) setFailed(true);
912
+ }
913
+ );
914
+ return () => {
915
+ live = false;
916
+ };
917
+ }, [open, locale, loadData, data?.locale]);
918
+ const focusTrigger = () => {
919
+ const el = anchorRef.current?.querySelector("button, [href], [tabindex]");
920
+ el?.focus();
921
+ };
922
+ const hide = () => {
923
+ const el = popoverEl();
924
+ if (el && isOpen(el)) el.hidePopover();
925
+ };
926
+ const sections = (0, import_react5.useMemo)(() => {
927
+ if (query.trim()) {
928
+ return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
929
+ }
930
+ const list = [];
931
+ if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
932
+ if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
933
+ if (data) {
934
+ data.groups.forEach((group, index) => {
935
+ list.push({ key: group.key, title: group.name, emojis: data.emojis.filter((e) => e.group === index).map((e) => e.emoji) });
936
+ });
937
+ }
938
+ return list;
939
+ }, [query, data, quickPicks, recent, labels.search, labels.quickPicks, labels.recent]);
940
+ const flat = (0, import_react5.useMemo)(() => sections.flatMap((s) => s.emojis), [sections]);
941
+ const sectionStart = (0, import_react5.useMemo)(() => {
942
+ const starts = /* @__PURE__ */ new Map();
943
+ let at = 0;
944
+ for (const section of sections) {
945
+ starts.set(section.key, at);
946
+ at += section.emojis.length;
947
+ }
948
+ return starts;
949
+ }, [sections]);
950
+ const activeIndex = Math.min(active, Math.max(flat.length - 1, 0));
951
+ const choose = (emoji) => {
952
+ setRecent(pushRecentEmoji(emoji, recentStorageKey));
953
+ onSelect(emoji);
954
+ hide();
955
+ };
956
+ const focusCell = (0, import_react5.useCallback)((index) => {
957
+ setActive(index);
958
+ const find = () => gridRef.current?.querySelector(`[data-emoji-index="${index}"]`);
959
+ const cell = find();
960
+ if (cell) cell.focus();
961
+ else requestAnimationFrame(() => find()?.focus());
962
+ }, []);
963
+ const onGridKeyDown = (event) => {
964
+ const target = event.target;
965
+ const index = Number(target.dataset.emojiIndex ?? -1);
966
+ if (index < 0) return;
967
+ const next = emojiGridMove(index, event.key, flat.length, EMOJI_GRID_COLUMNS);
968
+ if (event.key === "ArrowUp" && next === null) {
969
+ event.preventDefault();
970
+ searchRef.current?.focus();
971
+ return;
972
+ }
973
+ if (next === null) return;
974
+ event.preventDefault();
975
+ focusCell(next);
976
+ };
977
+ const onSearchKeyDown = (event) => {
978
+ if (event.key === "Escape") {
979
+ event.preventDefault();
980
+ hide();
981
+ } else if (event.key === "ArrowDown" && flat.length) {
982
+ event.preventDefault();
983
+ focusCell(0);
984
+ } else if (event.key === "Enter" && query.trim() && flat[0]) {
985
+ event.preventDefault();
986
+ choose(flat[0]);
987
+ }
988
+ };
989
+ const toggleFromTrigger = (event) => {
990
+ const el = popoverEl();
991
+ if (!el || event.defaultPrevented) return;
992
+ const was = pointerWasOpen.current;
993
+ pointerWasOpen.current = null;
994
+ if (was === true) return;
995
+ if (isOpen(el)) el.hidePopover();
996
+ else el.showPopover();
997
+ };
998
+ const triggerProps = (0, import_react5.isValidElement)(trigger) ? trigger.props : {};
999
+ const triggerEl = (0, import_react5.isValidElement)(trigger) ? (0, import_react5.cloneElement)(trigger, {
1000
+ "aria-haspopup": "dialog",
1001
+ "aria-expanded": open,
1002
+ "aria-controls": popoverId,
1003
+ onPointerDown: (event) => {
1004
+ pointerWasOpen.current = isOpen(popoverEl());
1005
+ triggerProps.onPointerDown?.(event);
1006
+ },
1007
+ onClick: (event) => {
1008
+ triggerProps.onClick?.(event);
1009
+ toggleFromTrigger(event);
1010
+ }
1011
+ }) : trigger;
1012
+ const names = data?.names;
1013
+ let offset = 0;
1014
+ const status = failed ? labels.loadFailed : !data && open ? labels.loading : query.trim() && data && flat.length === 0 ? labels.noResults : "";
1015
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { ref: anchorRef, className: cx("uix-emoji-picker__anchor", className), children: [
1016
+ triggerEl,
1017
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1018
+ Popover,
1019
+ {
1020
+ id: popoverId,
1021
+ anchor: anchorRef,
1022
+ placement,
1023
+ className: "uix-emoji-picker-popover",
1024
+ role: "dialog",
1025
+ "aria-label": labels.dialog,
1026
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uix-emoji-picker uix-emoji-picker--full", lang: locale, children: [
1027
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1028
+ "input",
1029
+ {
1030
+ ref: searchRef,
1031
+ type: "search",
1032
+ className: "uix-input uix-emoji-picker__search",
1033
+ "aria-label": labels.search,
1034
+ placeholder: labels.search,
1035
+ value: query,
1036
+ onChange: (event) => {
1037
+ setQuery(event.target.value);
1038
+ setActive(0);
1039
+ },
1040
+ onKeyDown: onSearchKeyDown,
1041
+ autoComplete: "off",
1042
+ spellCheck: false
1043
+ }
1044
+ ),
1045
+ data && !query.trim() ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uix-emoji-picker__nav", role: "group", "aria-label": labels.categories, children: sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1046
+ "button",
1047
+ {
1048
+ type: "button",
1049
+ className: "uix-emoji-picker__nav-btn",
1050
+ "aria-label": section.title,
1051
+ title: section.title,
1052
+ onClick: () => {
1053
+ const start = section.emojis.length ? sectionStart.get(section.key) ?? -1 : -1;
1054
+ gridRef.current?.querySelector(`[data-section="${section.key}"]`)?.scrollIntoView({ block: "start" });
1055
+ if (start >= 0) focusCell(start);
1056
+ },
1057
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
1058
+ },
1059
+ section.key
1060
+ )) }) : null,
1061
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: sections.map((section) => {
1062
+ const start = offset;
1063
+ offset += section.emojis.length;
1064
+ if (!section.emojis.length) return null;
1065
+ const headingId = `${id}-${section.key}`;
1066
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
1067
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
1068
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
1069
+ const index = start + i;
1070
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1071
+ "button",
1072
+ {
1073
+ type: "button",
1074
+ className: "uix-emoji-picker__btn",
1075
+ "data-emoji-index": index,
1076
+ tabIndex: index === activeIndex ? 0 : -1,
1077
+ "aria-label": names?.get(emoji) ?? emoji,
1078
+ title: names?.get(emoji),
1079
+ onClick: () => choose(emoji),
1080
+ onFocus: () => setActive(index),
1081
+ children: renderEmoji(emoji)
1082
+ },
1083
+ `${section.key}-${emoji}`
1084
+ );
1085
+ }) })
1086
+ ] }, section.key);
1087
+ }) }),
1088
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "uix-emoji-picker__status", role: "status", children: status })
1089
+ ] }) : null
1090
+ }
1091
+ )
1092
+ ] });
1093
+ }
1094
+
1095
+ // src/markdown-model.ts
1096
+ var ASCII_PUNCT2 = /[!-/:-@[-`{-~]/;
1097
+ var WORD = /[\p{L}\p{N}]/u;
1098
+ var NAMED_ENTITIES = {
1099
+ amp: "&",
1100
+ lt: "<",
1101
+ gt: ">",
1102
+ quot: '"',
1103
+ apos: "'",
1104
+ nbsp: "\xA0",
1105
+ copy: "\xA9",
1106
+ reg: "\xAE",
1107
+ hellip: "\u2026",
1108
+ mdash: "\u2014",
1109
+ ndash: "\u2013"
1110
+ };
1111
+ var ENTITY_RE = /^&(?:#(\d{1,7})|#[xX]([\da-fA-F]{1,6})|([a-zA-Z][a-zA-Z\d]{1,31}));/;
1112
+ var ENTITY_GLOBAL = /&(?:#\d{1,7}|#[xX][\da-fA-F]{1,6}|[a-zA-Z][a-zA-Z\d]{1,31});/g;
1113
+ var BARE_URL_RE = /^https?:\/\/[^\s<>()]+/;
1114
+ var BR_RE = /^<br\s*\/?>/i;
1115
+ var AUTOLINK_RE = /^<((?:https?:|mailto:)[^\s<>]*)>/i;
1116
+ var decodeEntity = (match) => {
1117
+ const [, dec, hex, name] = match;
1118
+ const code = dec ? Number(dec) : hex ? parseInt(hex, 16) : void 0;
1119
+ if (code !== void 0) return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : "\uFFFD";
1120
+ return name && Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, name) ? NAMED_ENTITIES[name] : null;
1121
+ };
1122
+ var MAX_SCAN = 2e3;
1123
+ var MAX_DEPTH = 12;
1124
+ var findBracketEnd = (text, open) => {
1125
+ let depth = 0;
1126
+ const stop = Math.min(text.length, open + MAX_SCAN);
1127
+ for (let i = open; i < stop; i += 1) {
1128
+ const ch = text[i];
1129
+ if (ch === "\\") {
1130
+ i += 1;
1131
+ continue;
1132
+ }
1133
+ if (ch === "`") {
1134
+ const close = text.indexOf("`", i + 1);
1135
+ if (close > 0) {
1136
+ i = close;
1137
+ continue;
1138
+ }
1139
+ }
1140
+ if (ch === "[") depth += 1;
1141
+ else if (ch === "]") {
1142
+ depth -= 1;
1143
+ if (depth === 0) return i;
1144
+ }
1145
+ }
1146
+ return -1;
1147
+ };
1148
+ var readDestination = (text, open) => {
1149
+ if (text[open] !== "(") return null;
1150
+ let depth = 0;
1151
+ let end = -1;
1152
+ const stop = Math.min(text.length, open + MAX_SCAN);
1153
+ for (let i = open; i < stop; i += 1) {
1154
+ const ch = text[i];
1155
+ if (ch === "\\") {
1156
+ i += 1;
1157
+ continue;
1158
+ }
1159
+ if (ch === "(") depth += 1;
1160
+ else if (ch === ")") {
1161
+ depth -= 1;
1162
+ if (depth === 0) {
1163
+ end = i;
1164
+ break;
1165
+ }
1166
+ }
1167
+ }
1168
+ if (end < 0) return null;
1169
+ const inner = text.slice(open + 1, end).trim();
1170
+ const titled = /^(\S+)\s+(?:"([^"]*)"|'([^']*)')$/.exec(inner);
1171
+ const rawUrl = titled ? titled[1] : inner;
1172
+ const url = rawUrl.replace(/^<(.*)>$/, "$1").replace(/\\([!-/:-@[-`{-~])/g, "$1").replace(ENTITY_GLOBAL, (match) => decodeEntity(ENTITY_RE.exec(match)) ?? match);
1173
+ const title = titled ? titled[2] ?? titled[3] : void 0;
1174
+ return title === void 0 ? { url, end } : { url, title, end };
1175
+ };
1176
+ var backtickRun = (text, at) => {
1177
+ let end = at;
1178
+ while (text[end] === "`") end += 1;
1179
+ return text.slice(at, end);
1180
+ };
1181
+ var findCloser = (text, from, delim, misses) => {
1182
+ if (from >= (misses.get(delim) ?? Infinity)) return -1;
1183
+ for (let i = from; i < text.length; i += 1) {
1184
+ const ch = text[i];
1185
+ if (ch === "\\") {
1186
+ i += 1;
1187
+ continue;
1188
+ }
1189
+ if (ch === "`") {
1190
+ const run = backtickRun(text, i);
1191
+ const close = text.indexOf(run, i + run.length);
1192
+ if (close > 0) {
1193
+ i = close + run.length - 1;
1194
+ continue;
1195
+ }
1196
+ }
1197
+ if (text.startsWith(delim, i) && i > from && !/\s/u.test(text[i - 1])) {
1198
+ if (delim[0] === "_" && WORD.test(text[i + delim.length] ?? "")) continue;
1199
+ if (delim.length === 1 && text[i + 1] === delim) {
1200
+ i += 1;
1201
+ continue;
1202
+ }
1203
+ return i;
1204
+ }
1205
+ }
1206
+ misses.set(delim, Math.min(from, misses.get(delim) ?? Infinity));
1207
+ return -1;
1208
+ };
1209
+ var pushText = (out, value) => {
1210
+ if (!value) return;
1211
+ const last = out[out.length - 1];
1212
+ if (last?.type === "text") last.value += value;
1213
+ else out.push({ type: "text", value });
1214
+ };
1215
+ function parseInline(text, depth = 0) {
1216
+ const out = [];
1217
+ if (depth > MAX_DEPTH) {
1218
+ pushText(out, text);
1219
+ return out;
1220
+ }
1221
+ const misses = /* @__PURE__ */ new Map();
1222
+ let i = 0;
1223
+ while (i < text.length) {
1224
+ const ch = text[i];
1225
+ const rest = text.slice(i);
1226
+ if (ch === "\\" && ASCII_PUNCT2.test(text[i + 1] ?? "")) {
1227
+ pushText(out, text[i + 1]);
1228
+ i += 2;
1229
+ continue;
1230
+ }
1231
+ if (ch === "\n") {
1232
+ out.push({ type: "br" });
1233
+ i += 1;
1234
+ continue;
1235
+ }
1236
+ if (ch === "`") {
1237
+ const run = backtickRun(text, i);
1238
+ const close = text.indexOf(run, i + run.length);
1239
+ if (close > 0 && text[close + run.length] !== "`") {
1240
+ let value = text.slice(i + run.length, close).replace(/\n/g, " ");
1241
+ if (/^ .* $/.test(value) && value.trim()) value = value.slice(1, -1);
1242
+ out.push({ type: "code", value });
1243
+ i = close + run.length;
1244
+ continue;
1245
+ }
1246
+ pushText(out, run);
1247
+ i += run.length;
1248
+ continue;
1249
+ }
1250
+ if (ch === "!" && text[i + 1] === "[") {
1251
+ const end = findBracketEnd(text, i + 1);
1252
+ const dest = end > 0 ? readDestination(text, end + 1) : null;
1253
+ if (dest) {
1254
+ const alt = plainText(parseInline(text.slice(i + 2, end), depth + 1));
1255
+ out.push(dest.title === void 0 ? { type: "image", src: dest.url, alt } : { type: "image", src: dest.url, alt, title: dest.title });
1256
+ i = dest.end + 1;
1257
+ continue;
1258
+ }
1259
+ }
1260
+ if (ch === "[") {
1261
+ const end = findBracketEnd(text, i);
1262
+ const dest = end > 0 ? readDestination(text, end + 1) : null;
1263
+ if (dest) {
1264
+ const children = parseInline(text.slice(i + 1, end), depth + 1);
1265
+ out.push(dest.title === void 0 ? { type: "link", href: dest.url, children } : { type: "link", href: dest.url, title: dest.title, children });
1266
+ i = dest.end + 1;
1267
+ continue;
1268
+ }
1269
+ }
1270
+ if (ch === "<") {
1271
+ const br = BR_RE.exec(rest);
1272
+ if (br) {
1273
+ out.push({ type: "br" });
1274
+ i += br[0].length;
1275
+ continue;
1276
+ }
1277
+ const auto = AUTOLINK_RE.exec(rest);
1278
+ if (auto) {
1279
+ out.push({ type: "link", href: auto[1], children: [{ type: "text", value: auto[1].replace(/^mailto:/i, "") }], bare: true });
1280
+ i += auto[0].length;
1281
+ continue;
1282
+ }
1283
+ }
1284
+ if (ch === "&") {
1285
+ const entity = ENTITY_RE.exec(rest);
1286
+ const decoded = entity ? decodeEntity(entity) : null;
1287
+ if (entity && decoded !== null) {
1288
+ pushText(out, decoded);
1289
+ i += entity[0].length;
1290
+ continue;
1291
+ }
1292
+ }
1293
+ if (ch === "~" && text[i + 1] === "~" && text[i + 2] && !/\s/u.test(text[i + 2])) {
1294
+ const close = findCloser(text, i + 2, "~~", misses);
1295
+ if (close > 0) {
1296
+ out.push({ type: "del", children: parseInline(text.slice(i + 2, close), depth + 1) });
1297
+ i = close + 2;
1298
+ continue;
1299
+ }
1300
+ }
1301
+ if (ch === "*" || ch === "_") {
1302
+ const leftOk = ch === "*" || !WORD.test(text[i - 1] ?? "");
1303
+ const double = text[i + 1] === ch;
1304
+ const delim = double ? ch + ch : ch;
1305
+ const after = text[i + delim.length];
1306
+ if (leftOk && after && !/\s/u.test(after)) {
1307
+ const close = findCloser(text, i + delim.length, delim, misses);
1308
+ if (close > 0) {
1309
+ const children = parseInline(text.slice(i + delim.length, close), depth + 1);
1310
+ out.push(double ? { type: "strong", children } : { type: "em", children });
1311
+ i = close + delim.length;
1312
+ continue;
1313
+ }
1314
+ }
1315
+ const run = new RegExp(`^\\${ch}+`).exec(rest)[0];
1316
+ pushText(out, run);
1317
+ i += run.length;
1318
+ continue;
1319
+ }
1320
+ if (ch === "h" && !WORD.test(text[i - 1] ?? "")) {
1321
+ const bare = BARE_URL_RE.exec(rest);
1322
+ if (bare) {
1323
+ const url = bare[0].replace(/[.,;:!?'"]+$/, "");
1324
+ out.push({ type: "link", href: url, children: [{ type: "text", value: url }], bare: true });
1325
+ i += url.length;
1326
+ continue;
1327
+ }
1328
+ }
1329
+ pushText(out, ch);
1330
+ i += 1;
1331
+ }
1332
+ return out;
1333
+ }
1334
+ function plainText(nodes) {
1335
+ return nodes.map((n) => {
1336
+ switch (n.type) {
1337
+ case "text":
1338
+ case "code":
1339
+ return n.value;
1340
+ case "br":
1341
+ return " ";
1342
+ case "image":
1343
+ return n.alt;
1344
+ default:
1345
+ return plainText(n.children);
1346
+ }
1347
+ }).join("");
1348
+ }
1349
+ var FENCE_RE = /^(`{3,}|~{3,})\s*([^`\s]*)/;
1350
+ var HEADING_RE = /^(#{1,6})\s+(.*?)(?:\s+#+)?$/;
1351
+ var RULE_RE = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
1352
+ var BULLET_RE = /^(\s*)([-*+])\s+(.*)$/;
1353
+ var ORDERED_RE = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
1354
+ var TASK_RE = /^\[([ xX])\]\s+(.*)$/;
1355
+ var DELIMITER_ROW_RE = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/;
1356
+ var indentOf = (s) => s.replace(/\t/g, " ").length;
1357
+ function splitTableRow(line) {
1358
+ let row = line.trim();
1359
+ if (row.startsWith("|")) row = row.slice(1);
1360
+ if (row.endsWith("|") && !row.endsWith("\\|")) row = row.slice(0, -1);
1361
+ const cells = [];
1362
+ let cell = "";
1363
+ let inCode = false;
1364
+ for (let i = 0; i < row.length; i += 1) {
1365
+ const ch = row[i];
1366
+ if (ch === "\\" && row[i + 1] === "|") {
1367
+ cell += "|";
1368
+ i += 1;
1369
+ continue;
1370
+ }
1371
+ if (ch === "`") inCode = !inCode;
1372
+ if (ch === "|" && !inCode) {
1373
+ cells.push(cell.trim());
1374
+ cell = "";
1375
+ continue;
1376
+ }
1377
+ cell += ch;
1378
+ }
1379
+ cells.push(cell.trim());
1380
+ return cells;
1381
+ }
1382
+ var alignOf = (cell) => {
1383
+ const c = cell.trim();
1384
+ const left = c.startsWith(":");
1385
+ const right = c.endsWith(":");
1386
+ return left && right ? "center" : right ? "right" : left ? "left" : null;
1387
+ };
1388
+ var matchListLine = (line) => {
1389
+ const bullet = BULLET_RE.exec(line);
1390
+ if (bullet && !RULE_RE.test(line.trim())) return { indent: indentOf(bullet[1]), ordered: false, number: 1, text: bullet[3] };
1391
+ const ordered = ORDERED_RE.exec(line);
1392
+ if (ordered) return { indent: indentOf(ordered[1]), ordered: true, number: Number(ordered[2]), text: ordered[3] };
1393
+ return null;
1394
+ };
1395
+ var toItem = (text) => {
1396
+ const task = TASK_RE.exec(text);
1397
+ return task ? { checked: task[1] !== " ", children: parseInline(task[2]), lists: [] } : { checked: null, children: parseInline(text), lists: [] };
1398
+ };
1399
+ var buildList = (lines) => {
1400
+ const lists = [];
1401
+ const stack = [];
1402
+ for (const line of lines) {
1403
+ while (stack.length && line.indent < stack[stack.length - 1].indent) stack.pop();
1404
+ let top = stack[stack.length - 1];
1405
+ if (top && line.indent > top.indent) {
1406
+ const parentItem = top.list.items[top.list.items.length - 1];
1407
+ const nested = { type: "list", ordered: line.ordered, start: line.number, items: [] };
1408
+ parentItem.lists.push(nested);
1409
+ stack.push({ list: nested, indent: line.indent });
1410
+ top = stack[stack.length - 1];
1411
+ } else if (!top || top.list.ordered !== line.ordered) {
1412
+ const list = { type: "list", ordered: line.ordered, start: line.number, items: [] };
1413
+ if (stack.length <= 1) {
1414
+ lists.push(list);
1415
+ stack.length = 0;
1416
+ } else {
1417
+ stack.pop();
1418
+ const parent = stack[stack.length - 1].list;
1419
+ parent.items[parent.items.length - 1].lists.push(list);
1420
+ }
1421
+ stack.push({ list, indent: line.indent });
1422
+ top = stack[stack.length - 1];
1423
+ }
1424
+ top.list.items.push(toItem(line.text));
1425
+ }
1426
+ return lists;
1427
+ };
1428
+ function parseMarkdown(source) {
1429
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
1430
+ const blocks = [];
1431
+ let paragraph = [];
1432
+ let i = 0;
1433
+ const flush = () => {
1434
+ if (paragraph.length) {
1435
+ blocks.push({ type: "paragraph", children: parseInline(paragraph.join("\n")) });
1436
+ paragraph = [];
1437
+ }
1438
+ };
1439
+ while (i < lines.length) {
1440
+ const line = lines[i];
1441
+ const trimmed = line.trim();
1442
+ if (trimmed === "") {
1443
+ flush();
1444
+ i += 1;
1445
+ continue;
1446
+ }
1447
+ const fence = FENCE_RE.exec(trimmed);
1448
+ if (fence) {
1449
+ flush();
1450
+ const marker = fence[1];
1451
+ const body = [];
1452
+ i += 1;
1453
+ while (i < lines.length && !lines[i].trim().startsWith(marker)) {
1454
+ body.push(lines[i]);
1455
+ i += 1;
1456
+ }
1457
+ i += 1;
1458
+ blocks.push({ type: "code", lang: fence[2] ?? "", value: body.join("\n") });
1459
+ continue;
1460
+ }
1461
+ const heading = HEADING_RE.exec(trimmed);
1462
+ if (heading) {
1463
+ flush();
1464
+ blocks.push({ type: "heading", level: heading[1].length, children: parseInline(heading[2]) });
1465
+ i += 1;
1466
+ continue;
1467
+ }
1468
+ if (RULE_RE.test(trimmed)) {
1469
+ flush();
1470
+ blocks.push({ type: "rule" });
1471
+ i += 1;
1472
+ continue;
1473
+ }
1474
+ if (trimmed.startsWith(">")) {
1475
+ flush();
1476
+ const quote = [];
1477
+ while (i < lines.length && lines[i].trim().startsWith(">")) {
1478
+ quote.push(lines[i].trim().replace(/^>\s?/, ""));
1479
+ i += 1;
1480
+ }
1481
+ blocks.push({ type: "quote", children: parseInline(quote.join(" ")) });
1482
+ continue;
1483
+ }
1484
+ if (trimmed.includes("|") && i + 1 < lines.length && DELIMITER_ROW_RE.test(lines[i + 1]) && lines[i + 1].includes("-")) {
1485
+ const head = splitTableRow(line);
1486
+ const delimiters = splitTableRow(lines[i + 1]);
1487
+ if (delimiters.length === head.length) {
1488
+ flush();
1489
+ const rows = [];
1490
+ i += 2;
1491
+ while (i < lines.length && lines[i].trim() !== "" && lines[i].includes("|")) {
1492
+ const cells = splitTableRow(lines[i]);
1493
+ rows.push(head.map((_, c) => parseInline(cells[c] ?? "")));
1494
+ i += 1;
1495
+ }
1496
+ blocks.push({ type: "table", align: delimiters.map(alignOf), head: head.map((c) => parseInline(c)), rows });
1497
+ continue;
1498
+ }
1499
+ }
1500
+ const first = matchListLine(line);
1501
+ if (first) {
1502
+ flush();
1503
+ const listLines = [];
1504
+ while (i < lines.length) {
1505
+ const current = lines[i];
1506
+ const item = matchListLine(current);
1507
+ if (item) {
1508
+ if (listLines.length && item.indent <= listLines[0].indent && item.ordered !== listLines[0].ordered) break;
1509
+ listLines.push(item);
1510
+ } else if (current.trim() !== "" && indentOf(current) > listLines[0].indent && listLines.length) {
1511
+ listLines[listLines.length - 1].text += `
1512
+ ${current.trim()}`;
1513
+ } else {
1514
+ break;
1515
+ }
1516
+ i += 1;
1517
+ }
1518
+ const base = Math.min(...listLines.map((l) => l.indent));
1519
+ blocks.push(...buildList(listLines.map((l) => ({ ...l, indent: l.indent - base }))));
1520
+ continue;
1521
+ }
1522
+ paragraph.push(trimmed);
1523
+ i += 1;
1524
+ }
1525
+ flush();
1526
+ return blocks;
1527
+ }
1528
+
1529
+ // src/components/Markdown.tsx
1530
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1531
+ var defaultMarkdownIsSafeUrl = defaultIsSafeUrl;
1532
+ var isExternal = (href) => /^https?:/i.test(href.trim());
1533
+ function renderInlines(nodes, policy) {
1534
+ return nodes.map((node, i) => {
1535
+ switch (node.type) {
1536
+ case "text":
1537
+ return node.value;
1538
+ case "br":
1539
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("br", {}, i);
1540
+ case "code":
1541
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { children: node.value }, i);
1542
+ case "strong":
1543
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: renderInlines(node.children, policy) }, i);
1544
+ case "em":
1545
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("em", { children: renderInlines(node.children, policy) }, i);
1546
+ case "del":
1547
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("del", { children: renderInlines(node.children, policy) }, i);
1548
+ case "link": {
1549
+ const children = renderInlines(node.children, policy);
1550
+ if (!node.href.trim() || !policy.isSafeUrl(node.href)) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children }, i);
1551
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1552
+ "a",
1553
+ {
1554
+ href: node.href.trim(),
1555
+ title: node.title,
1556
+ ...isExternal(node.href) ? { target: "_blank", rel: "noopener noreferrer" } : {},
1557
+ children
1558
+ },
1559
+ i
1560
+ );
1561
+ }
1562
+ case "image": {
1563
+ const resolved = policy.resolveImageSrc?.(node.src) ?? null;
1564
+ if (resolved) {
1565
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: resolved, alt: node.alt, title: node.title, loading: "lazy", decoding: "async" }, i);
1566
+ }
1567
+ const label = node.alt || node.src;
1568
+ if (!node.src.trim() || !policy.isSafeUrl(node.src)) return label;
1569
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: node.src.trim(), ...isExternal(node.src) ? { target: "_blank", rel: "noopener noreferrer" } : {}, children: label }, i);
1570
+ }
1571
+ }
1572
+ });
1573
+ }
1574
+ function renderList(list, policy, key) {
1575
+ const items = list.items.map((item, i) => {
1576
+ const content = renderInlines(item.children, policy);
1577
+ const nested = item.lists.map((sub, n) => renderList(sub, policy, n));
1578
+ if (item.checked === null) {
1579
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { children: [
1580
+ content,
1581
+ nested
1582
+ ] }, i);
1583
+ }
1584
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "uix-markdown__task", "data-checked": item.checked || void 0, children: [
1585
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "checkbox", defaultChecked: item.checked, disabled: true, "aria-label": plainText(item.children) }),
1586
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: content }),
1587
+ nested
1588
+ ] }, i);
1589
+ });
1590
+ const hasTasks = list.items.some((item) => item.checked !== null);
1591
+ const className = hasTasks ? "uix-markdown__tasks" : void 0;
1592
+ return list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className, start: list.start !== 1 ? list.start : void 0, children: items }, key) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className, children: items }, key);
1593
+ }
1594
+ function renderBlock(block, policy, headingOffset, key) {
1595
+ switch (block.type) {
1596
+ case "paragraph":
1597
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: renderInlines(block.children, policy) }, key);
1598
+ case "heading": {
1599
+ const Tag = `h${Math.min(Math.max(block.level + headingOffset, 1), 6)}`;
1600
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Tag, { children: renderInlines(block.children, policy) }, key);
1601
+ }
1602
+ case "code":
1603
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { "data-language": block.lang || void 0, children: block.value }) }, key);
1604
+ case "quote":
1605
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("blockquote", { children: renderInlines(block.children, policy) }, key);
1606
+ case "rule":
1607
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("hr", {}, key);
1608
+ case "list":
1609
+ return renderList(block, policy, key);
1610
+ case "table":
1611
+ return (
1612
+ // A scrollable region must be reachable from the keyboard.
1613
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "uix-markdown__table", tabIndex: 0, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("table", { children: [
1614
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tr", { children: block.head.map((cell, c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { scope: "col", style: block.align[c] ? { textAlign: block.align[c] } : void 0, children: renderInlines(cell, policy) }, c)) }) }),
1615
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tbody", { children: block.rows.map((row, r) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tr", { children: row.map((cell, c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: block.align[c] ? { textAlign: block.align[c] } : void 0, children: renderInlines(cell, policy) }, c)) }, r)) })
1616
+ ] }) }, key)
1617
+ );
1618
+ }
1619
+ }
1620
+ function Markdown({ children, className, isSafeUrl = defaultMarkdownIsSafeUrl, resolveImageSrc, headingOffset = 2 }) {
1621
+ const source = typeof children === "string" ? children : "";
1622
+ if (source.trim() === "") return null;
1623
+ const policy = { isSafeUrl, resolveImageSrc };
1624
+ const blocks = parseMarkdown(source);
1625
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: cx("uix-prose", "uix-markdown", className), "data-component": "markdown", children: blocks.map((block, i) => renderBlock(block, policy, headingOffset, i)) });
1626
+ }
1627
+
1628
+ // src/components/Textarea.tsx
1629
+ var import_react6 = require("react");
1630
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1631
+ var Textarea = (0, import_react6.forwardRef)(
1632
+ ({ invalid, className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1633
+ "textarea",
1634
+ {
1635
+ ref,
1636
+ className: cx("uix-textarea", className),
1637
+ "aria-invalid": invalid || void 0,
1638
+ ...props
1639
+ }
1640
+ )
1641
+ );
1642
+ Textarea.displayName = "Textarea";
1643
+
1644
+ // src/components/RichTextEditor.tsx
1645
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1646
+ var DEFAULT_RICH_TEXT_LABELS = {
1647
+ toolbar: "Formatting",
1648
+ heading1: "Heading 1",
1649
+ heading2: "Heading 2",
1650
+ heading3: "Heading 3",
1651
+ bold: "Bold",
1652
+ italic: "Italic",
1653
+ strike: "Strikethrough",
1654
+ code: "Inline code",
1655
+ bulletList: "Bulleted list",
1656
+ orderedList: "Numbered list",
1657
+ taskList: "Checklist",
1658
+ blockquote: "Quote",
1659
+ codeBlock: "Code block",
1660
+ horizontalRule: "Divider",
1661
+ link: "Link",
1662
+ linkUrl: "Link address",
1663
+ linkApply: "Apply",
1664
+ linkRemove: "Remove link",
1665
+ linkInvalid: "Use a web, mail or in-app address.",
1666
+ table: "Table",
1667
+ tableAddRow: "Add row",
1668
+ tableAddColumn: "Add column",
1669
+ tableDeleteRow: "Delete row",
1670
+ tableDeleteColumn: "Delete column",
1671
+ tableDelete: "Delete table",
1672
+ image: "Image",
1673
+ imageUploading: "Uploading image\u2026",
1674
+ imageFailed: "The image could not be added.",
1675
+ emoji: "Emoji",
1676
+ emojiSuggestions: "Emoji suggestions",
1677
+ undo: "Undo",
1678
+ redo: "Redo",
1679
+ sourceMode: "Show markdown",
1680
+ richMode: "Show formatted",
1681
+ characterCount: "{count} of {max} characters"
1682
+ };
1683
+ var COMMON = ["bold", "italic", "strike", "code", "bulletList", "orderedList", "taskList", "blockquote", "codeBlock", "link", "emoji", "history", "source"];
1684
+ var PRESETS = {
1685
+ full: /* @__PURE__ */ new Set([...COMMON, "heading", "horizontalRule", "table", "image"]),
1686
+ comment: new Set(COMMON),
1687
+ template: /* @__PURE__ */ new Set([...COMMON, "heading", "horizontalRule", "table"])
1688
+ };
1689
+ var emojiSuggestionKey = new import_state.PluginKey("uixEmojiSuggestion");
1690
+ var SHORTCODE_RE = /:([a-zA-Z0-9_+-]+):$/;
1691
+ var emojiItems = OFFLINE_EMOJI_ITEMS;
1692
+ var findShortcode = (name) => emojiItems.find((item) => item.emoji && (item.name === name || item.shortcodes.includes(name)));
1693
+ function suggestEmoji(query, limit = 8) {
1694
+ const q = query.toLowerCase();
1695
+ if (q.length < 2) return [];
1696
+ const starts = [];
1697
+ const tagged = [];
1698
+ for (const item of emojiItems) {
1699
+ if (!item.emoji) continue;
1700
+ if (item.shortcodes.some((s) => s.startsWith(q))) starts.push(item);
1701
+ else if (item.tags.some((t) => t.startsWith(q))) tagged.push(item);
1702
+ if (starts.length >= limit) break;
1703
+ }
1704
+ return [...starts, ...tagged].slice(0, limit);
1705
+ }
1706
+ var htmlAttr = (value) => value === void 0 || value === false || value === "" ? void 0 : String(value);
1707
+ var popoverOpen = (el) => {
1708
+ try {
1709
+ return el.matches(":popover-open");
1710
+ } catch {
1711
+ return false;
1712
+ }
1713
+ };
1714
+ var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent);
1715
+ function RichTextEditorFallback(props) {
1716
+ const {
1717
+ value,
1718
+ onChange,
1719
+ onSubmitShortcut,
1720
+ placeholder,
1721
+ disabled,
1722
+ readOnly,
1723
+ maxLength,
1724
+ id,
1725
+ name,
1726
+ variant = "field",
1727
+ toolbarEnd,
1728
+ minRows,
1729
+ className,
1730
+ onBlur,
1731
+ labels: labelOverrides
1732
+ } = props;
1733
+ const labels = { ...DEFAULT_RICH_TEXT_LABELS, ...labelOverrides };
1734
+ const counterId = (0, import_react7.useId)();
1735
+ const over = maxLength !== void 0 && value.length > maxLength;
1736
+ const counter = maxLength !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { id: counterId, className: "uix-rich-text__counter", "data-over": over || void 0, children: formatLabel(labels.characterCount, { count: value.length, max: maxLength }) }) : null;
1737
+ const textarea = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1738
+ Textarea,
1739
+ {
1740
+ id,
1741
+ name,
1742
+ value,
1743
+ onChange: (event) => onChange(event.target.value),
1744
+ onBlur,
1745
+ onKeyDown: (event) => {
1746
+ if (onSubmitShortcut && (event.metaKey || event.ctrlKey) && event.key === "Enter") {
1747
+ event.preventDefault();
1748
+ onSubmitShortcut();
1749
+ }
1750
+ },
1751
+ placeholder,
1752
+ disabled,
1753
+ readOnly,
1754
+ rows: minRows ?? (variant === "composer" ? 3 : 4),
1755
+ className: "uix-rich-text__source",
1756
+ "aria-label": props["aria-label"],
1757
+ "aria-labelledby": props["aria-labelledby"],
1758
+ "aria-describedby": [props["aria-describedby"], counter ? counterId : void 0].filter(Boolean).join(" ") || void 0,
1759
+ "aria-invalid": props["aria-invalid"] || over || void 0,
1760
+ "aria-required": props["aria-required"] || void 0,
1761
+ "data-editor-fallback": ""
1762
+ }
1763
+ );
1764
+ if (variant === "composer") {
1765
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Composer, { className: cx("uix-rich-text", "uix-rich-text--composer", className), children: [
1766
+ textarea,
1767
+ counter || toolbarEnd ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(ComposerBar, { className: "uix-rich-text__bar", children: [
1768
+ counter,
1769
+ toolbarEnd
1770
+ ] }) : null
1771
+ ] });
1772
+ }
1773
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: cx("uix-rich-text", "uix-rich-text--fallback", className), "data-disabled": disabled || void 0, children: [
1774
+ textarea,
1775
+ counter ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-rich-text__footer", children: counter }) : null
1776
+ ] });
1777
+ }
1778
+ function RichTextEditor(props) {
1779
+ const {
1780
+ value,
1781
+ onChange,
1782
+ features = "full",
1783
+ headingLevels = [1, 2, 3],
1784
+ onUploadImage,
1785
+ isSafeUrl = defaultIsSafeUrl,
1786
+ resolveImageSrc,
1787
+ onSubmitShortcut,
1788
+ emoji = true,
1789
+ maxLength,
1790
+ placeholder,
1791
+ disabled = false,
1792
+ readOnly = false,
1793
+ labels: labelOverrides,
1794
+ emojiPickerLabels,
1795
+ emojiLocale,
1796
+ id,
1797
+ name,
1798
+ variant = "field",
1799
+ toolbarEnd,
1800
+ minRows,
1801
+ className,
1802
+ onBlur,
1803
+ emojiImageBaseUrl
1804
+ } = props;
1805
+ const emojiBase = (0, import_react7.useMemo)(() => normalizeEmojiImageBaseUrl(emojiImageBaseUrl), [emojiImageBaseUrl]);
1806
+ const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
1807
+ const labels = (0, import_react7.useMemo)(() => ({ ...DEFAULT_RICH_TEXT_LABELS, ...labelOverrides }), [labelOverrides]);
1808
+ const uid = (0, import_react7.useId)();
1809
+ const counterId = `${uid}-counter`;
1810
+ const statusId = `${uid}-status`;
1811
+ const suggestId = `${uid}-emoji`;
1812
+ const linkPopoverId = `${uid}-link`;
1813
+ const controls = PRESETS[features];
1814
+ const has = (control) => controls.has(control) && (control !== "emoji" || emoji) && (control !== "image" || !!onUploadImage);
1815
+ const latest = (0, import_react7.useRef)({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false });
1816
+ latest.current = {
1817
+ onChange,
1818
+ onSubmitShortcut,
1819
+ onBlur,
1820
+ isSafeUrl,
1821
+ resolveImageSrc,
1822
+ onUploadImage,
1823
+ placeholder,
1824
+ imagesOn: controls.has("image") && !!onUploadImage
1825
+ };
1826
+ const lastValue = (0, import_react7.useRef)(value);
1827
+ const origin = (0, import_react7.useRef)(null);
1828
+ const [markdown, setMarkdown] = (0, import_react7.useState)(value);
1829
+ const [mode, setMode] = (0, import_react7.useState)("rich");
1830
+ const [status, setStatus] = (0, import_react7.useState)(null);
1831
+ const [suggest, setSuggest] = (0, import_react7.useState)(null);
1832
+ const [suggestIndex, setSuggestIndex] = (0, import_react7.useState)(0);
1833
+ const [linkUrl, setLinkUrl] = (0, import_react7.useState)("");
1834
+ const [linkError, setLinkError] = (0, import_react7.useState)(false);
1835
+ const [rovingIndex, setRovingIndex] = (0, import_react7.useState)(0);
1836
+ const toolbarRef = (0, import_react7.useRef)(null);
1837
+ const linkButtonRef = (0, import_react7.useRef)(null);
1838
+ const linkInputRef = (0, import_react7.useRef)(null);
1839
+ const fileRef = (0, import_react7.useRef)(null);
1840
+ const sourceRef = (0, import_react7.useRef)(null);
1841
+ const openLinkRef = (0, import_react7.useRef)(() => {
1842
+ });
1843
+ const suggestState = (0, import_react7.useRef)({ suggest, suggestIndex });
1844
+ suggestState.current = { suggest, suggestIndex };
1845
+ const bridge = (0, import_react7.useRef)({ update: () => {
1846
+ }, keyDown: () => false });
1847
+ bridge.current = {
1848
+ update: (p) => {
1849
+ if (!p || p.items.length === 0) {
1850
+ setSuggest(null);
1851
+ return;
1852
+ }
1853
+ setSuggest({ query: p.query, items: p.items, command: p.command, rect: p.clientRect?.() ?? null });
1854
+ setSuggestIndex(0);
1855
+ },
1856
+ keyDown: ({ event }) => {
1857
+ const { suggest: open, suggestIndex: index } = suggestState.current;
1858
+ if (!open) return false;
1859
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1860
+ const delta = event.key === "ArrowDown" ? 1 : -1;
1861
+ setSuggestIndex((index + delta + open.items.length) % open.items.length);
1862
+ return true;
1863
+ }
1864
+ if (event.key === "Enter" || event.key === "Tab") {
1865
+ const item = open.items[index];
1866
+ if (item) open.command(item);
1867
+ return true;
1868
+ }
1869
+ if (event.key === "Escape") {
1870
+ setSuggest(null);
1871
+ return true;
1872
+ }
1873
+ return false;
1874
+ }
1875
+ };
1876
+ const headingKey = [...headingLevels].sort().join(",");
1877
+ const extensions = (0, import_react7.useMemo)(() => {
1878
+ const image = import_extension_image2.Image.extend({
1879
+ parseHTML() {
1880
+ return [{
1881
+ tag: "img[src]",
1882
+ // Pasted images are kept only when the consumer allows images and can resolve them.
1883
+ getAttrs: (el) => {
1884
+ const src = el.getAttribute("src") ?? "";
1885
+ return latest.current.imagesOn && latest.current.resolveImageSrc?.(src) ? null : false;
1886
+ }
1887
+ }];
1888
+ },
1889
+ renderHTML({ node, HTMLAttributes }) {
1890
+ const src = String(node.attrs.src ?? "");
1891
+ const resolved = latest.current.resolveImageSrc?.(src) ?? null;
1892
+ if (resolved) return ["img", (0, import_core2.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes, { src: resolved })];
1893
+ return ["span", { class: "uix-rich-text__image-link", "data-src": src, title: src }, node.attrs.alt || src];
1894
+ }
1895
+ }).configure({ inline: true, allowBase64: false });
1896
+ const unicodeEmoji = import_extension_emoji2.Emoji.extend({
1897
+ addStorage() {
1898
+ return { emojis: this.options.emojis, isSupported: () => true };
1899
+ },
1900
+ // Emoji are inserted as text; the node type exists only because the extension defines it.
1901
+ // Unknown names (e.g. pasted from another editor) keep their shortcode rather than vanish.
1902
+ renderMarkdown: (node) => {
1903
+ const name2 = String(node.attrs?.name ?? "");
1904
+ return findShortcode(name2)?.emoji ?? (name2 ? `:${name2}:` : "");
1905
+ },
1906
+ addInputRules() {
1907
+ return [new import_core2.InputRule({
1908
+ find: SHORTCODE_RE,
1909
+ handler: ({ range, match, chain }) => {
1910
+ const item = findShortcode(match[1] ?? "");
1911
+ if (item?.emoji) chain().insertContentAt(range, item.emoji).run();
1912
+ }
1913
+ })];
1914
+ },
1915
+ addPasteRules() {
1916
+ return [];
1917
+ },
1918
+ addProseMirrorPlugins() {
1919
+ return [(0, import_suggestion.Suggestion)({ editor: this.editor, ...this.options.suggestion })];
1920
+ }
1921
+ }).configure({
1922
+ // Offline list: no CDN fallbackImage, no GitHub image-only emoji (on-prem, CSP img-src 'self').
1923
+ emojis: [...emojiItems],
1924
+ enableEmoticons: false,
1925
+ forceFallbackImages: false,
1926
+ suggestion: {
1927
+ char: ":",
1928
+ pluginKey: emojiSuggestionKey,
1929
+ items: ({ query }) => suggestEmoji(query),
1930
+ command: ({ editor: editor2, range, props: item }) => {
1931
+ if (item.emoji) editor2.chain().focus().insertContentAt(range, item.emoji).run();
1932
+ },
1933
+ allow: ({ state: state2, range }) => !state2.doc.resolve(range.from).parent.type.spec.code,
1934
+ render: () => ({
1935
+ onStart: (p) => bridge.current.update(p),
1936
+ onUpdate: (p) => bridge.current.update(p),
1937
+ onExit: () => bridge.current.update(null),
1938
+ onKeyDown: (p) => bridge.current.keyDown(p)
1939
+ })
1940
+ }
1941
+ });
1942
+ const shortcuts = import_core2.Extension.create({
1943
+ name: "uixRichTextShortcuts",
1944
+ addKeyboardShortcuts() {
1945
+ return {
1946
+ "Mod-Enter": () => {
1947
+ const submit = latest.current.onSubmitShortcut;
1948
+ if (!submit) return false;
1949
+ submit();
1950
+ return true;
1951
+ },
1952
+ "Mod-k": () => {
1953
+ openLinkRef.current();
1954
+ return true;
1955
+ },
1956
+ "Mod-Shift-x": () => this.editor.commands.toggleStrike()
1957
+ };
1958
+ }
1959
+ });
1960
+ const emojiImages = import_core2.Extension.create({
1961
+ name: "uixEmojiImages",
1962
+ addProseMirrorPlugins() {
1963
+ const key = new import_state.PluginKey("uixEmojiImages");
1964
+ const build = (doc) => {
1965
+ if (!emojiBase) return import_view.DecorationSet.empty;
1966
+ const decorations = [];
1967
+ doc.descendants((node, pos) => {
1968
+ if (node.type.spec.code) return false;
1969
+ if (!node.isText || !node.text) return true;
1970
+ for (const match of node.text.matchAll(emojiPattern())) {
1971
+ const glyph = match[0];
1972
+ if (canRenderEmoji(glyph)) continue;
1973
+ const from = pos + (match.index ?? 0);
1974
+ decorations.push(import_view.Decoration.inline(from, from + glyph.length, { class: "uix-rich-text__emoji-text" }));
1975
+ decorations.push(import_view.Decoration.widget(from, () => {
1976
+ const img = document.createElement("img");
1977
+ img.className = "uix-emoji-img";
1978
+ img.alt = glyph;
1979
+ img.draggable = false;
1980
+ img.src = emojiImageSrc(emojiBase, glyph);
1981
+ return img;
1982
+ }, { side: -1, key: `emoji-${glyph}`, ignoreSelection: true }));
1983
+ }
1984
+ return false;
1985
+ });
1986
+ return import_view.DecorationSet.create(doc, decorations);
1987
+ };
1988
+ return [new import_state.Plugin({
1989
+ key,
1990
+ state: {
1991
+ init: (_, state2) => build(state2.doc),
1992
+ apply: (tr, previous) => tr.docChanged ? build(tr.doc) : previous
1993
+ },
1994
+ props: { decorations: (state2) => key.getState(state2) }
1995
+ })];
1996
+ }
1997
+ });
1998
+ return [
1999
+ ...createSchemaExtensions({
2000
+ headingLevels: headingKey.split(",").map(Number),
2001
+ isSafeUrl: (url) => latest.current.isSafeUrl(url),
2002
+ image
2003
+ }),
2004
+ import_markdown2.Markdown.configure({ marked: createMarked() }),
2005
+ import_extensions.Placeholder.configure({ placeholder: () => latest.current.placeholder ?? "" }),
2006
+ shortcuts,
2007
+ ...emoji ? [unicodeEmoji] : [],
2008
+ ...emojiBase ? [emojiImages] : []
2009
+ ];
2010
+ }, [headingKey, emoji, emojiBase]);
2011
+ const managerOf = (ed) => {
2012
+ const manager = ed.markdown ?? ed.storage.markdown.manager;
2013
+ return patchManager(manager);
2014
+ };
2015
+ const load = (0, import_react7.useCallback)((ed, source) => {
2016
+ const read = readMarkdown(source, managerOf(ed), ed.schema);
2017
+ origin.current = read;
2018
+ ed.chain().setMeta("addToHistory", false).setContent(read.doc, { emitUpdate: false }).run();
2019
+ }, []);
2020
+ const emit = (next) => {
2021
+ if (next === lastValue.current) return;
2022
+ lastValue.current = next;
2023
+ setMarkdown(next);
2024
+ latest.current.onChange(next);
2025
+ };
2026
+ const uploadImage = async (ed, file) => {
2027
+ const upload = latest.current.onUploadImage;
2028
+ if (!upload || !file.type.startsWith("image/")) return;
2029
+ setStatus({ kind: "busy", text: labels.imageUploading });
2030
+ try {
2031
+ const { src, alt } = await upload(file);
2032
+ ed.chain().focus().setImage({ src, alt }).run();
2033
+ setStatus(null);
2034
+ } catch (error) {
2035
+ const message = error instanceof Error && error.message ? error.message : "";
2036
+ setStatus({ kind: "error", text: message ? `${labels.imageFailed} ${message}` : labels.imageFailed });
2037
+ }
2038
+ };
2039
+ const editor = (0, import_react8.useEditor)({
2040
+ extensions,
2041
+ immediatelyRender: false,
2042
+ // Styles ship in @tensor_1/tokens (rich-text.css); no runtime <style> injection under CSP.
2043
+ injectCSS: false,
2044
+ shouldRerenderOnTransaction: false,
2045
+ editable: !disabled && !readOnly,
2046
+ editorProps: {
2047
+ handlePaste: (_view, event) => {
2048
+ const files = Array.from(event.clipboardData?.files ?? []).filter((f) => f.type.startsWith("image/"));
2049
+ if (!files.length || !latest.current.imagesOn || !editorRef.current) return false;
2050
+ files.forEach((file) => void uploadImage(editorRef.current, file));
2051
+ return true;
2052
+ },
2053
+ handleDrop: (_view, event) => {
2054
+ const files = Array.from(event.dataTransfer?.files ?? []).filter((f) => f.type.startsWith("image/"));
2055
+ if (!files.length || !latest.current.imagesOn || !editorRef.current) return false;
2056
+ event.preventDefault();
2057
+ files.forEach((file) => void uploadImage(editorRef.current, file));
2058
+ return true;
2059
+ }
2060
+ },
2061
+ onCreate: ({ editor: ed }) => load(ed, lastValue.current),
2062
+ onUpdate: ({ editor: ed }) => {
2063
+ if (!origin.current) return;
2064
+ emit(writeMarkdown(ed.getJSON(), origin.current, managerOf(ed)));
2065
+ },
2066
+ onBlur: () => latest.current.onBlur?.()
2067
+ }, [extensions]);
2068
+ const editorRef = (0, import_react7.useRef)(null);
2069
+ editorRef.current = editor;
2070
+ (0, import_react7.useEffect)(() => {
2071
+ if (value === lastValue.current) return;
2072
+ lastValue.current = value;
2073
+ setMarkdown(value);
2074
+ if (editor && !editor.isDestroyed) load(editor, value);
2075
+ }, [value, editor, load]);
2076
+ (0, import_react7.useEffect)(() => {
2077
+ if (editor && !editor.isDestroyed) editor.setEditable(!disabled && !readOnly, false);
2078
+ }, [editor, disabled, readOnly]);
2079
+ const over = maxLength !== void 0 && markdown.length > maxLength;
2080
+ const invalid = props["aria-invalid"] || over;
2081
+ const describedBy = [props["aria-describedby"], maxLength !== void 0 ? counterId : void 0, status ? statusId : void 0].filter(Boolean).join(" ") || void 0;
2082
+ const suggestOpen = !!suggest && mode === "rich";
2083
+ const rows = minRows ?? (variant === "composer" ? 3 : 4);
2084
+ (0, import_react7.useEffect)(() => {
2085
+ if (!editor || editor.isDestroyed) return;
2086
+ editor.setOptions({
2087
+ editorProps: {
2088
+ ...editor.options.editorProps,
2089
+ attributes: Object.fromEntries(Object.entries({
2090
+ id: mode === "rich" ? id : void 0,
2091
+ role: "textbox",
2092
+ "aria-multiline": "true",
2093
+ "aria-label": props["aria-label"],
2094
+ "aria-labelledby": props["aria-labelledby"],
2095
+ "aria-describedby": describedBy,
2096
+ "aria-invalid": htmlAttr(invalid),
2097
+ "aria-required": htmlAttr(props["aria-required"]),
2098
+ "aria-disabled": htmlAttr(disabled),
2099
+ "aria-readonly": htmlAttr(readOnly),
2100
+ "aria-autocomplete": emoji ? "list" : void 0,
2101
+ "aria-controls": suggestOpen ? suggestId : void 0,
2102
+ "aria-activedescendant": suggestOpen ? `${suggestId}-${suggestIndex}` : void 0,
2103
+ class: "uix-rich-text__content uix-prose",
2104
+ style: `min-height: calc(${rows} * 1lh + 2 * var(--uix-space-3))`
2105
+ }).filter(([, v]) => v !== void 0))
2106
+ }
2107
+ });
2108
+ }, [
2109
+ editor,
2110
+ mode,
2111
+ id,
2112
+ props["aria-label"],
2113
+ props["aria-labelledby"],
2114
+ describedBy,
2115
+ invalid,
2116
+ props["aria-required"],
2117
+ disabled,
2118
+ readOnly,
2119
+ emoji,
2120
+ suggestOpen,
2121
+ suggestId,
2122
+ suggestIndex,
2123
+ rows
2124
+ ]);
2125
+ const state = (0, import_react8.useEditorState)({
2126
+ editor,
2127
+ selector: ({ editor: ed }) => {
2128
+ if (!ed) return null;
2129
+ const can = ed.can();
2130
+ return {
2131
+ bold: ed.isActive("bold"),
2132
+ italic: ed.isActive("italic"),
2133
+ strike: ed.isActive("strike"),
2134
+ code: ed.isActive("code"),
2135
+ h1: ed.isActive("heading", { level: 1 }),
2136
+ h2: ed.isActive("heading", { level: 2 }),
2137
+ h3: ed.isActive("heading", { level: 3 }),
2138
+ bulletList: ed.isActive("bulletList"),
2139
+ orderedList: ed.isActive("orderedList"),
2140
+ taskList: ed.isActive("taskList"),
2141
+ blockquote: ed.isActive("blockquote"),
2142
+ codeBlock: ed.isActive("codeBlock"),
2143
+ link: ed.isActive("link"),
2144
+ inTable: ed.isActive("table"),
2145
+ canUndo: can.undo(),
2146
+ canRedo: can.redo()
2147
+ };
2148
+ }
2149
+ });
2150
+ const openLink = () => {
2151
+ if (!editor || mode !== "rich") return;
2152
+ setLinkUrl(String(editor.getAttributes("link").href ?? ""));
2153
+ setLinkError(false);
2154
+ const pop = document.getElementById(linkPopoverId);
2155
+ if (pop && !popoverOpen(pop)) pop.showPopover();
2156
+ requestAnimationFrame(() => linkInputRef.current?.select());
2157
+ };
2158
+ openLinkRef.current = openLink;
2159
+ const closeLink = () => {
2160
+ const pop = document.getElementById(linkPopoverId);
2161
+ if (pop && popoverOpen(pop)) pop.hidePopover();
2162
+ editor?.commands.focus();
2163
+ };
2164
+ const applyLink = () => {
2165
+ if (!editor) return;
2166
+ const url = linkUrl.trim();
2167
+ if (!url) {
2168
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
2169
+ closeLink();
2170
+ return;
2171
+ }
2172
+ if (!isSafeUrl(url)) {
2173
+ setLinkError(true);
2174
+ linkInputRef.current?.focus();
2175
+ return;
2176
+ }
2177
+ const chain = editor.chain().focus().extendMarkRange("link");
2178
+ if (editor.state.selection.empty && !editor.isActive("link")) {
2179
+ chain.insertContent({ type: "text", text: url, marks: [{ type: "link", attrs: { href: url } }] }).run();
2180
+ } else {
2181
+ chain.setLink({ href: url }).run();
2182
+ }
2183
+ closeLink();
2184
+ };
2185
+ const toggleMode = () => {
2186
+ if (!editor) return;
2187
+ if (mode === "rich") {
2188
+ setMode("source");
2189
+ requestAnimationFrame(() => sourceRef.current?.focus());
2190
+ } else {
2191
+ load(editor, lastValue.current);
2192
+ setMode("rich");
2193
+ requestAnimationFrame(() => editor.commands.focus());
2194
+ }
2195
+ };
2196
+ const insertEmoji = (glyph) => {
2197
+ if (mode === "source") {
2198
+ const box = sourceRef.current;
2199
+ if (!box) return;
2200
+ const next = `${box.value.slice(0, box.selectionStart)}${glyph}${box.value.slice(box.selectionEnd)}`;
2201
+ const caret = box.selectionStart + glyph.length;
2202
+ emit(next);
2203
+ requestAnimationFrame(() => {
2204
+ box.focus();
2205
+ box.setSelectionRange(caret, caret);
2206
+ });
2207
+ return;
2208
+ }
2209
+ editor?.chain().focus().insertContent(glyph).run();
2210
+ };
2211
+ const run = (fn) => () => {
2212
+ if (editor) fn(editor);
2213
+ };
2214
+ const locked = disabled || !editor;
2215
+ const richOnly = locked || mode === "source";
2216
+ const shortcut = (key) => `${isMac() ? "\u2318" : "Ctrl+"}${key}`;
2217
+ const groups = [];
2218
+ if (has("heading")) {
2219
+ groups.push(headingLevels.map((level) => ({
2220
+ key: `h${level}`,
2221
+ icon: `heading${level}`,
2222
+ label: labels[`heading${level}`],
2223
+ pressed: !!state?.[`h${level}`],
2224
+ disabled: richOnly,
2225
+ onClick: run((ed) => ed.chain().focus().toggleHeading({ level }).run())
2226
+ })));
2227
+ }
2228
+ const marks = [];
2229
+ if (has("bold")) marks.push({ key: "bold", icon: "bold", label: labels.bold, hint: shortcut("B"), pressed: state?.bold, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleBold().run()) });
2230
+ if (has("italic")) marks.push({ key: "italic", icon: "italic", label: labels.italic, hint: shortcut("I"), pressed: state?.italic, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleItalic().run()) });
2231
+ if (has("strike")) marks.push({ key: "strike", icon: "strike", label: labels.strike, hint: shortcut("Shift+X"), pressed: state?.strike, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleStrike().run()) });
2232
+ if (has("code")) marks.push({ key: "code", icon: "code", label: labels.code, hint: shortcut("E"), pressed: state?.code, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleCode().run()) });
2233
+ if (has("link")) marks.push({ key: "link", icon: "link", label: labels.link, hint: shortcut("K"), pressed: state?.link, disabled: richOnly, onClick: openLink });
2234
+ if (marks.length) groups.push(marks);
2235
+ const blocks = [];
2236
+ if (has("bulletList")) blocks.push({ key: "bulletList", icon: "bulletList", label: labels.bulletList, pressed: state?.bulletList, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleBulletList().run()) });
2237
+ if (has("orderedList")) blocks.push({ key: "orderedList", icon: "orderedList", label: labels.orderedList, pressed: state?.orderedList, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleOrderedList().run()) });
2238
+ if (has("taskList")) blocks.push({ key: "taskList", icon: "taskList", label: labels.taskList, pressed: state?.taskList, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleTaskList().run()) });
2239
+ if (has("blockquote")) blocks.push({ key: "blockquote", icon: "blockquote", label: labels.blockquote, pressed: state?.blockquote, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleBlockquote().run()) });
2240
+ if (has("codeBlock")) blocks.push({ key: "codeBlock", icon: "codeBlock", label: labels.codeBlock, pressed: state?.codeBlock, disabled: richOnly, onClick: run((ed) => ed.chain().focus().toggleCodeBlock().run()) });
2241
+ if (has("horizontalRule")) blocks.push({ key: "horizontalRule", icon: "horizontalRule", label: labels.horizontalRule, disabled: richOnly, onClick: run((ed) => ed.chain().focus().setHorizontalRule().run()) });
2242
+ if (blocks.length) groups.push(blocks);
2243
+ const inserts = [];
2244
+ if (has("table")) inserts.push({ key: "table", icon: "table", label: labels.table, pressed: state?.inTable, disabled: richOnly || state?.inTable, onClick: run((ed) => ed.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()) });
2245
+ if (has("image")) inserts.push({ key: "image", icon: "image", label: labels.image, disabled: richOnly || status?.kind === "busy", onClick: () => fileRef.current?.click() });
2246
+ if (inserts.length) groups.push(inserts);
2247
+ if (has("table") && state?.inTable && mode === "rich") {
2248
+ groups.push([
2249
+ { key: "tableAddRow", icon: "tableAddRow", label: labels.tableAddRow, disabled: locked, onClick: run((ed) => ed.chain().focus().addRowAfter().run()) },
2250
+ { key: "tableAddColumn", icon: "tableAddColumn", label: labels.tableAddColumn, disabled: locked, onClick: run((ed) => ed.chain().focus().addColumnAfter().run()) },
2251
+ { key: "tableDeleteRow", icon: "tableDeleteRow", label: labels.tableDeleteRow, disabled: locked, onClick: run((ed) => ed.chain().focus().deleteRow().run()) },
2252
+ { key: "tableDeleteColumn", icon: "tableDeleteColumn", label: labels.tableDeleteColumn, disabled: locked, onClick: run((ed) => ed.chain().focus().deleteColumn().run()) },
2253
+ { key: "tableDelete", icon: "trash", label: labels.tableDelete, disabled: locked, onClick: run((ed) => ed.chain().focus().deleteTable().run()) }
2254
+ ]);
2255
+ }
2256
+ const trailing = [];
2257
+ if (has("history")) {
2258
+ trailing.push({ key: "undo", icon: "undo", label: labels.undo, hint: shortcut("Z"), disabled: richOnly || !state?.canUndo, onClick: run((ed) => ed.chain().focus().undo().run()) });
2259
+ trailing.push({ key: "redo", icon: "redo", label: labels.redo, hint: shortcut(isMac() ? "Shift+Z" : "Y"), disabled: richOnly || !state?.canRedo, onClick: run((ed) => ed.chain().focus().redo().run()) });
2260
+ }
2261
+ if (has("source")) {
2262
+ trailing.push({
2263
+ key: "source",
2264
+ icon: mode === "rich" ? "sourceMode" : "richMode",
2265
+ label: mode === "rich" ? labels.sourceMode : labels.richMode,
2266
+ pressed: mode === "source",
2267
+ disabled: locked,
2268
+ onClick: toggleMode
2269
+ });
2270
+ }
2271
+ const tools = () => Array.from(toolbarRef.current?.querySelectorAll("button[data-tool]:not(:disabled)") ?? []);
2272
+ const onToolbarKeyDown = (event) => {
2273
+ const list = tools();
2274
+ const current = list.indexOf(document.activeElement);
2275
+ if (current < 0) return;
2276
+ let next = current;
2277
+ if (event.key === "ArrowRight") next = (current + 1) % list.length;
2278
+ else if (event.key === "ArrowLeft") next = (current - 1 + list.length) % list.length;
2279
+ else if (event.key === "Home") next = 0;
2280
+ else if (event.key === "End") next = list.length - 1;
2281
+ else return;
2282
+ event.preventDefault();
2283
+ list[next].focus();
2284
+ list[next].scrollIntoView?.({ block: "nearest", inline: "nearest" });
2285
+ };
2286
+ const enabledCount = { value: 0 };
2287
+ const renderTool = (tool) => {
2288
+ const index = tool.disabled ? -1 : enabledCount.value++;
2289
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2290
+ "button",
2291
+ {
2292
+ ref: tool.key === "link" ? linkButtonRef : void 0,
2293
+ type: "button",
2294
+ "data-tool": tool.key,
2295
+ className: "uix-rich-text__tool",
2296
+ "aria-label": tool.label,
2297
+ title: tool.hint ? `${tool.label} (${tool.hint})` : tool.label,
2298
+ "aria-pressed": tool.pressed === void 0 ? void 0 : !!tool.pressed,
2299
+ "aria-keyshortcuts": tool.hint ? tool.hint.replace("\u2318", "Meta+") : void 0,
2300
+ disabled: tool.disabled,
2301
+ tabIndex: index === rovingIndex ? 0 : -1,
2302
+ onFocus: () => {
2303
+ if (index >= 0) setRovingIndex(index);
2304
+ },
2305
+ onMouseDown: (event) => {
2306
+ if (tool.key !== "source") event.preventDefault();
2307
+ },
2308
+ onClick: tool.onClick,
2309
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditorIcon, { name: tool.icon })
2310
+ },
2311
+ tool.key
2312
+ );
2313
+ };
2314
+ (0, import_react7.useEffect)(() => {
2315
+ const count = tools().length;
2316
+ if (count && rovingIndex >= count) setRovingIndex(0);
2317
+ });
2318
+ const toolbar = readOnly ? null : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2319
+ "div",
2320
+ {
2321
+ ref: toolbarRef,
2322
+ className: "uix-rich-text__toolbar",
2323
+ role: "toolbar",
2324
+ "aria-label": labels.toolbar,
2325
+ "aria-controls": editor || mode === "source" ? id : void 0,
2326
+ onKeyDown: onToolbarKeyDown,
2327
+ children: [
2328
+ groups.map((group, g) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-rich-text__group", role: "group", children: group.map(renderTool) }, g)),
2329
+ has("emoji") ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-rich-text__group", role: "group", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2330
+ EmojiPicker,
2331
+ {
2332
+ labels: emojiPickerLabels,
2333
+ locale: emojiLocale,
2334
+ emojiImageBaseUrl,
2335
+ onSelect: insertEmoji,
2336
+ trigger: (() => {
2337
+ const index = locked ? -1 : enabledCount.value++;
2338
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2339
+ "button",
2340
+ {
2341
+ type: "button",
2342
+ "data-tool": "emoji",
2343
+ className: "uix-rich-text__tool",
2344
+ "aria-label": labels.emoji,
2345
+ title: labels.emoji,
2346
+ disabled: locked,
2347
+ tabIndex: index === rovingIndex ? 0 : -1,
2348
+ onFocus: () => {
2349
+ if (index >= 0) setRovingIndex(index);
2350
+ },
2351
+ onMouseDown: (event) => event.preventDefault(),
2352
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditorIcon, { name: "emoji" })
2353
+ }
2354
+ );
2355
+ })()
2356
+ }
2357
+ ) }) : null,
2358
+ trailing.length ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-rich-text__group uix-rich-text__group--end", role: "group", children: trailing.map(renderTool) }) : null,
2359
+ has("link") ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Popover, { id: linkPopoverId, anchor: linkButtonRef, className: "uix-rich-text__link", role: "dialog", "aria-label": labels.link, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2360
+ "form",
2361
+ {
2362
+ className: "uix-rich-text__link-form",
2363
+ onSubmit: (event) => {
2364
+ event.preventDefault();
2365
+ applyLink();
2366
+ },
2367
+ onKeyDown: (event) => {
2368
+ if (event.key === "Escape") {
2369
+ event.preventDefault();
2370
+ closeLink();
2371
+ }
2372
+ },
2373
+ children: [
2374
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "uix-field", children: [
2375
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "uix-field__label", htmlFor: `${uid}-link-url`, children: labels.linkUrl }),
2376
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2377
+ "input",
2378
+ {
2379
+ ref: linkInputRef,
2380
+ id: `${uid}-link-url`,
2381
+ className: "uix-input",
2382
+ type: "text",
2383
+ inputMode: "url",
2384
+ autoComplete: "url",
2385
+ value: linkUrl,
2386
+ "aria-invalid": linkError || void 0,
2387
+ "aria-describedby": linkError ? `${uid}-link-error` : void 0,
2388
+ onChange: (event) => {
2389
+ setLinkUrl(event.target.value);
2390
+ setLinkError(false);
2391
+ }
2392
+ }
2393
+ ),
2394
+ linkError ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-field__msg", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { id: `${uid}-link-error`, className: "uix-field__error", role: "alert", children: labels.linkInvalid }) }) : null
2395
+ ] }),
2396
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "uix-rich-text__link-actions", children: [
2397
+ state?.link ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2398
+ "button",
2399
+ {
2400
+ type: "button",
2401
+ className: "uix-btn uix-btn--ghost uix-btn--sm",
2402
+ onClick: () => {
2403
+ editor?.chain().focus().extendMarkRange("link").unsetLink().run();
2404
+ closeLink();
2405
+ },
2406
+ children: labels.linkRemove
2407
+ }
2408
+ ) : null,
2409
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "submit", className: "uix-btn uix-btn--primary uix-btn--sm", children: labels.linkApply })
2410
+ ] })
2411
+ ]
2412
+ }
2413
+ ) }) : null,
2414
+ has("image") ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2415
+ "input",
2416
+ {
2417
+ ref: fileRef,
2418
+ type: "file",
2419
+ accept: "image/*",
2420
+ hidden: true,
2421
+ tabIndex: -1,
2422
+ "aria-hidden": "true",
2423
+ onChange: (event) => {
2424
+ const file = event.target.files?.[0];
2425
+ event.target.value = "";
2426
+ if (file && editor) void uploadImage(editor, file);
2427
+ }
2428
+ }
2429
+ ) : null
2430
+ ]
2431
+ }
2432
+ );
2433
+ const surface = /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "uix-rich-text__surface", children: [
2434
+ mode === "source" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2435
+ Textarea,
2436
+ {
2437
+ ref: sourceRef,
2438
+ id,
2439
+ value: markdown,
2440
+ onChange: (event) => emit(event.target.value),
2441
+ onBlur,
2442
+ onKeyDown: (event) => {
2443
+ if (onSubmitShortcut && (event.metaKey || event.ctrlKey) && event.key === "Enter") {
2444
+ event.preventDefault();
2445
+ onSubmitShortcut();
2446
+ }
2447
+ },
2448
+ disabled,
2449
+ readOnly,
2450
+ placeholder,
2451
+ rows,
2452
+ spellCheck: false,
2453
+ className: "uix-rich-text__source",
2454
+ "aria-label": props["aria-label"],
2455
+ "aria-labelledby": props["aria-labelledby"],
2456
+ "aria-describedby": describedBy,
2457
+ "aria-invalid": invalid || void 0,
2458
+ "aria-required": props["aria-required"] || void 0
2459
+ }
2460
+ ) : editor ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react8.EditorContent, { editor, className: "uix-rich-text__editor" }) : (
2461
+ // One frame before Tiptap mounts: the same content, read-only, without layout shift.
2462
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-rich-text__content uix-rich-text__content--loading", "aria-busy": "true", style: { minHeight: `calc(${rows} * 1lh + 2 * var(--uix-space-3))` }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Markdown, { isSafeUrl, resolveImageSrc, children: value }) })
2463
+ ),
2464
+ suggestOpen && suggest ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2465
+ EmojiSuggestions,
2466
+ {
2467
+ id: suggestId,
2468
+ label: labels.emojiSuggestions,
2469
+ items: suggest.items,
2470
+ query: suggest.query,
2471
+ active: suggestIndex,
2472
+ rect: suggest.rect,
2473
+ onPick: (item) => suggest.command(item),
2474
+ renderEmoji
2475
+ }
2476
+ ) : null
2477
+ ] });
2478
+ const counter = maxLength !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { id: counterId, className: "uix-rich-text__counter", "data-over": over || void 0, children: formatLabel(labels.characterCount, { count: markdown.length, max: maxLength }) }) : null;
2479
+ const statusLine = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { id: statusId, className: "uix-rich-text__status", role: "status", "data-kind": status?.kind, children: status?.text ?? "" });
2480
+ const hidden = name ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "hidden", name, value: markdown }) : null;
2481
+ const stateAttrs = {
2482
+ "data-disabled": disabled || void 0,
2483
+ "data-readonly": readOnly || void 0,
2484
+ "data-invalid": invalid || void 0,
2485
+ "data-mode": mode
2486
+ };
2487
+ if (variant === "composer") {
2488
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Composer, { className: cx("uix-rich-text", "uix-rich-text--composer", className), ...stateAttrs, children: [
2489
+ surface,
2490
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(ComposerBar, { className: "uix-rich-text__bar", children: [
2491
+ toolbar,
2492
+ statusLine,
2493
+ counter,
2494
+ toolbarEnd
2495
+ ] }),
2496
+ hidden
2497
+ ] });
2498
+ }
2499
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: cx("uix-rich-text", className), ...stateAttrs, children: [
2500
+ toolbar,
2501
+ surface,
2502
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "uix-rich-text__footer", children: [
2503
+ statusLine,
2504
+ counter,
2505
+ toolbarEnd
2506
+ ] }),
2507
+ hidden
2508
+ ] });
2509
+ }
2510
+ function EmojiSuggestions({ id, label, items, query, active, rect, onPick, renderEmoji }) {
2511
+ const listRef = (0, import_react7.useRef)(null);
2512
+ const [pos, setPos] = (0, import_react7.useState)(null);
2513
+ (0, import_react7.useEffect)(() => {
2514
+ const el = listRef.current;
2515
+ if (!el || !rect || typeof window === "undefined") return;
2516
+ const box = el.getBoundingClientRect();
2517
+ const result = computePosition(
2518
+ { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
2519
+ { width: box.width, height: box.height },
2520
+ { width: window.innerWidth, height: window.innerHeight },
2521
+ { placement: "bottom-start", offset: 4, padding: 8 }
2522
+ );
2523
+ setPos({ x: result.x, y: result.y });
2524
+ }, [rect, items.length]);
2525
+ (0, import_react7.useEffect)(() => {
2526
+ document.getElementById(`${id}-${active}`)?.scrollIntoView?.({ block: "nearest" });
2527
+ }, [active, id]);
2528
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2529
+ "div",
2530
+ {
2531
+ ref: listRef,
2532
+ id,
2533
+ role: "listbox",
2534
+ "aria-label": label,
2535
+ className: "uix-rich-text__suggest",
2536
+ style: { left: pos?.x ?? -9999, top: pos?.y ?? -9999 },
2537
+ children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2538
+ "div",
2539
+ {
2540
+ id: `${id}-${i}`,
2541
+ role: "option",
2542
+ "aria-selected": i === active,
2543
+ className: "uix-rich-text__suggest-item",
2544
+ onMouseDown: (event) => {
2545
+ event.preventDefault();
2546
+ onPick(item);
2547
+ },
2548
+ children: [
2549
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "uix-rich-text__suggest-emoji", "aria-hidden": "true", children: renderEmoji(item.emoji ?? "") }),
2550
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "uix-rich-text__suggest-name", children: [
2551
+ ":",
2552
+ item.shortcodes.find((s) => s.startsWith(query.toLowerCase())) ?? item.shortcodes[0] ?? item.name,
2553
+ ":"
2554
+ ] })
2555
+ ]
2556
+ },
2557
+ item.name
2558
+ ))
2559
+ }
2560
+ );
2561
+ }
2562
+
2563
+ // src/rich-text/round-trip.ts
2564
+ var shared;
2565
+ async function roundTripMarkdown(markdown, options) {
2566
+ const pipeline = options ? createPipeline(options) : shared ?? (shared = createPipeline());
2567
+ const origin = pipeline.read(markdown);
2568
+ const doc = pipeline.schema.nodeFromJSON(origin.doc).toJSON();
2569
+ return pipeline.write(doc, origin);
2570
+ }
2571
+ // Annotate the CommonJS export names for ESM import in node:
2572
+ 0 && (module.exports = {
2573
+ DEFAULT_RICH_TEXT_LABELS,
2574
+ RichTextEditor,
2575
+ RichTextEditorFallback,
2576
+ emojiImageFileName,
2577
+ roundTripMarkdown
2578
+ });