@tensor_1/react 2.39.0 → 2.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,16 +31,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var rich_text_exports = {};
32
32
  __export(rich_text_exports, {
33
33
  DEFAULT_RICH_TEXT_LABELS: () => DEFAULT_RICH_TEXT_LABELS,
34
+ MENTION_SCHEME: () => MENTION_SCHEME,
34
35
  RichTextEditor: () => RichTextEditor,
35
36
  RichTextEditorFallback: () => RichTextEditorFallback,
36
37
  emojiImageFileName: () => emojiImageFileName,
38
+ findMentions: () => findMentions,
39
+ formatMention: () => formatMention,
40
+ isMentionId: () => isMentionId,
41
+ readMention: () => readMention,
37
42
  roundTripMarkdown: () => roundTripMarkdown
38
43
  });
39
44
  module.exports = __toCommonJS(rich_text_exports);
40
45
 
41
46
  // src/components/RichTextEditor.tsx
42
47
  var import_react7 = require("react");
43
- var import_core2 = require("@tiptap/core");
48
+ var import_core3 = require("@tiptap/core");
44
49
  var import_react8 = require("@tiptap/react");
45
50
  var import_markdown2 = require("@tiptap/markdown");
46
51
  var import_extensions = require("@tiptap/extensions");
@@ -328,7 +333,7 @@ function useEmojiRenderer(emojiImageBaseUrl) {
328
333
  }
329
334
 
330
335
  // src/rich-text/pipeline.ts
331
- var import_core = require("@tiptap/core");
336
+ var import_core2 = require("@tiptap/core");
332
337
  var import_markdown = require("@tiptap/markdown");
333
338
  var import_extension_list = require("@tiptap/extension-list");
334
339
  var import_extension_table = require("@tiptap/extension-table");
@@ -346,1578 +351,1699 @@ function defaultIsSafeUrl(url) {
346
351
  return ALLOWED_START.test(candidate);
347
352
  }
348
353
 
349
- // src/rich-text/pipeline.ts
350
- var indentContinuation = (h) => {
351
- let first = true;
352
- return {
353
- ...h,
354
- renderChildren: (nodes) => {
355
- const out = h.renderChildren(nodes);
356
- if (!first) return out;
357
- first = false;
358
- return out.split("\n").map((line, n) => n > 0 && line ? h.indent(line) : line).join("\n");
359
- }
360
- };
361
- };
362
- var listItemRender = import_extension_list.ListItem.config.renderMarkdown;
363
- var UixListItem = import_extension_list.ListItem.extend({
364
- renderMarkdown: (node, h, ctx) => listItemRender(node, indentContinuation(h), ctx)
365
- });
366
- var UixTaskItem = import_extension_list.TaskItem.extend({
367
- renderMarkdown: (node, h) => {
368
- const prefix = `- [${node.attrs?.checked ? "x" : " "}] `;
369
- const [first, ...rest] = node.content ?? [];
370
- if (first?.type !== "paragraph" || !(first.content ?? []).some((c) => c.type === "hardBreak")) {
371
- return (0, import_core.renderNestedMarkdownContent)(node, h, prefix);
372
- }
373
- const lines = [[]];
374
- for (const child of first.content ?? []) {
375
- if (child.type === "hardBreak") lines.push([]);
376
- else lines[lines.length - 1].push(child);
377
- }
378
- const paragraphs = lines.filter((line) => line.length > 0).map((content) => ({ type: "paragraph", content }));
379
- return (0, import_core.renderNestedMarkdownContent)({ ...node, content: [...paragraphs, ...rest] }, h, prefix);
380
- }
381
- });
382
- function createSchemaExtensions(options = {}) {
383
- const isSafeUrl = options.isSafeUrl ?? defaultIsSafeUrl;
384
- return [
385
- import_starter_kit.StarterKit.configure({
386
- underline: false,
387
- listItem: false,
388
- heading: { levels: [...options.headingLevels ?? [1, 2, 3]] },
389
- link: {
390
- openOnClick: false,
391
- autolink: true,
392
- linkOnPaste: true,
393
- defaultProtocol: "https",
394
- // Unsafe links never become marks: pasted or typed, they stay text.
395
- isAllowedUri: (url) => isSafeUrl(url),
396
- shouldAutoLink: (url) => isSafeUrl(url),
397
- HTMLAttributes: { rel: "noopener noreferrer nofollow", target: null }
398
- }
399
- }),
400
- UixListItem,
401
- import_extension_list.TaskList,
402
- UixTaskItem.configure({ nested: true }),
403
- import_extension_table.TableKit.configure({ table: { resizable: false } }),
404
- options.image ?? import_extension_image.Image.configure({ inline: true, allowBase64: false })
405
- ];
354
+ // src/rich-text/mention.ts
355
+ var import_core = require("@tiptap/core");
356
+
357
+ // src/markdown-model.ts
358
+ var MENTION_SCHEME = "mention:";
359
+ var MENTION_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
360
+ var MENTION_RE = /^@\[((?:\\[\\[\]]|[^\\[\]\n]){1,200})\]\(mention:([A-Za-z0-9][A-Za-z0-9_.-]{0,127})\)/;
361
+ var cleanMentionLabel = (label) => label.replace(/\s+/gu, " ").trim().slice(0, 200);
362
+ function isMentionId(id) {
363
+ return MENTION_ID.test(id);
364
+ }
365
+ function formatMention(mention) {
366
+ const label = cleanMentionLabel(mention.label);
367
+ if (!isMentionId(mention.id) || !label) return null;
368
+ return `@[${label.replace(/[\\[\]]/g, (ch) => `\\${ch}`)}](${MENTION_SCHEME}${mention.id})`;
369
+ }
370
+ function readMention(text) {
371
+ const match = MENTION_RE.exec(text);
372
+ if (!match) return null;
373
+ const label = cleanMentionLabel(match[1].replace(/\\([\\[\]])/g, "$1"));
374
+ return label ? { id: match[2], label, length: match[0].length } : null;
406
375
  }
407
- var ENTITY_AFTER_AMP = /^(?:#\d+;|#x[\da-f]+;|[a-z][a-z\d]*;)/i;
408
- var TAG_START = /^[a-z/!?]$/i;
409
376
  var ASCII_PUNCT = /[!-/:-@[-`{-~]/;
410
- var LIST_MARKER = /^(?:\d+|[ivxlcdmIVXLCDM]+|[a-zA-Z]{1,2})$/;
411
- var BACKSLASH = String.fromCharCode(92);
412
- function encodeText(text, options = {}) {
413
- let out = "";
414
- const chars = Array.from(text);
415
- let lineStart = 0;
416
- let inLead = options.lineStart ?? true;
417
- for (let i = 0; i < chars.length; i += 1) {
418
- const ch = chars[i];
419
- const prev = chars[i - 1] ?? "";
420
- const next = chars[i + 1] ?? "";
421
- if (ch === "\n") {
422
- out += ch;
423
- lineStart = i + 1;
424
- inLead = true;
377
+ var WORD = /[\p{L}\p{N}]/u;
378
+ var NAMED_ENTITIES = {
379
+ amp: "&",
380
+ lt: "<",
381
+ gt: ">",
382
+ quot: '"',
383
+ apos: "'",
384
+ nbsp: "\xA0",
385
+ copy: "\xA9",
386
+ reg: "\xAE",
387
+ hellip: "\u2026",
388
+ mdash: "\u2014",
389
+ ndash: "\u2013"
390
+ };
391
+ var ENTITY_RE = /^&(?:#(\d{1,7})|#[xX]([\da-fA-F]{1,6})|([a-zA-Z][a-zA-Z\d]{1,31}));/;
392
+ var ENTITY_GLOBAL = /&(?:#\d{1,7}|#[xX][\da-fA-F]{1,6}|[a-zA-Z][a-zA-Z\d]{1,31});/g;
393
+ var BARE_URL_RE = /^https?:\/\/[^\s<>()]+/;
394
+ var BR_RE = /^<br\s*\/?>/i;
395
+ var AUTOLINK_RE = /^<((?:https?:|mailto:)[^\s<>]*)>/i;
396
+ var decodeEntity = (match) => {
397
+ const [, dec, hex, name] = match;
398
+ const code = dec ? Number(dec) : hex ? parseInt(hex, 16) : void 0;
399
+ if (code !== void 0) return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : "\uFFFD";
400
+ return name && Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, name) ? NAMED_ENTITIES[name] : null;
401
+ };
402
+ var MAX_SCAN = 2e3;
403
+ var MAX_DEPTH = 12;
404
+ var findBracketEnd = (text, open) => {
405
+ let depth = 0;
406
+ const stop = Math.min(text.length, open + MAX_SCAN);
407
+ for (let i = open; i < stop; i += 1) {
408
+ const ch = text[i];
409
+ if (ch === "\\") {
410
+ i += 1;
425
411
  continue;
426
412
  }
427
- if (inLead && (ch === " " || ch === " ")) {
428
- continue;
413
+ if (ch === "`") {
414
+ const close = text.indexOf("`", i + 1);
415
+ if (close > 0) {
416
+ i = close;
417
+ continue;
418
+ }
429
419
  }
430
- const atStart = inLead;
431
- inLead = false;
432
- const lineRest = chars.slice(i).join("").split("\n")[0];
433
- const endOrSpace = next === "" || next === "\n" || /\s/u.test(next);
434
- const markerBefore = !atStart && LIST_MARKER.test(out.slice(out.lastIndexOf("\n") + 1));
435
- if (
436
- // `#` always: marked ends a list item at a line starting with `#`, space or not.
437
- atStart && (ch === "#" || /[-+*]/.test(ch) && endOrSpace) || atStart && /[-=*_]/.test(ch) && new RegExp(`^(?:\\${ch}[ \\t]*)+$`).test(lineRest) || markerBefore && /[.)]/.test(ch) && endOrSpace
438
- ) {
439
- out += BACKSLASH + ch;
440
- continue;
420
+ if (ch === "[") depth += 1;
421
+ else if (ch === "]") {
422
+ depth -= 1;
423
+ if (depth === 0) return i;
441
424
  }
442
- if (ch === "#" && options.headingEnd && (prev === "" || /\s/u.test(prev)) && /^#+$/.test(lineRest)) {
443
- out += BACKSLASH + ch;
425
+ }
426
+ return -1;
427
+ };
428
+ var readDestination = (text, open) => {
429
+ if (text[open] !== "(") return null;
430
+ let depth = 0;
431
+ let end = -1;
432
+ const stop = Math.min(text.length, open + MAX_SCAN);
433
+ for (let i = open; i < stop; i += 1) {
434
+ const ch = text[i];
435
+ if (ch === "\\") {
436
+ i += 1;
444
437
  continue;
445
438
  }
446
- switch (ch) {
447
- case BACKSLASH:
448
- out += ASCII_PUNCT.test(next) || next === "" ? BACKSLASH + BACKSLASH : BACKSLASH;
449
- break;
450
- case "`":
451
- case "[":
452
- case "]":
453
- case "~":
454
- out += BACKSLASH + ch;
455
- break;
456
- case "|":
457
- out += options.inTable ? BACKSLASH + ch : ch;
458
- break;
459
- case "&":
460
- out += ENTITY_AFTER_AMP.test(chars.slice(i + 1, i + 40).join("")) ? "&amp;" : ch;
461
- break;
462
- case "<":
463
- out += TAG_START.test(next) ? "&lt;" : ch;
464
- break;
465
- case "!":
466
- out += next === "" && options.beforeLink ? BACKSLASH + ch : ch;
467
- break;
468
- case "*":
469
- case "_": {
470
- const spaced = (prev === "" || /\s/u.test(prev)) && (next === "" || /\s/u.test(next));
471
- const intraword = ch === "_" && /[\p{L}\p{N}]/u.test(prev) && /[\p{L}\p{N}]/u.test(next);
472
- out += spaced || intraword ? ch : BACKSLASH + ch;
439
+ if (ch === "(") depth += 1;
440
+ else if (ch === ")") {
441
+ depth -= 1;
442
+ if (depth === 0) {
443
+ end = i;
473
444
  break;
474
445
  }
475
- case ">":
476
- out += atStart ? BACKSLASH + ch : ch;
477
- break;
478
- default:
479
- out += ch;
480
446
  }
481
447
  }
482
- return out;
483
- }
484
- function createMarked() {
485
- return new import_marked.Marked({
486
- gfm: true,
487
- tokenizer: {
488
- html: () => void 0,
489
- // `<br>` is the one tag read: GFM tables have no other way to hold a line break,
490
- // and Tiptap writes hard breaks in cells that way.
491
- tag: (src) => {
492
- const br = /^<br\s*\/?>/i.exec(src);
493
- return br ? { type: "br", raw: br[0] } : void 0;
448
+ if (end < 0) return null;
449
+ const inner = text.slice(open + 1, end).trim();
450
+ const titled = /^(\S+)\s+(?:"([^"]*)"|'([^']*)')$/.exec(inner);
451
+ const rawUrl = titled ? titled[1] : inner;
452
+ const url = rawUrl.replace(/^<(.*)>$/, "$1").replace(/\\([!-/:-@[-`{-~])/g, "$1").replace(ENTITY_GLOBAL, (match) => decodeEntity(ENTITY_RE.exec(match)) ?? match);
453
+ const title = titled ? titled[2] ?? titled[3] : void 0;
454
+ return title === void 0 ? { url, end } : { url, title, end };
455
+ };
456
+ var backtickRun = (text, at) => {
457
+ let end = at;
458
+ while (text[end] === "`") end += 1;
459
+ return text.slice(at, end);
460
+ };
461
+ var findCloser = (text, from, delim, misses) => {
462
+ if (from >= (misses.get(delim) ?? Infinity)) return -1;
463
+ for (let i = from; i < text.length; i += 1) {
464
+ const ch = text[i];
465
+ if (ch === "\\") {
466
+ i += 1;
467
+ continue;
468
+ }
469
+ if (ch === "`") {
470
+ const run = backtickRun(text, i);
471
+ const close = text.indexOf(run, i + run.length);
472
+ if (close > 0) {
473
+ i = close + run.length - 1;
474
+ continue;
494
475
  }
495
476
  }
496
- });
497
- }
498
- var hasLink = (node) => (node?.marks ?? []).some((m) => (typeof m === "string" ? m : m.type) === "link");
499
- function patchManager(manager) {
500
- const internals = manager;
501
- internals.encodeTextForMarkdown = function encode(text, node, parentNode) {
502
- const inCode = parentNode?.type != null && internals.codeTypes.has(parentNode.type) || (node.marks ?? []).some((m) => internals.codeTypes.has(typeof m === "string" ? m : m.type));
503
- if (inCode) return text;
504
- const siblings = parentNode?.content ?? [];
505
- const index = siblings.indexOf(node);
506
- const previous = index > 0 ? siblings[index - 1] : void 0;
507
- const next = index >= 0 ? siblings[index + 1] : void 0;
508
- return encodeText(text, {
509
- inTable: !!internals.uixInTable,
510
- beforeLink: hasLink(next) && !hasLink(node),
511
- // Unknown position (node not found among its siblings): assume a line start, the safe side.
512
- lineStart: index <= 0 || previous?.type === "hardBreak",
513
- headingEnd: parentNode?.type === "heading" && (index < 0 || index === siblings.length - 1)
514
- });
515
- };
516
- return manager;
517
- }
518
- function serializeNode(manager, node) {
519
- const internals = manager;
520
- internals.uixInTable = node.type === "table";
521
- try {
522
- return manager.serialize({ type: "doc", content: [node] }).replace(/^\n+|\n+$/g, "");
523
- } finally {
524
- internals.uixInTable = false;
525
- }
526
- }
527
- function createManager(extensions) {
528
- return patchManager(new import_markdown.MarkdownManager({ marked: createMarked(), extensions }));
529
- }
530
- var SEPARATOR_TOKENS = /* @__PURE__ */ new Set(["space", "def"]);
531
- var nodeKey = (node) => JSON.stringify(node);
532
- var normalize = (schema, content) => {
533
- if (content.length === 0) return [];
534
- const blocks = [];
535
- for (const node of content) {
536
- if (node.type && schema.nodes[node.type]?.isInline) {
537
- const last = blocks[blocks.length - 1];
538
- if (last?.type === "paragraph" && last.wrapped) last.content.push(node);
539
- else blocks.push({ type: "paragraph", content: [node], wrapped: true });
540
- } else {
541
- blocks.push(node);
477
+ if (text.startsWith(delim, i) && i > from && !/\s/u.test(text[i - 1])) {
478
+ if (delim[0] === "_" && WORD.test(text[i + delim.length] ?? "")) continue;
479
+ if (delim.length === 1 && text[i + 1] === delim) {
480
+ i += 1;
481
+ continue;
482
+ }
483
+ return i;
542
484
  }
543
485
  }
544
- for (const block of blocks) delete block.wrapped;
545
- const doc = schema.nodeFromJSON({ type: "doc", content: blocks });
546
- doc.check();
547
- return doc.toJSON().content ?? [];
486
+ misses.set(delim, Math.min(from, misses.get(delim) ?? Infinity));
487
+ return -1;
548
488
  };
549
- function readMarkdown(source, manager, schema) {
550
- const normalized = source.replace(/\r\n?/g, "\n");
551
- const lexed = manager.instance.lexer(normalized);
552
- const origin = [];
553
- for (let i = 0, o = 0; i <= normalized.length; i += 1, o += 1) {
554
- origin.push(o);
555
- if (source[o] === "\r" && source[o + 1] === "\n") o += 1;
489
+ var pushText = (out, value) => {
490
+ if (!value) return;
491
+ const last = out[out.length - 1];
492
+ if (last?.type === "text") last.value += value;
493
+ else out.push({ type: "text", value });
494
+ };
495
+ function parseInline(text, depth = 0) {
496
+ const out = [];
497
+ if (depth > MAX_DEPTH) {
498
+ pushText(out, text);
499
+ return out;
556
500
  }
557
- let cursor = 0;
558
- const tokens = lexed.map((t) => {
559
- const start = origin[cursor];
560
- cursor += t.raw.length;
561
- return { type: t.type, raw: source.slice(start, origin[cursor]) };
562
- });
563
- const joined = lexed.map((t) => t.raw).join("") === normalized ? tokens.map((t) => t.raw).join("") : null;
564
- const blocks = [];
565
- const eol = /\r\n/.test(source) && !/(^|[^\r])\n/.test(source) ? "\r\n" : "\n";
566
- let lead = "";
567
- let defs = [];
568
- const keepInLead = (raw) => {
569
- lead += raw;
570
- const text = raw.replace(/\s+$/u, "").replace(/^\s+/u, "");
571
- if (text) defs.push(text);
572
- };
573
- const pushBlock = (raw) => {
574
- let nodes;
575
- try {
576
- nodes = normalize(schema, manager.parse(raw).content ?? []);
577
- } catch {
578
- nodes = normalize(schema, [{ type: "paragraph", content: [{ type: "text", text: raw }] }]);
579
- }
580
- if (nodes.length === 0) {
581
- keepInLead(raw);
582
- return;
501
+ const misses = /* @__PURE__ */ new Map();
502
+ let i = 0;
503
+ while (i < text.length) {
504
+ const ch = text[i];
505
+ const rest = text.slice(i);
506
+ if (ch === "\\" && ASCII_PUNCT.test(text[i + 1] ?? "")) {
507
+ pushText(out, text[i + 1]);
508
+ i += 2;
509
+ continue;
583
510
  }
584
- blocks.push({ raw, lead, nodes, keys: nodes.map(nodeKey), defs });
585
- lead = "";
586
- defs = [];
587
- };
588
- if (joined !== source) {
589
- const core = source.replace(/\s+$/u, "");
590
- lead = source.slice(0, source.length - source.trimStart().length);
591
- if (core.trim()) pushBlock(core.trimStart());
592
- const tail = source.slice(core.length);
593
- return { source, blocks, tail, tailDefs: [], eol, doc: { type: "doc", content: blocks.flatMap((b) => b.nodes) } };
594
- }
595
- for (const token of tokens) {
596
- if (SEPARATOR_TOKENS.has(token.type) || token.raw.trim() === "") {
597
- if (token.type === "def") keepInLead(token.raw);
598
- else lead += token.raw;
511
+ if (ch === "\n") {
512
+ out.push({ type: "br" });
513
+ i += 1;
599
514
  continue;
600
515
  }
601
- const leading = /^(?:[ \t]*\r?\n)*/.exec(token.raw)[0];
602
- lead += leading;
603
- const body = token.raw.slice(leading.length);
604
- const core = body.replace(/\s+$/u, "");
605
- pushBlock(core);
606
- lead += body.slice(core.length);
607
- }
608
- return {
609
- source,
610
- blocks,
611
- tail: lead,
612
- tailDefs: defs,
613
- eol,
614
- doc: { type: "doc", content: blocks.flatMap((b) => b.nodes) }
615
- };
616
- }
617
- var isEmptyParagraph = (node) => node.type === "paragraph" && !(node.content ?? []).some((c) => c.type !== "text" || (c.text ?? "").trim() !== "");
618
- var matchesAt = (children, at, block) => block.keys.every((key, k) => children[at + k] === key);
619
- var LOOKAHEAD = 8;
620
- function writeMarkdown(doc, origin, manager) {
621
- const content = [...doc.content ?? []];
622
- while (content.length > 0 && isEmptyParagraph(content[content.length - 1])) content.pop();
623
- if (content.length === 0) {
624
- return origin.blocks.length === 0 ? origin.source : "";
625
- }
626
- const keys = content.map(nodeKey);
627
- const { blocks, eol } = origin;
628
- const pieces = [];
629
- let i = 0;
630
- let j = 0;
631
- while (i < content.length) {
632
- if (j < blocks.length && matchesAt(keys, i, blocks[j])) {
633
- pieces.push({ text: blocks[j].raw, slot: j, edited: false });
634
- i += blocks[j].keys.length;
635
- j += 1;
516
+ if (ch === "`") {
517
+ const run = backtickRun(text, i);
518
+ const close = text.indexOf(run, i + run.length);
519
+ if (close > 0 && text[close + run.length] !== "`") {
520
+ let value = text.slice(i + run.length, close).replace(/\n/g, " ");
521
+ if (/^ .* $/.test(value) && value.trim()) value = value.slice(1, -1);
522
+ out.push({ type: "code", value });
523
+ i = close + run.length;
524
+ continue;
525
+ }
526
+ pushText(out, run);
527
+ i += run.length;
636
528
  continue;
637
529
  }
638
- let skip = 0;
639
- for (let d = 1; d <= LOOKAHEAD && j + d < blocks.length; d += 1) {
640
- if (matchesAt(keys, i, blocks[j + d])) {
641
- skip = d;
642
- break;
530
+ if (ch === "@" && text[i + 1] === "[") {
531
+ const mention = readMention(rest);
532
+ if (mention) {
533
+ out.push({ type: "mention", id: mention.id, label: mention.label });
534
+ i += mention.length;
535
+ continue;
643
536
  }
644
537
  }
645
- let insert = 0;
646
- if (j < blocks.length) {
647
- for (let d = 1; d <= LOOKAHEAD && i + d < content.length; d += 1) {
648
- if (matchesAt(keys, i + d, blocks[j])) {
649
- insert = d;
650
- break;
651
- }
538
+ if (ch === "!" && text[i + 1] === "[") {
539
+ const end = findBracketEnd(text, i + 1);
540
+ const dest = end > 0 ? readDestination(text, end + 1) : null;
541
+ if (dest) {
542
+ const alt = plainText(parseInline(text.slice(i + 2, end), depth + 1));
543
+ out.push(dest.title === void 0 ? { type: "image", src: dest.url, alt } : { type: "image", src: dest.url, alt, title: dest.title });
544
+ i = dest.end + 1;
545
+ continue;
652
546
  }
653
547
  }
654
- if (skip && (!insert || skip <= insert)) {
655
- j += skip;
548
+ if (ch === "[") {
549
+ const end = findBracketEnd(text, i);
550
+ const dest = end > 0 ? readDestination(text, end + 1) : null;
551
+ if (dest) {
552
+ const children = parseInline(text.slice(i + 1, end), depth + 1);
553
+ out.push(dest.title === void 0 ? { type: "link", href: dest.url, children } : { type: "link", href: dest.url, title: dest.title, children });
554
+ i = dest.end + 1;
555
+ continue;
556
+ }
557
+ }
558
+ if (ch === "<") {
559
+ const br = BR_RE.exec(rest);
560
+ if (br) {
561
+ out.push({ type: "br" });
562
+ i += br[0].length;
563
+ continue;
564
+ }
565
+ const auto = AUTOLINK_RE.exec(rest);
566
+ if (auto) {
567
+ out.push({ type: "link", href: auto[1], children: [{ type: "text", value: auto[1].replace(/^mailto:/i, "") }], bare: true });
568
+ i += auto[0].length;
569
+ continue;
570
+ }
571
+ }
572
+ if (ch === "&") {
573
+ const entity = ENTITY_RE.exec(rest);
574
+ const decoded = entity ? decodeEntity(entity) : null;
575
+ if (entity && decoded !== null) {
576
+ pushText(out, decoded);
577
+ i += entity[0].length;
578
+ continue;
579
+ }
580
+ }
581
+ if (ch === "~" && text[i + 1] === "~" && text[i + 2] && !/\s/u.test(text[i + 2])) {
582
+ const close = findCloser(text, i + 2, "~~", misses);
583
+ if (close > 0) {
584
+ out.push({ type: "del", children: parseInline(text.slice(i + 2, close), depth + 1) });
585
+ i = close + 2;
586
+ continue;
587
+ }
588
+ }
589
+ if (ch === "*" || ch === "_") {
590
+ const leftOk = ch === "*" || !WORD.test(text[i - 1] ?? "");
591
+ const double = text[i + 1] === ch;
592
+ const delim = double ? ch + ch : ch;
593
+ const after = text[i + delim.length];
594
+ if (leftOk && after && !/\s/u.test(after)) {
595
+ const close = findCloser(text, i + delim.length, delim, misses);
596
+ if (close > 0) {
597
+ const children = parseInline(text.slice(i + delim.length, close), depth + 1);
598
+ out.push(double ? { type: "strong", children } : { type: "em", children });
599
+ i = close + delim.length;
600
+ continue;
601
+ }
602
+ }
603
+ const run = new RegExp(`^\\${ch}+`).exec(rest)[0];
604
+ pushText(out, run);
605
+ i += run.length;
656
606
  continue;
657
607
  }
658
- let text = serializeNode(manager, content[i]);
659
- if (eol !== "\n") text = text.replace(/\n/g, eol);
660
- const inPlace = !insert && j < blocks.length && blocks[j].keys.length === 1;
661
- pieces.push(inPlace ? { text, slot: j, edited: true } : { text, edited: true });
608
+ if (ch === "h" && !WORD.test(text[i - 1] ?? "")) {
609
+ const bare = BARE_URL_RE.exec(rest);
610
+ if (bare) {
611
+ const url = bare[0].replace(/[.,;:!?'"]+$/, "");
612
+ out.push({ type: "link", href: url, children: [{ type: "text", value: url }], bare: true });
613
+ i += url.length;
614
+ continue;
615
+ }
616
+ }
617
+ pushText(out, ch);
662
618
  i += 1;
663
- if (!insert && j < blocks.length) j += 1;
664
619
  }
665
- const blank = eol + eol;
666
- const emitted = /* @__PURE__ */ new Set();
667
- let out = "";
668
- pieces.forEach((piece, n) => {
669
- const prev = pieces[n - 1];
670
- const slot = piece.slot;
671
- const lead = slot !== void 0 ? blocks[slot].lead : "";
672
- if (n === 0) {
673
- if (slot === 0) {
674
- out += lead;
675
- emitted.add(0);
676
- }
677
- } else if (slot !== void 0 && prev?.slot === slot - 1 && // Next to a re-serialized block, only a separator with a blank line is safe.
678
- (!(piece.edited || prev.edited) || /\n[ \t]*\r?\n/.test(lead))) {
679
- out += lead;
680
- emitted.add(slot);
681
- } else {
682
- out += blank;
683
- }
684
- out += piece.text;
685
- });
686
- const last = pieces[pieces.length - 1];
687
- const tailKept = last.slot === blocks.length - 1;
688
- const lost = [
689
- ...blocks.flatMap((block, n) => emitted.has(n) ? [] : block.defs),
690
- ...tailKept ? [] : origin.tailDefs
691
- ];
692
- if (lost.length) out += blank + lost.join(eol);
693
- if (tailKept) out += origin.tail;
694
- else if (/\n$/.test(origin.tail)) out += eol;
695
620
  return out;
696
621
  }
697
- function createPipeline(options = {}) {
698
- const extensions = createSchemaExtensions(options);
699
- const schema = (0, import_core.getSchema)(extensions);
700
- const manager = createManager(extensions);
701
- return {
702
- extensions,
703
- schema,
704
- manager,
705
- read: (source) => readMarkdown(source, manager, schema),
706
- write: (doc, origin) => writeMarkdown(doc, origin, manager)
707
- };
708
- }
709
-
710
- // src/components/Composer.tsx
711
- var import_react2 = require("react");
712
- var import_jsx_runtime2 = require("react/jsx-runtime");
713
- function Composer({ children, className, label = "Add a comment", ...props }) {
714
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx("uix-composer", className), ...props, children: import_react2.Children.map(children, (child) => {
715
- if (!(0, import_react2.isValidElement)(child) || child.type !== "textarea") return child;
716
- const p = child.props;
717
- return p["aria-label"] || p["aria-labelledby"] || p.id ? child : (0, import_react2.cloneElement)(child, { "aria-label": label });
718
- }) });
719
- }
720
- function ComposerBar({ children, className, ...props }) {
721
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx("uix-composer__bar", className), ...props, children });
622
+ function plainText(nodes) {
623
+ return nodes.map((n) => {
624
+ switch (n.type) {
625
+ case "text":
626
+ case "code":
627
+ return n.value;
628
+ case "br":
629
+ return " ";
630
+ case "image":
631
+ return n.alt;
632
+ case "mention":
633
+ return `@${n.label}`;
634
+ default:
635
+ return plainText(n.children);
636
+ }
637
+ }).join("");
722
638
  }
723
-
724
- // src/components/EditorIcons.tsx
725
- var import_jsx_runtime3 = require("react/jsx-runtime");
726
- var PATHS = {
727
- heading1: "M4 12h8M4 18V6M12 18V6M17 12l3-2v8",
728
- heading2: "M4 12h8M4 18V6M12 18V6M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1",
729
- 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",
730
- 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",
731
- italic: "M19 4h-9M14 20H5M15 4 9 20",
732
- strike: "M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6M4 12h16",
733
- code: "m16 18 6-6-6-6M8 6l-6 6 6 6",
734
- bulletList: "M3 12h.01M3 18h.01M3 6h.01M8 12h13M8 18h13M8 6h13",
735
- orderedList: "M10 12h11M10 18h11M10 6h11M4 10h2M4 6h1v4M6 18H4c0-1 2-2 2-3s-1-1.5-2-1",
736
- taskList: "m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8",
737
- blockquote: "M17 6H3M21 12H8M21 18H8M3 12v6",
738
- 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",
739
- horizontalRule: "M5 12h14",
740
- 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",
741
- 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",
742
- 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",
743
- 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",
744
- 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",
745
- 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",
746
- 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",
747
- 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",
748
- 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",
749
- addReaction: "M22 11v1a10 10 0 1 1-9-10M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01M16 5h6M19 2v6",
750
- undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
751
- redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13",
752
- 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",
753
- 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",
754
- clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM12 6v6l4 2",
755
- grip: "M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01",
756
- more: "M5 12h.01M12 12h.01M19 12h.01"
639
+ var collectMentions = (nodes, out) => {
640
+ for (const n of nodes) {
641
+ if (n.type === "mention") out.push({ id: n.id, label: n.label });
642
+ else if ("children" in n) collectMentions(n.children, out);
643
+ }
757
644
  };
758
- function EditorIcon({ name, ...props }) {
759
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
760
- "svg",
761
- {
762
- viewBox: "0 0 24 24",
763
- fill: "none",
764
- stroke: "currentColor",
765
- strokeWidth: 2,
766
- strokeLinecap: "round",
767
- strokeLinejoin: "round",
768
- "aria-hidden": "true",
769
- focusable: "false",
770
- ...props,
771
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: PATHS[name] })
645
+ function findMentions(markdown) {
646
+ const out = [];
647
+ const walkList = (list) => {
648
+ for (const item of list.items) {
649
+ collectMentions(item.children, out);
650
+ item.lists.forEach(walkList);
772
651
  }
773
- );
774
- }
775
-
776
- // src/components/EmojiPicker.tsx
777
- var import_react5 = require("react");
778
-
779
- // src/components/Popover.tsx
780
- var import_react4 = require("react");
781
-
782
- // src/hooks/useAnchoredPosition.ts
783
- var import_react3 = require("react");
784
- var useIsomorphicLayoutEffect = typeof document !== "undefined" ? import_react3.useLayoutEffect : import_react3.useEffect;
785
- var resolveAnchor = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
786
- var anchoringSupported;
787
- function supportsAnchoring() {
788
- if (anchoringSupported === void 0) {
789
- try {
790
- const css = typeof CSS !== "undefined" ? CSS : void 0;
791
- anchoringSupported = !!css && typeof css.supports === "function" && css.supports("anchor-name", "--uix-a") && css.supports("position-anchor", "--uix-a") && css.supports("top", "round(anchor(bottom) + 1px, 1px)");
792
- } catch {
793
- anchoringSupported = false;
652
+ };
653
+ for (const block of parseMarkdown(markdown)) {
654
+ switch (block.type) {
655
+ case "paragraph":
656
+ case "heading":
657
+ case "quote":
658
+ collectMentions(block.children, out);
659
+ break;
660
+ case "list":
661
+ walkList(block);
662
+ break;
663
+ case "table":
664
+ block.head.forEach((cell) => collectMentions(cell, out));
665
+ block.rows.forEach((row) => row.forEach((cell) => collectMentions(cell, out)));
666
+ break;
667
+ default:
668
+ break;
794
669
  }
795
670
  }
796
- return anchoringSupported;
797
- }
798
- var ANCHOR_BAND = 64;
799
- var anchorCount = 0;
800
- var namesOf = (value) => value.split(",").map((n) => n.trim()).filter((n) => n.startsWith("--"));
801
- function addAnchorName(el, name) {
802
- const names = namesOf(getComputedStyle(el).getPropertyValue("anchor-name"));
803
- if (!names.includes(name)) names.push(name);
804
- el.style.setProperty("anchor-name", names.join(", "));
805
- }
806
- function removeAnchorName(el, name) {
807
- const names = namesOf(el.style.getPropertyValue("anchor-name")).filter((n) => n !== name);
808
- if (names.length) el.style.setProperty("anchor-name", names.join(", "));
809
- else el.style.removeProperty("anchor-name");
671
+ return out;
810
672
  }
811
- function translationOf(el) {
812
- const t = getComputedStyle(el).transform;
813
- if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
814
- try {
815
- const m = new DOMMatrixReadOnly(t);
816
- return { x: m.m41, y: m.m42 };
817
- } catch {
818
- return { x: 0, y: 0 };
673
+ var FENCE_RE = /^(`{3,}|~{3,})\s*([^`\s]*)/;
674
+ var HEADING_RE = /^(#{1,6})\s+(.*?)(?:\s+#+)?$/;
675
+ var RULE_RE = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
676
+ var BULLET_RE = /^(\s*)([-*+])\s+(.*)$/;
677
+ var ORDERED_RE = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
678
+ var TASK_RE = /^\[([ xX])\]\s+(.*)$/;
679
+ var DELIMITER_ROW_RE = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/;
680
+ var indentOf = (s) => s.replace(/\t/g, " ").length;
681
+ function splitTableRow(line) {
682
+ let row = line.trim();
683
+ if (row.startsWith("|")) row = row.slice(1);
684
+ if (row.endsWith("|") && !row.endsWith("\\|")) row = row.slice(0, -1);
685
+ const cells = [];
686
+ let cell = "";
687
+ let inCode = false;
688
+ for (let i = 0; i < row.length; i += 1) {
689
+ const ch = row[i];
690
+ if (ch === "\\" && row[i + 1] === "|") {
691
+ cell += "|";
692
+ i += 1;
693
+ continue;
694
+ }
695
+ if (ch === "`") inCode = !inCode;
696
+ if (ch === "|" && !inCode) {
697
+ cells.push(cell.trim());
698
+ cell = "";
699
+ continue;
700
+ }
701
+ cell += ch;
819
702
  }
703
+ cells.push(cell.trim());
704
+ return cells;
820
705
  }
821
- var signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
822
- var prepared = /* @__PURE__ */ new WeakSet();
823
- function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8, shiftMainAxis = true, capHeight = false, onAnchorHidden }) {
824
- const opts = (0, import_react3.useRef)({ placement, offset, padding, shiftMainAxis, capHeight });
825
- opts.current = { placement, offset, padding, shiftMainAxis, capHeight };
826
- const onAnchorHiddenRef = (0, import_react3.useRef)(onAnchorHidden);
827
- onAnchorHiddenRef.current = onAnchorHidden;
828
- const heightCap = (0, import_react3.useRef)(null);
829
- const session = (0, import_react3.useRef)(false);
830
- const side = (0, import_react3.useRef)(null);
831
- const written = (0, import_react3.useRef)("");
832
- const anchoring = (0, import_react3.useRef)(null);
833
- const anchoringFailed = (0, import_react3.useRef)(false);
834
- const anchorName = (0, import_react3.useRef)("");
835
- const release = (0, import_react3.useCallback)((keepPlace) => {
836
- const current = anchoring.current;
837
- if (!current) return;
838
- anchoring.current = null;
839
- written.current = "";
840
- const floating = floatingRef.current;
841
- if (floating) {
842
- const s = floating.style;
843
- if (keepPlace && current.anchor.isConnected) {
844
- const a = current.anchor.getBoundingClientRect();
845
- s.top = `${Math.round((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy)}px`;
846
- s.left = `${Math.round((current.xEdge === "right" ? a.right : a.left) + current.dx)}px`;
706
+ var alignOf = (cell) => {
707
+ const c = cell.trim();
708
+ const left = c.startsWith(":");
709
+ const right = c.endsWith(":");
710
+ return left && right ? "center" : right ? "right" : left ? "left" : null;
711
+ };
712
+ var matchListLine = (line) => {
713
+ const bullet = BULLET_RE.exec(line);
714
+ if (bullet && !RULE_RE.test(line.trim())) return { indent: indentOf(bullet[1]), ordered: false, number: 1, text: bullet[3] };
715
+ const ordered = ORDERED_RE.exec(line);
716
+ if (ordered) return { indent: indentOf(ordered[1]), ordered: true, number: Number(ordered[2]), text: ordered[3] };
717
+ return null;
718
+ };
719
+ var toItem = (text) => {
720
+ const task = TASK_RE.exec(text);
721
+ return task ? { checked: task[1] !== " ", children: parseInline(task[2]), lists: [] } : { checked: null, children: parseInline(text), lists: [] };
722
+ };
723
+ var buildList = (lines) => {
724
+ const lists = [];
725
+ const stack = [];
726
+ for (const line of lines) {
727
+ while (stack.length && line.indent < stack[stack.length - 1].indent) stack.pop();
728
+ let top = stack[stack.length - 1];
729
+ if (top && line.indent > top.indent) {
730
+ const parentItem = top.list.items[top.list.items.length - 1];
731
+ const nested = { type: "list", ordered: line.ordered, start: line.number, items: [] };
732
+ parentItem.lists.push(nested);
733
+ stack.push({ list: nested, indent: line.indent });
734
+ top = stack[stack.length - 1];
735
+ } else if (!top || top.list.ordered !== line.ordered) {
736
+ const list = { type: "list", ordered: line.ordered, start: line.number, items: [] };
737
+ if (stack.length <= 1) {
738
+ lists.push(list);
739
+ stack.length = 0;
740
+ } else {
741
+ stack.pop();
742
+ const parent = stack[stack.length - 1].list;
743
+ parent.items[parent.items.length - 1].lists.push(list);
847
744
  }
848
- s.removeProperty("position-anchor");
745
+ stack.push({ list, indent: line.indent });
746
+ top = stack[stack.length - 1];
849
747
  }
850
- removeAnchorName(current.anchor, current.name);
851
- }, [floatingRef]);
852
- const uncap = (0, import_react3.useCallback)(() => {
853
- const cap = heightCap.current;
854
- if (!cap) return;
855
- heightCap.current = null;
856
- const floating = floatingRef.current;
857
- if (!floating) return;
858
- floating.style.maxHeight = cap.inlineMaxHeight;
859
- if (cap.setOverflow) floating.style.overflowY = cap.inlineOverflowY;
860
- }, [floatingRef]);
861
- const reposition = (0, import_react3.useCallback)(() => {
862
- const anchorEl = resolveAnchor(anchor);
863
- const floating = floatingRef.current;
864
- if (!anchorEl || !floating) return;
865
- const s = floating.style;
866
- if (!prepared.has(floating)) {
867
- s.position = "fixed";
868
- s.inset = "auto";
869
- s.margin = "0";
870
- prepared.add(floating);
871
- written.current = "";
748
+ top.list.items.push(toItem(line.text));
749
+ }
750
+ return lists;
751
+ };
752
+ function parseMarkdown(source) {
753
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
754
+ const blocks = [];
755
+ let paragraph = [];
756
+ let i = 0;
757
+ const flush = () => {
758
+ if (paragraph.length) {
759
+ blocks.push({ type: "paragraph", children: parseInline(paragraph.join("\n")) });
760
+ paragraph = [];
872
761
  }
873
- const { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, capHeight: cap } = opts.current;
874
- if (cap) {
875
- if (!heightCap.current) {
876
- const own = getComputedStyle(floating);
877
- const setOverflow = !own.overflowY || own.overflowY === "visible";
878
- heightCap.current = {
879
- inlineMaxHeight: s.maxHeight,
880
- inlineOverflowY: s.overflowY,
881
- ownMaxHeight: own.maxHeight && own.maxHeight !== "none" ? own.maxHeight : "",
882
- setOverflow,
883
- written: ""
884
- };
885
- if (setOverflow) s.overflowY = "auto";
886
- }
887
- const room = `${Math.max(0, window.innerHeight - 2 * pad)}px`;
888
- const max = heightCap.current.ownMaxHeight ? `min(${room}, ${heightCap.current.ownMaxHeight})` : room;
889
- if (max !== heightCap.current.written) {
890
- s.maxHeight = max;
891
- heightCap.current.written = max;
762
+ };
763
+ while (i < lines.length) {
764
+ const line = lines[i];
765
+ const trimmed = line.trim();
766
+ if (trimmed === "") {
767
+ flush();
768
+ i += 1;
769
+ continue;
770
+ }
771
+ const fence = FENCE_RE.exec(trimmed);
772
+ if (fence) {
773
+ flush();
774
+ const marker = fence[1];
775
+ const body = [];
776
+ i += 1;
777
+ while (i < lines.length && !lines[i].trim().startsWith(marker)) {
778
+ body.push(lines[i]);
779
+ i += 1;
892
780
  }
893
- } else if (heightCap.current) {
894
- uncap();
781
+ i += 1;
782
+ blocks.push({ type: "code", lang: fence[2] ?? "", value: body.join("\n") });
783
+ continue;
895
784
  }
896
- const a = anchorEl.getBoundingClientRect();
897
- const size = { width: floating.offsetWidth, height: floating.offsetHeight };
898
- const pos = computePosition(
899
- { x: a.x, y: a.y, width: a.width, height: a.height },
900
- size,
901
- { width: window.innerWidth, height: window.innerHeight },
902
- { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
903
- );
904
- const measured = size.width > 0 || size.height > 0;
905
- if (measured && session.current) side.current = pos.side;
906
- if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
907
- const writeFixed = (at) => {
908
- const left2 = `${Math.round(at.x)}px`;
909
- const top2 = `${Math.round(at.y)}px`;
910
- const key2 = `fixed ${left2} ${top2}`;
911
- if (key2 === written.current) return;
912
- s.left = left2;
913
- s.top = top2;
914
- written.current = key2;
915
- };
916
- const limit = pos.mainAxisRange;
917
- const nearLimit = limit !== null && (pos.mainAxisNatural < limit[0] + ANCHOR_BAND || pos.mainAxisNatural > limit[1] - ANCHOR_BAND);
918
- if (anchoringFailed.current || nearLimit || !supportsAnchoring()) {
919
- if (anchoring.current) release(false);
920
- writeFixed(pos);
921
- return;
785
+ const heading = HEADING_RE.exec(trimmed);
786
+ if (heading) {
787
+ flush();
788
+ blocks.push({ type: "heading", level: heading[1].length, children: parseInline(heading[2]) });
789
+ i += 1;
790
+ continue;
922
791
  }
923
- if (anchoring.current && anchoring.current.anchor !== anchorEl) release(false);
924
- if (!anchoring.current) {
925
- anchorName.current || (anchorName.current = `--uix-anchor-${++anchorCount}`);
926
- addAnchorName(anchorEl, anchorName.current);
927
- s.setProperty("position-anchor", anchorName.current);
928
- anchoring.current = { anchor: anchorEl, name: anchorName.current, yEdge: "bottom", xEdge: "left", dy: 0, dx: 0, verified: false };
929
- written.current = "";
792
+ if (RULE_RE.test(trimmed)) {
793
+ flush();
794
+ blocks.push({ type: "rule" });
795
+ i += 1;
796
+ continue;
930
797
  }
931
- const current = anchoring.current;
932
- current.yEdge = pos.side === "bottom" ? "bottom" : "top";
933
- current.xEdge = pos.side === "right" ? "right" : "left";
934
- current.dy = pos.y - (current.yEdge === "bottom" ? a.bottom : a.top);
935
- current.dx = pos.x - (current.xEdge === "right" ? a.right : a.left);
936
- const top = `round(anchor(${current.yEdge}) ${signed(current.dy)}, 1px)`;
937
- const left = `round(anchor(${current.xEdge}) ${signed(current.dx)}, 1px)`;
938
- const key = `anchored ${top} ${left}`;
939
- if (key !== written.current) {
940
- s.top = top;
941
- s.left = left;
942
- written.current = key;
798
+ if (trimmed.startsWith(">")) {
799
+ flush();
800
+ const quote = [];
801
+ while (i < lines.length && lines[i].trim().startsWith(">")) {
802
+ quote.push(lines[i].trim().replace(/^>\s?/, ""));
803
+ i += 1;
804
+ }
805
+ blocks.push({ type: "quote", children: parseInline(quote.join(" ")) });
806
+ continue;
943
807
  }
944
- if (!current.verified && measured) {
945
- current.verified = true;
946
- const box = floating.getBoundingClientRect();
947
- const t = translationOf(floating);
948
- const dxc = box.left + box.width / 2 - t.x - (pos.x + size.width / 2);
949
- const dyc = box.top + box.height / 2 - t.y - (pos.y + size.height / 2);
950
- if (Math.abs(dxc) > 2 || Math.abs(dyc) > 2) {
951
- anchoringFailed.current = true;
952
- release(false);
953
- writeFixed(pos);
808
+ if (trimmed.includes("|") && i + 1 < lines.length && DELIMITER_ROW_RE.test(lines[i + 1]) && lines[i + 1].includes("-")) {
809
+ const head = splitTableRow(line);
810
+ const delimiters = splitTableRow(lines[i + 1]);
811
+ if (delimiters.length === head.length) {
812
+ flush();
813
+ const rows = [];
814
+ i += 2;
815
+ while (i < lines.length && lines[i].trim() !== "" && lines[i].includes("|")) {
816
+ const cells = splitTableRow(lines[i]);
817
+ rows.push(head.map((_, c) => parseInline(cells[c] ?? "")));
818
+ i += 1;
819
+ }
820
+ blocks.push({ type: "table", align: delimiters.map(alignOf), head: head.map((c) => parseInline(c)), rows });
821
+ continue;
954
822
  }
955
823
  }
956
- }, [anchor, floatingRef, release, uncap]);
957
- useIsomorphicLayoutEffect(() => {
958
- if (!open) return;
959
- session.current = true;
960
- reposition();
961
- const anchorEl = resolveAnchor(anchor);
962
- const floating = floatingRef.current;
963
- const onScroll = (event) => {
964
- const target = event.target;
965
- if (target && typeof target.contains === "function") {
966
- if (floating?.contains(target)) return;
967
- if (anchorEl && !target.contains(anchorEl)) return;
824
+ const first = matchListLine(line);
825
+ if (first) {
826
+ flush();
827
+ const listLines = [];
828
+ while (i < lines.length) {
829
+ const current = lines[i];
830
+ const item = matchListLine(current);
831
+ if (item) {
832
+ if (listLines.length && item.indent <= listLines[0].indent && item.ordered !== listLines[0].ordered) break;
833
+ listLines.push(item);
834
+ } else if (current.trim() !== "" && indentOf(current) > listLines[0].indent && listLines.length) {
835
+ listLines[listLines.length - 1].text += `
836
+ ${current.trim()}`;
837
+ } else {
838
+ break;
839
+ }
840
+ i += 1;
968
841
  }
969
- reposition();
970
- };
971
- const onResize = () => reposition();
972
- window.addEventListener("scroll", onScroll, { passive: true, capture: true });
973
- window.addEventListener("resize", onResize);
974
- const observer = typeof ResizeObserver === "function" ? new ResizeObserver(() => reposition()) : null;
975
- if (floating) observer?.observe(floating);
976
- if (anchorEl) observer?.observe(anchorEl);
977
- return () => {
978
- window.removeEventListener("scroll", onScroll, true);
979
- window.removeEventListener("resize", onResize);
980
- observer?.disconnect();
981
- session.current = false;
982
- side.current = null;
983
- anchoringFailed.current = false;
984
- release(true);
985
- uncap();
986
- };
987
- }, [open, reposition, anchor, floatingRef, release, uncap]);
988
- const watchAnchor = open && !!onAnchorHidden;
989
- (0, import_react3.useEffect)(() => {
990
- const anchorEl = resolveAnchor(anchor);
991
- if (!watchAnchor || !anchorEl || typeof IntersectionObserver !== "function") return;
992
- const observer = new IntersectionObserver((entries) => {
993
- const latest = entries[entries.length - 1];
994
- if (latest && !latest.isIntersecting) onAnchorHiddenRef.current?.();
995
- });
996
- observer.observe(anchorEl);
997
- return () => observer.disconnect();
998
- }, [watchAnchor, anchor]);
999
- (0, import_react3.useEffect)(() => () => release(false), [release]);
1000
- return reposition;
842
+ const base = Math.min(...listLines.map((l) => l.indent));
843
+ blocks.push(...buildList(listLines.map((l) => ({ ...l, indent: l.indent - base }))));
844
+ continue;
845
+ }
846
+ paragraph.push(trimmed);
847
+ i += 1;
848
+ }
849
+ flush();
850
+ return blocks;
1001
851
  }
1002
852
 
1003
- // src/components/Popover.tsx
1004
- var import_jsx_runtime4 = require("react/jsx-runtime");
1005
- var useIsomorphicLayoutEffect2 = typeof document !== "undefined" ? import_react4.useLayoutEffect : import_react4.useEffect;
1006
- var PLACING = "data-uix-placing";
1007
- var isPopoverOpen = (el) => {
1008
- try {
1009
- return el.matches(":popover-open");
1010
- } catch {
1011
- return false;
1012
- }
1013
- };
1014
- var settle = (el) => {
1015
- if (!el.hasAttribute(PLACING)) return;
1016
- getComputedStyle(el).getPropertyValue("transform");
1017
- el.removeAttribute(PLACING);
1018
- };
1019
- function Popover({
1020
- popover: popoverProp,
1021
- anchor,
1022
- placement = "bottom-start",
1023
- offset = 6,
1024
- closeWhenAnchorHidden = false,
1025
- onAnchorHidden,
1026
- capHeight = false,
1027
- openOnHover = false,
1028
- className,
1029
- children,
1030
- ...props
1031
- }) {
1032
- const hover = !!openOnHover && !!anchor;
1033
- const popover = popoverProp ?? (hover ? "manual" : "auto");
1034
- const openDelay = typeof openOnHover === "object" ? openOnHover.openDelay ?? 300 : 300;
1035
- const closeDelay = typeof openOnHover === "object" ? openOnHover.closeDelay ?? 150 : 150;
1036
- const ref = (0, import_react4.useRef)(null);
1037
- const [open, setOpen] = (0, import_react4.useState)(false);
1038
- const openRef = (0, import_react4.useRef)(false);
1039
- const onAnchorHiddenRef = (0, import_react4.useRef)(onAnchorHidden);
1040
- onAnchorHiddenRef.current = onAnchorHidden;
1041
- const popoverAttr = { popover };
1042
- const reposition = useAnchoredPosition(anchor ?? null, ref, {
1043
- open: open && !!anchor,
1044
- placement,
1045
- offset,
1046
- capHeight,
1047
- onAnchorHidden: closeWhenAnchorHidden ? () => {
1048
- const el = ref.current;
1049
- if (!el || !isPopoverOpen(el)) return;
1050
- onAnchorHiddenRef.current?.();
1051
- const active = el.ownerDocument.activeElement;
1052
- if (active && el.contains(active)) active.blur();
1053
- el.hidePopover();
1054
- } : void 0
1055
- });
1056
- (0, import_react4.useEffect)(() => {
1057
- const el = ref.current;
1058
- if (!el || !anchor) return;
1059
- const hold = (event) => {
1060
- if (event.newState !== "open") return;
1061
- el.setAttribute(PLACING, "");
1062
- window.setTimeout(() => settle(el), 150);
1063
- };
1064
- const sync = () => {
1065
- if (!isPopoverOpen(el)) {
1066
- el.removeAttribute(PLACING);
1067
- setOpen(false);
1068
- return;
1069
- }
1070
- reposition();
1071
- window.setTimeout(() => {
1072
- if (!isPopoverOpen(el)) return;
1073
- if (!openRef.current) {
1074
- setOpen(true);
1075
- return;
1076
- }
1077
- reposition();
1078
- settle(el);
1079
- }, 0);
1080
- };
1081
- el.addEventListener("beforetoggle", hold);
1082
- el.addEventListener("toggle", sync);
1083
- return () => {
1084
- el.removeEventListener("beforetoggle", hold);
1085
- el.removeEventListener("toggle", sync);
1086
- el.removeAttribute(PLACING);
1087
- };
1088
- }, [anchor, reposition]);
1089
- (0, import_react4.useEffect)(() => {
1090
- const el = ref.current;
1091
- const anchorEl = resolveAnchor(anchor);
1092
- if (!hover || !el || !anchorEl) return;
1093
- let timer;
1094
- const inside = (node) => !!node && (anchorEl.contains(node) || el.contains(node));
1095
- const stillWanted = () => {
1096
- try {
1097
- return anchorEl.matches(":hover") || el.matches(":hover") || inside(el.ownerDocument.activeElement);
1098
- } catch {
1099
- return false;
1100
- }
1101
- };
1102
- const show = (delay) => {
1103
- window.clearTimeout(timer);
1104
- timer = window.setTimeout(() => {
1105
- if (!isPopoverOpen(el)) {
1106
- try {
1107
- el.showPopover?.();
1108
- } catch {
1109
- }
1110
- }
1111
- }, delay);
1112
- };
1113
- const hide = (delay) => {
1114
- window.clearTimeout(timer);
1115
- timer = window.setTimeout(() => {
1116
- if (isPopoverOpen(el) && !stillWanted()) {
1117
- try {
1118
- el.hidePopover?.();
1119
- } catch {
1120
- }
1121
- }
1122
- }, delay);
853
+ // src/rich-text/mention.ts
854
+ var Mention = import_core.Node.create({
855
+ name: "mention",
856
+ group: "inline",
857
+ inline: true,
858
+ atom: true,
859
+ selectable: false,
860
+ addAttributes() {
861
+ return {
862
+ id: { default: null },
863
+ label: { default: null }
1123
864
  };
1124
- const onEnter = (event) => {
1125
- if (event.pointerType !== "touch") show(openDelay);
1126
- };
1127
- const onLeave = (event) => {
1128
- if (event.pointerType !== "touch") hide(closeDelay);
1129
- };
1130
- const onFocusIn = () => show(0);
1131
- const onFocusOut = (event) => {
1132
- if (!inside(event.relatedTarget)) hide(closeDelay);
1133
- };
1134
- const onKeyDown = (event) => {
1135
- if (event.key !== "Escape" || !isPopoverOpen(el)) return;
1136
- window.clearTimeout(timer);
1137
- const hadFocus = el.contains(el.ownerDocument.activeElement);
1138
- try {
1139
- el.hidePopover?.();
1140
- } catch {
865
+ },
866
+ parseHTML() {
867
+ return [{
868
+ tag: "span[data-mention-id]",
869
+ getAttrs: (el) => {
870
+ const node = el;
871
+ const id = node.getAttribute("data-mention-id") ?? "";
872
+ const label = node.getAttribute("data-mention-label") ?? node.textContent?.replace(/^@/, "") ?? "";
873
+ return formatMention({ id, label }) ? { id, label } : false;
1141
874
  }
1142
- if (hadFocus) {
1143
- anchorEl.removeEventListener("focusin", onFocusIn);
1144
- anchorEl.focus();
1145
- anchorEl.addEventListener("focusin", onFocusIn);
1146
- }
1147
- };
1148
- for (const node of [anchorEl, el]) {
1149
- node.addEventListener("pointerenter", onEnter);
1150
- node.addEventListener("pointerleave", onLeave);
1151
- node.addEventListener("focusout", onFocusOut);
1152
- node.addEventListener("keydown", onKeyDown);
1153
- }
1154
- anchorEl.addEventListener("focusin", onFocusIn);
1155
- return () => {
1156
- window.clearTimeout(timer);
1157
- for (const node of [anchorEl, el]) {
1158
- node.removeEventListener("pointerenter", onEnter);
1159
- node.removeEventListener("pointerleave", onLeave);
1160
- node.removeEventListener("focusout", onFocusOut);
1161
- node.removeEventListener("keydown", onKeyDown);
1162
- }
1163
- anchorEl.removeEventListener("focusin", onFocusIn);
1164
- };
1165
- }, [hover, anchor, openDelay, closeDelay]);
1166
- useIsomorphicLayoutEffect2(() => {
1167
- openRef.current = open;
1168
- if (open && ref.current) settle(ref.current);
1169
- }, [open]);
1170
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
1171
- }
875
+ }];
876
+ },
877
+ renderHTML({ node, HTMLAttributes }) {
878
+ const label = String(node.attrs.label ?? "");
879
+ return [
880
+ "span",
881
+ (0, import_core.mergeAttributes)(HTMLAttributes, {
882
+ class: "uix-mention",
883
+ "data-mention-id": String(node.attrs.id ?? ""),
884
+ "data-mention-label": label,
885
+ contenteditable: "false"
886
+ }),
887
+ `@${label}`
888
+ ];
889
+ },
890
+ renderText({ node }) {
891
+ return `@${String(node.attrs.label ?? "")}`;
892
+ },
893
+ markdownTokenizer: {
894
+ name: "mention",
895
+ level: "inline",
896
+ start: (src) => src.indexOf("@["),
897
+ tokenize: (src) => {
898
+ const found = readMention(src);
899
+ if (!found) return void 0;
900
+ return { type: "mention", raw: src.slice(0, found.length), id: found.id, label: found.label };
901
+ }
902
+ },
903
+ parseMarkdown: (token, helpers) => helpers.createNode("mention", { id: String(token.id ?? ""), label: String(token.label ?? "") }),
904
+ renderMarkdown: (node) => formatMention({ id: String(node.attrs?.id ?? ""), label: String(node.attrs?.label ?? "") }) ?? `@${String(node.attrs?.label ?? "")}`
905
+ });
1172
906
 
1173
- // src/components/EmojiPicker.tsx
1174
- var import_jsx_runtime5 = require("react/jsx-runtime");
1175
- var DEFAULT_EMOJI_PICKER_LABELS = {
1176
- dialog: "Choose an emoji",
1177
- search: "Search emoji",
1178
- recent: "Recently used",
1179
- quickPicks: "Frequently used",
1180
- categories: "Emoji categories",
1181
- noResults: "No emoji found",
1182
- loading: "Loading emoji\u2026",
1183
- loadFailed: "Emoji could not be loaded."
1184
- };
1185
- var EMOJI_GRID_COLUMNS = 8;
1186
- var useIsomorphicLayoutEffect3 = typeof document !== "undefined" ? import_react5.useLayoutEffect : import_react5.useEffect;
1187
- function revealInGrid(grid, cell) {
1188
- const g = grid.getBoundingClientRect();
1189
- const c = cell.getBoundingClientRect();
1190
- const top = g.top + (parseFloat(getComputedStyle(grid).scrollPaddingTop) || 0);
1191
- if (c.top < top) grid.scrollTop -= top - c.top;
1192
- else if (c.bottom > g.bottom) grid.scrollTop += c.bottom - g.bottom;
1193
- }
1194
- function EmojiPicker({
1195
- onSelect,
1196
- trigger,
1197
- labels: labelOverrides,
1198
- locale = "en",
1199
- quickPicks,
1200
- loadData = loadEmojiData,
1201
- recentStorageKey = DEFAULT_RECENT_KEY,
1202
- placement = "bottom-start",
1203
- onOpenChange,
1204
- className,
1205
- emojiImageBaseUrl
1206
- }) {
1207
- const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
1208
- const labels = { ...DEFAULT_EMOJI_PICKER_LABELS, ...labelOverrides };
1209
- const id = (0, import_react5.useId)();
1210
- const popoverId = `${id}-picker`;
1211
- const anchorRef = (0, import_react5.useRef)(null);
1212
- const searchRef = (0, import_react5.useRef)(null);
1213
- const gridRef = (0, import_react5.useRef)(null);
1214
- const pointerWasOpen = (0, import_react5.useRef)(null);
1215
- const closedByAnchor = (0, import_react5.useRef)(false);
1216
- const pendingJump = (0, import_react5.useRef)(null);
1217
- const [open, setOpen] = (0, import_react5.useState)(false);
1218
- const [data, setData] = (0, import_react5.useState)(null);
1219
- const [failed, setFailed] = (0, import_react5.useState)(false);
1220
- const [query, setQuery] = (0, import_react5.useState)("");
1221
- const [recent, setRecent] = (0, import_react5.useState)([]);
1222
- const [active, setActive] = (0, import_react5.useState)(0);
1223
- const popoverEl = () => typeof document === "undefined" ? null : document.getElementById(popoverId);
1224
- const isOpen = (el) => {
1225
- try {
1226
- return !!el?.matches(":popover-open");
1227
- } catch {
1228
- return false;
1229
- }
1230
- };
1231
- (0, import_react5.useEffect)(() => {
1232
- const el = popoverEl();
1233
- if (!el) return;
1234
- const onToggle = (event) => {
1235
- const { newState } = event;
1236
- const nowOpen = newState ? newState === "open" : isOpen(el);
1237
- setOpen(nowOpen);
1238
- onOpenChange?.(nowOpen);
1239
- if (nowOpen) {
1240
- closedByAnchor.current = false;
1241
- pendingJump.current = null;
1242
- setRecent(readRecentEmoji(recentStorageKey));
1243
- setQuery("");
1244
- setActive(0);
1245
- requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
1246
- } else if (closedByAnchor.current) {
1247
- closedByAnchor.current = false;
1248
- } else if (el.contains(document.activeElement) || document.activeElement === document.body) {
1249
- focusTrigger();
1250
- }
1251
- };
1252
- el.addEventListener("toggle", onToggle);
1253
- return () => el.removeEventListener("toggle", onToggle);
1254
- }, [popoverId, recentStorageKey, onOpenChange]);
1255
- (0, import_react5.useEffect)(() => {
1256
- if (!open || data?.locale === locale) return;
1257
- let live = true;
1258
- setFailed(false);
1259
- loadData(locale).then(
1260
- (loaded) => {
1261
- if (live) setData(loaded);
1262
- },
1263
- () => {
1264
- if (live) setFailed(true);
1265
- }
1266
- );
1267
- return () => {
1268
- live = false;
1269
- };
1270
- }, [open, locale, loadData, data?.locale]);
1271
- const focusTrigger = () => {
1272
- const el = anchorRef.current?.querySelector("button, [href], [tabindex]");
1273
- el?.focus();
1274
- };
1275
- const hide = () => {
1276
- const el = popoverEl();
1277
- if (el && isOpen(el)) el.hidePopover();
1278
- };
1279
- const browseSections = (0, import_react5.useMemo)(() => {
1280
- const list = [];
1281
- if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
1282
- if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
1283
- if (data) {
1284
- data.groups.forEach((group, index) => {
1285
- list.push({ key: group.key, title: group.name, emojis: data.emojis.filter((e) => e.group === index).map((e) => e.emoji) });
1286
- });
1287
- }
1288
- return list;
1289
- }, [data, quickPicks, recent, labels.quickPicks, labels.recent]);
1290
- const sections = (0, import_react5.useMemo)(() => {
1291
- if (!query.trim()) return browseSections;
1292
- return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
1293
- }, [query, data, browseSections, labels.search]);
1294
- const flat = (0, import_react5.useMemo)(() => sections.flatMap((s) => s.emojis), [sections]);
1295
- const sectionStart = (0, import_react5.useMemo)(() => {
1296
- const starts = /* @__PURE__ */ new Map();
1297
- let at = 0;
1298
- for (const section of sections) {
1299
- starts.set(section.key, at);
1300
- at += section.emojis.length;
1301
- }
1302
- return starts;
1303
- }, [sections]);
1304
- const activeIndex = Math.min(active, Math.max(flat.length - 1, 0));
1305
- const choose = (emoji) => {
1306
- setRecent(pushRecentEmoji(emoji, recentStorageKey));
1307
- onSelect(emoji);
1308
- hide();
1309
- };
1310
- const focusCell = (0, import_react5.useCallback)((index, reveal = true) => {
1311
- setActive(index);
1312
- const find = () => gridRef.current?.querySelector(`[data-emoji-index="${index}"]`);
1313
- const move = (cell2) => {
1314
- cell2.focus({ preventScroll: true });
1315
- if (reveal && gridRef.current) revealInGrid(gridRef.current, cell2);
1316
- };
1317
- const cell = find();
1318
- if (cell) move(cell);
1319
- else requestAnimationFrame(() => {
1320
- const later = find();
1321
- if (later) move(later);
1322
- });
1323
- }, []);
1324
- const showSection = (key) => {
1325
- const grid = gridRef.current;
1326
- const section = grid?.querySelector(`[data-section="${key}"]`);
1327
- if (!grid || !section) return;
1328
- grid.scrollTop += section.getBoundingClientRect().top - grid.getBoundingClientRect().top;
1329
- const start = sectionStart.get(key);
1330
- if (start !== void 0) focusCell(start, false);
1331
- };
1332
- const onCategory = (key) => {
1333
- if (!query.trim()) {
1334
- showSection(key);
1335
- return;
1336
- }
1337
- pendingJump.current = key;
1338
- setQuery("");
1339
- setActive(0);
1340
- };
1341
- useIsomorphicLayoutEffect3(() => {
1342
- const key = pendingJump.current;
1343
- if (key === null || query.trim()) return;
1344
- pendingJump.current = null;
1345
- showSection(key);
1346
- }, [query, sections]);
1347
- const onGridKeyDown = (event) => {
1348
- const target = event.target;
1349
- const index = Number(target.dataset.emojiIndex ?? -1);
1350
- if (index < 0) return;
1351
- const next = emojiGridMove(index, event.key, flat.length, EMOJI_GRID_COLUMNS);
1352
- if (event.key === "ArrowUp" && next === null) {
1353
- event.preventDefault();
1354
- searchRef.current?.focus({ preventScroll: true });
1355
- return;
1356
- }
1357
- if (next === null) return;
1358
- event.preventDefault();
1359
- focusCell(next);
1360
- };
1361
- const onSearchKeyDown = (event) => {
1362
- if (event.key === "Escape") {
1363
- event.preventDefault();
1364
- hide();
1365
- } else if (event.key === "ArrowDown" && flat.length) {
1366
- event.preventDefault();
1367
- focusCell(0);
1368
- } else if (event.key === "Enter" && query.trim() && flat[0]) {
1369
- event.preventDefault();
1370
- choose(flat[0]);
1371
- }
1372
- };
1373
- const toggleFromTrigger = (event) => {
1374
- const el = popoverEl();
1375
- if (!el || event.defaultPrevented) return;
1376
- const was = pointerWasOpen.current;
1377
- pointerWasOpen.current = null;
1378
- if (was === true) return;
1379
- if (isOpen(el)) el.hidePopover();
1380
- else el.showPopover();
1381
- };
1382
- const triggerProps = (0, import_react5.isValidElement)(trigger) ? trigger.props : {};
1383
- const triggerEl = (0, import_react5.isValidElement)(trigger) ? (0, import_react5.cloneElement)(trigger, {
1384
- "aria-haspopup": "dialog",
1385
- "aria-expanded": open,
1386
- "aria-controls": popoverId,
1387
- onPointerDown: (event) => {
1388
- pointerWasOpen.current = isOpen(popoverEl());
1389
- triggerProps.onPointerDown?.(event);
1390
- },
1391
- onClick: (event) => {
1392
- triggerProps.onClick?.(event);
1393
- toggleFromTrigger(event);
1394
- }
1395
- }) : trigger;
1396
- const names = data?.names;
1397
- let offset = 0;
1398
- const status = failed ? labels.loadFailed : !data && open ? labels.loading : query.trim() && data && flat.length === 0 ? labels.noResults : "";
1399
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { ref: anchorRef, className: cx("uix-emoji-picker__anchor", className), children: [
1400
- triggerEl,
1401
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1402
- Popover,
1403
- {
1404
- id: popoverId,
1405
- anchor: anchorRef,
1406
- placement,
1407
- className: "uix-emoji-picker-popover",
1408
- role: "dialog",
1409
- "aria-label": labels.dialog,
1410
- closeWhenAnchorHidden: true,
1411
- onAnchorHidden: () => {
1412
- closedByAnchor.current = true;
1413
- },
1414
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uix-emoji-picker uix-emoji-picker--full", lang: locale, children: [
1415
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1416
- "input",
1417
- {
1418
- ref: searchRef,
1419
- type: "search",
1420
- className: "uix-input uix-emoji-picker__search",
1421
- "aria-label": labels.search,
1422
- placeholder: labels.search,
1423
- value: query,
1424
- onChange: (event) => {
1425
- setQuery(event.target.value);
1426
- setActive(0);
1427
- },
1428
- onKeyDown: onSearchKeyDown,
1429
- autoComplete: "off",
1430
- spellCheck: false
1431
- }
1432
- ),
1433
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1434
- "div",
1435
- {
1436
- className: "uix-emoji-picker__nav",
1437
- role: data ? "group" : void 0,
1438
- "aria-label": data ? labels.categories : void 0,
1439
- children: data ? browseSections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1440
- "button",
1441
- {
1442
- type: "button",
1443
- className: "uix-emoji-picker__nav-btn",
1444
- "aria-label": section.title,
1445
- title: section.title,
1446
- onClick: () => onCategory(section.key),
1447
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
1448
- },
1449
- section.key
1450
- )) : null
1451
- }
1452
- ),
1453
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: [
1454
- sections.map((section) => {
1455
- const start = offset;
1456
- offset += section.emojis.length;
1457
- if (!section.emojis.length) return null;
1458
- const headingId = `${id}-${section.key}`;
1459
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
1460
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
1461
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
1462
- const index = start + i;
1463
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1464
- "button",
1465
- {
1466
- type: "button",
1467
- className: "uix-emoji-picker__btn",
1468
- "data-emoji-index": index,
1469
- tabIndex: index === activeIndex ? 0 : -1,
1470
- "aria-label": names?.get(emoji) ?? emoji,
1471
- title: names?.get(emoji),
1472
- onClick: () => choose(emoji),
1473
- onFocus: () => setActive(index),
1474
- children: renderEmoji(emoji)
1475
- },
1476
- `${section.key}-${emoji}`
1477
- );
1478
- }) })
1479
- ] }, section.key);
1480
- }),
1481
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "uix-emoji-picker__status", role: "status", children: status })
1482
- ] })
1483
- ] }) : null
907
+ // src/rich-text/pipeline.ts
908
+ var indentContinuation = (h) => {
909
+ let first = true;
910
+ return {
911
+ ...h,
912
+ renderChildren: (nodes) => {
913
+ const out = h.renderChildren(nodes);
914
+ if (!first) return out;
915
+ first = false;
916
+ return out.split("\n").map((line, n) => n > 0 && line ? h.indent(line) : line).join("\n");
917
+ }
918
+ };
919
+ };
920
+ var listItemRender = import_extension_list.ListItem.config.renderMarkdown;
921
+ var UixListItem = import_extension_list.ListItem.extend({
922
+ renderMarkdown: (node, h, ctx) => listItemRender(node, indentContinuation(h), ctx)
923
+ });
924
+ var UixTaskItem = import_extension_list.TaskItem.extend({
925
+ renderMarkdown: (node, h) => {
926
+ const prefix = `- [${node.attrs?.checked ? "x" : " "}] `;
927
+ const [first, ...rest] = node.content ?? [];
928
+ if (first?.type !== "paragraph" || !(first.content ?? []).some((c) => c.type === "hardBreak")) {
929
+ return (0, import_core2.renderNestedMarkdownContent)(node, h, prefix);
930
+ }
931
+ const lines = [[]];
932
+ for (const child of first.content ?? []) {
933
+ if (child.type === "hardBreak") lines.push([]);
934
+ else lines[lines.length - 1].push(child);
935
+ }
936
+ const paragraphs = lines.filter((line) => line.length > 0).map((content) => ({ type: "paragraph", content }));
937
+ return (0, import_core2.renderNestedMarkdownContent)({ ...node, content: [...paragraphs, ...rest] }, h, prefix);
938
+ }
939
+ });
940
+ function createSchemaExtensions(options = {}) {
941
+ const isSafeUrl = options.isSafeUrl ?? defaultIsSafeUrl;
942
+ return [
943
+ import_starter_kit.StarterKit.configure({
944
+ underline: false,
945
+ listItem: false,
946
+ heading: { levels: [...options.headingLevels ?? [1, 2, 3]] },
947
+ link: {
948
+ openOnClick: false,
949
+ autolink: true,
950
+ linkOnPaste: true,
951
+ defaultProtocol: "https",
952
+ // Unsafe links never become marks: pasted or typed, they stay text.
953
+ isAllowedUri: (url) => isSafeUrl(url),
954
+ shouldAutoLink: (url) => isSafeUrl(url),
955
+ HTMLAttributes: { rel: "noopener noreferrer nofollow", target: null }
1484
956
  }
1485
- )
1486
- ] });
957
+ }),
958
+ UixListItem,
959
+ import_extension_list.TaskList,
960
+ UixTaskItem.configure({ nested: true }),
961
+ import_extension_table.TableKit.configure({ table: { resizable: false } }),
962
+ options.image ?? import_extension_image.Image.configure({ inline: true, allowBase64: false }),
963
+ // HAR-1599: `@[Name](mention:<id>)` is one token in every editor and headless pipeline.
964
+ Mention
965
+ ];
1487
966
  }
1488
-
1489
- // src/markdown-model.ts
967
+ var ENTITY_AFTER_AMP = /^(?:#\d+;|#x[\da-f]+;|[a-z][a-z\d]*;)/i;
968
+ var TAG_START = /^[a-z/!?]$/i;
1490
969
  var ASCII_PUNCT2 = /[!-/:-@[-`{-~]/;
1491
- var WORD = /[\p{L}\p{N}]/u;
1492
- var NAMED_ENTITIES = {
1493
- amp: "&",
1494
- lt: "<",
1495
- gt: ">",
1496
- quot: '"',
1497
- apos: "'",
1498
- nbsp: "\xA0",
1499
- copy: "\xA9",
1500
- reg: "\xAE",
1501
- hellip: "\u2026",
1502
- mdash: "\u2014",
1503
- ndash: "\u2013"
1504
- };
1505
- var ENTITY_RE = /^&(?:#(\d{1,7})|#[xX]([\da-fA-F]{1,6})|([a-zA-Z][a-zA-Z\d]{1,31}));/;
1506
- var ENTITY_GLOBAL = /&(?:#\d{1,7}|#[xX][\da-fA-F]{1,6}|[a-zA-Z][a-zA-Z\d]{1,31});/g;
1507
- var BARE_URL_RE = /^https?:\/\/[^\s<>()]+/;
1508
- var BR_RE = /^<br\s*\/?>/i;
1509
- var AUTOLINK_RE = /^<((?:https?:|mailto:)[^\s<>]*)>/i;
1510
- var decodeEntity = (match) => {
1511
- const [, dec, hex, name] = match;
1512
- const code = dec ? Number(dec) : hex ? parseInt(hex, 16) : void 0;
1513
- if (code !== void 0) return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : "\uFFFD";
1514
- return name && Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, name) ? NAMED_ENTITIES[name] : null;
1515
- };
1516
- var MAX_SCAN = 2e3;
1517
- var MAX_DEPTH = 12;
1518
- var findBracketEnd = (text, open) => {
1519
- let depth = 0;
1520
- const stop = Math.min(text.length, open + MAX_SCAN);
1521
- for (let i = open; i < stop; i += 1) {
1522
- const ch = text[i];
1523
- if (ch === "\\") {
1524
- i += 1;
970
+ var LIST_MARKER = /^(?:\d+|[ivxlcdmIVXLCDM]+|[a-zA-Z]{1,2})$/;
971
+ var BACKSLASH = String.fromCharCode(92);
972
+ function encodeText(text, options = {}) {
973
+ let out = "";
974
+ const chars = Array.from(text);
975
+ let lineStart = 0;
976
+ let inLead = options.lineStart ?? true;
977
+ for (let i = 0; i < chars.length; i += 1) {
978
+ const ch = chars[i];
979
+ const prev = chars[i - 1] ?? "";
980
+ const next = chars[i + 1] ?? "";
981
+ if (ch === "\n") {
982
+ out += ch;
983
+ lineStart = i + 1;
984
+ inLead = true;
1525
985
  continue;
1526
986
  }
1527
- if (ch === "`") {
1528
- const close = text.indexOf("`", i + 1);
1529
- if (close > 0) {
1530
- i = close;
1531
- continue;
987
+ if (inLead && (ch === " " || ch === " ")) {
988
+ continue;
989
+ }
990
+ const atStart = inLead;
991
+ inLead = false;
992
+ const lineRest = chars.slice(i).join("").split("\n")[0];
993
+ const endOrSpace = next === "" || next === "\n" || /\s/u.test(next);
994
+ const markerBefore = !atStart && LIST_MARKER.test(out.slice(out.lastIndexOf("\n") + 1));
995
+ if (
996
+ // `#` always: marked ends a list item at a line starting with `#`, space or not.
997
+ atStart && (ch === "#" || /[-+*]/.test(ch) && endOrSpace) || atStart && /[-=*_]/.test(ch) && new RegExp(`^(?:\\${ch}[ \\t]*)+$`).test(lineRest) || markerBefore && /[.)]/.test(ch) && endOrSpace
998
+ ) {
999
+ out += BACKSLASH + ch;
1000
+ continue;
1001
+ }
1002
+ if (ch === "#" && options.headingEnd && (prev === "" || /\s/u.test(prev)) && /^#+$/.test(lineRest)) {
1003
+ out += BACKSLASH + ch;
1004
+ continue;
1005
+ }
1006
+ switch (ch) {
1007
+ case BACKSLASH:
1008
+ out += ASCII_PUNCT2.test(next) || next === "" ? BACKSLASH + BACKSLASH : BACKSLASH;
1009
+ break;
1010
+ case "`":
1011
+ case "[":
1012
+ case "]":
1013
+ case "~":
1014
+ out += BACKSLASH + ch;
1015
+ break;
1016
+ case "|":
1017
+ out += options.inTable ? BACKSLASH + ch : ch;
1018
+ break;
1019
+ case "&":
1020
+ out += ENTITY_AFTER_AMP.test(chars.slice(i + 1, i + 40).join("")) ? "&amp;" : ch;
1021
+ break;
1022
+ case "<":
1023
+ out += TAG_START.test(next) ? "&lt;" : ch;
1024
+ break;
1025
+ case "!":
1026
+ out += next === "" && options.beforeLink ? BACKSLASH + ch : ch;
1027
+ break;
1028
+ case "*":
1029
+ case "_": {
1030
+ const spaced = (prev === "" || /\s/u.test(prev)) && (next === "" || /\s/u.test(next));
1031
+ const intraword = ch === "_" && /[\p{L}\p{N}]/u.test(prev) && /[\p{L}\p{N}]/u.test(next);
1032
+ out += spaced || intraword ? ch : BACKSLASH + ch;
1033
+ break;
1532
1034
  }
1035
+ case ">":
1036
+ out += atStart ? BACKSLASH + ch : ch;
1037
+ break;
1038
+ default:
1039
+ out += ch;
1533
1040
  }
1534
- if (ch === "[") depth += 1;
1535
- else if (ch === "]") {
1536
- depth -= 1;
1537
- if (depth === 0) return i;
1041
+ }
1042
+ return out;
1043
+ }
1044
+ function createMarked() {
1045
+ return new import_marked.Marked({
1046
+ gfm: true,
1047
+ tokenizer: {
1048
+ html: () => void 0,
1049
+ // `<br>` is the one tag read: GFM tables have no other way to hold a line break,
1050
+ // and Tiptap writes hard breaks in cells that way.
1051
+ tag: (src) => {
1052
+ const br = /^<br\s*\/?>/i.exec(src);
1053
+ return br ? { type: "br", raw: br[0] } : void 0;
1054
+ }
1055
+ }
1056
+ });
1057
+ }
1058
+ var hasLink = (node) => (node?.marks ?? []).some((m) => (typeof m === "string" ? m : m.type) === "link");
1059
+ function patchManager(manager) {
1060
+ const internals = manager;
1061
+ internals.encodeTextForMarkdown = function encode(text, node, parentNode) {
1062
+ const inCode = parentNode?.type != null && internals.codeTypes.has(parentNode.type) || (node.marks ?? []).some((m) => internals.codeTypes.has(typeof m === "string" ? m : m.type));
1063
+ if (inCode) return text;
1064
+ const siblings = parentNode?.content ?? [];
1065
+ const index = siblings.indexOf(node);
1066
+ const previous = index > 0 ? siblings[index - 1] : void 0;
1067
+ const next = index >= 0 ? siblings[index + 1] : void 0;
1068
+ return encodeText(text, {
1069
+ inTable: !!internals.uixInTable,
1070
+ beforeLink: hasLink(next) && !hasLink(node),
1071
+ // Unknown position (node not found among its siblings): assume a line start, the safe side.
1072
+ lineStart: index <= 0 || previous?.type === "hardBreak",
1073
+ headingEnd: parentNode?.type === "heading" && (index < 0 || index === siblings.length - 1)
1074
+ });
1075
+ };
1076
+ return manager;
1077
+ }
1078
+ function serializeNode(manager, node) {
1079
+ const internals = manager;
1080
+ internals.uixInTable = node.type === "table";
1081
+ try {
1082
+ return manager.serialize({ type: "doc", content: [node] }).replace(/^\n+|\n+$/g, "");
1083
+ } finally {
1084
+ internals.uixInTable = false;
1085
+ }
1086
+ }
1087
+ function createManager(extensions) {
1088
+ return patchManager(new import_markdown.MarkdownManager({ marked: createMarked(), extensions }));
1089
+ }
1090
+ var SEPARATOR_TOKENS = /* @__PURE__ */ new Set(["space", "def"]);
1091
+ var nodeKey = (node) => JSON.stringify(node);
1092
+ var normalize = (schema, content) => {
1093
+ if (content.length === 0) return [];
1094
+ const blocks = [];
1095
+ for (const node of content) {
1096
+ if (node.type && schema.nodes[node.type]?.isInline) {
1097
+ const last = blocks[blocks.length - 1];
1098
+ if (last?.type === "paragraph" && last.wrapped) last.content.push(node);
1099
+ else blocks.push({ type: "paragraph", content: [node], wrapped: true });
1100
+ } else {
1101
+ blocks.push(node);
1538
1102
  }
1539
1103
  }
1540
- return -1;
1104
+ for (const block of blocks) delete block.wrapped;
1105
+ const doc = schema.nodeFromJSON({ type: "doc", content: blocks });
1106
+ doc.check();
1107
+ return doc.toJSON().content ?? [];
1541
1108
  };
1542
- var readDestination = (text, open) => {
1543
- if (text[open] !== "(") return null;
1544
- let depth = 0;
1545
- let end = -1;
1546
- const stop = Math.min(text.length, open + MAX_SCAN);
1547
- for (let i = open; i < stop; i += 1) {
1548
- const ch = text[i];
1549
- if (ch === "\\") {
1550
- i += 1;
1551
- continue;
1109
+ function readMarkdown(source, manager, schema) {
1110
+ const normalized = source.replace(/\r\n?/g, "\n");
1111
+ const lexed = manager.instance.lexer(normalized);
1112
+ const origin = [];
1113
+ for (let i = 0, o = 0; i <= normalized.length; i += 1, o += 1) {
1114
+ origin.push(o);
1115
+ if (source[o] === "\r" && source[o + 1] === "\n") o += 1;
1116
+ }
1117
+ let cursor = 0;
1118
+ const tokens = lexed.map((t) => {
1119
+ const start = origin[cursor];
1120
+ cursor += t.raw.length;
1121
+ return { type: t.type, raw: source.slice(start, origin[cursor]) };
1122
+ });
1123
+ const joined = lexed.map((t) => t.raw).join("") === normalized ? tokens.map((t) => t.raw).join("") : null;
1124
+ const blocks = [];
1125
+ const eol = /\r\n/.test(source) && !/(^|[^\r])\n/.test(source) ? "\r\n" : "\n";
1126
+ let lead = "";
1127
+ let defs = [];
1128
+ const keepInLead = (raw) => {
1129
+ lead += raw;
1130
+ const text = raw.replace(/\s+$/u, "").replace(/^\s+/u, "");
1131
+ if (text) defs.push(text);
1132
+ };
1133
+ const pushBlock = (raw) => {
1134
+ let nodes;
1135
+ try {
1136
+ nodes = normalize(schema, manager.parse(raw).content ?? []);
1137
+ } catch {
1138
+ nodes = normalize(schema, [{ type: "paragraph", content: [{ type: "text", text: raw }] }]);
1552
1139
  }
1553
- if (ch === "(") depth += 1;
1554
- else if (ch === ")") {
1555
- depth -= 1;
1556
- if (depth === 0) {
1557
- end = i;
1558
- break;
1559
- }
1140
+ if (nodes.length === 0) {
1141
+ keepInLead(raw);
1142
+ return;
1560
1143
  }
1144
+ blocks.push({ raw, lead, nodes, keys: nodes.map(nodeKey), defs });
1145
+ lead = "";
1146
+ defs = [];
1147
+ };
1148
+ if (joined !== source) {
1149
+ const core = source.replace(/\s+$/u, "");
1150
+ lead = source.slice(0, source.length - source.trimStart().length);
1151
+ if (core.trim()) pushBlock(core.trimStart());
1152
+ const tail = source.slice(core.length);
1153
+ return { source, blocks, tail, tailDefs: [], eol, doc: { type: "doc", content: blocks.flatMap((b) => b.nodes) } };
1561
1154
  }
1562
- if (end < 0) return null;
1563
- const inner = text.slice(open + 1, end).trim();
1564
- const titled = /^(\S+)\s+(?:"([^"]*)"|'([^']*)')$/.exec(inner);
1565
- const rawUrl = titled ? titled[1] : inner;
1566
- const url = rawUrl.replace(/^<(.*)>$/, "$1").replace(/\\([!-/:-@[-`{-~])/g, "$1").replace(ENTITY_GLOBAL, (match) => decodeEntity(ENTITY_RE.exec(match)) ?? match);
1567
- const title = titled ? titled[2] ?? titled[3] : void 0;
1568
- return title === void 0 ? { url, end } : { url, title, end };
1569
- };
1570
- var backtickRun = (text, at) => {
1571
- let end = at;
1572
- while (text[end] === "`") end += 1;
1573
- return text.slice(at, end);
1574
- };
1575
- var findCloser = (text, from, delim, misses) => {
1576
- if (from >= (misses.get(delim) ?? Infinity)) return -1;
1577
- for (let i = from; i < text.length; i += 1) {
1578
- const ch = text[i];
1579
- if (ch === "\\") {
1580
- i += 1;
1155
+ for (const token of tokens) {
1156
+ if (SEPARATOR_TOKENS.has(token.type) || token.raw.trim() === "") {
1157
+ if (token.type === "def") keepInLead(token.raw);
1158
+ else lead += token.raw;
1581
1159
  continue;
1582
1160
  }
1583
- if (ch === "`") {
1584
- const run = backtickRun(text, i);
1585
- const close = text.indexOf(run, i + run.length);
1586
- if (close > 0) {
1587
- i = close + run.length - 1;
1588
- continue;
1589
- }
1590
- }
1591
- if (text.startsWith(delim, i) && i > from && !/\s/u.test(text[i - 1])) {
1592
- if (delim[0] === "_" && WORD.test(text[i + delim.length] ?? "")) continue;
1593
- if (delim.length === 1 && text[i + 1] === delim) {
1594
- i += 1;
1595
- continue;
1596
- }
1597
- return i;
1598
- }
1161
+ const leading = /^(?:[ \t]*\r?\n)*/.exec(token.raw)[0];
1162
+ lead += leading;
1163
+ const body = token.raw.slice(leading.length);
1164
+ const core = body.replace(/\s+$/u, "");
1165
+ pushBlock(core);
1166
+ lead += body.slice(core.length);
1599
1167
  }
1600
- misses.set(delim, Math.min(from, misses.get(delim) ?? Infinity));
1601
- return -1;
1602
- };
1603
- var pushText = (out, value) => {
1604
- if (!value) return;
1605
- const last = out[out.length - 1];
1606
- if (last?.type === "text") last.value += value;
1607
- else out.push({ type: "text", value });
1608
- };
1609
- function parseInline(text, depth = 0) {
1610
- const out = [];
1611
- if (depth > MAX_DEPTH) {
1612
- pushText(out, text);
1613
- return out;
1168
+ return {
1169
+ source,
1170
+ blocks,
1171
+ tail: lead,
1172
+ tailDefs: defs,
1173
+ eol,
1174
+ doc: { type: "doc", content: blocks.flatMap((b) => b.nodes) }
1175
+ };
1176
+ }
1177
+ var isEmptyParagraph = (node) => node.type === "paragraph" && !(node.content ?? []).some((c) => c.type !== "text" || (c.text ?? "").trim() !== "");
1178
+ var matchesAt = (children, at, block) => block.keys.every((key, k) => children[at + k] === key);
1179
+ var LOOKAHEAD = 8;
1180
+ function writeMarkdown(doc, origin, manager) {
1181
+ const content = [...doc.content ?? []];
1182
+ while (content.length > 0 && isEmptyParagraph(content[content.length - 1])) content.pop();
1183
+ if (content.length === 0) {
1184
+ return origin.blocks.length === 0 ? origin.source : "";
1614
1185
  }
1615
- const misses = /* @__PURE__ */ new Map();
1186
+ const keys = content.map(nodeKey);
1187
+ const { blocks, eol } = origin;
1188
+ const pieces = [];
1616
1189
  let i = 0;
1617
- while (i < text.length) {
1618
- const ch = text[i];
1619
- const rest = text.slice(i);
1620
- if (ch === "\\" && ASCII_PUNCT2.test(text[i + 1] ?? "")) {
1621
- pushText(out, text[i + 1]);
1622
- i += 2;
1190
+ let j = 0;
1191
+ while (i < content.length) {
1192
+ if (j < blocks.length && matchesAt(keys, i, blocks[j])) {
1193
+ pieces.push({ text: blocks[j].raw, slot: j, edited: false });
1194
+ i += blocks[j].keys.length;
1195
+ j += 1;
1623
1196
  continue;
1624
1197
  }
1625
- if (ch === "\n") {
1626
- out.push({ type: "br" });
1627
- i += 1;
1628
- continue;
1198
+ let skip = 0;
1199
+ for (let d = 1; d <= LOOKAHEAD && j + d < blocks.length; d += 1) {
1200
+ if (matchesAt(keys, i, blocks[j + d])) {
1201
+ skip = d;
1202
+ break;
1203
+ }
1629
1204
  }
1630
- if (ch === "`") {
1631
- const run = backtickRun(text, i);
1632
- const close = text.indexOf(run, i + run.length);
1633
- if (close > 0 && text[close + run.length] !== "`") {
1634
- let value = text.slice(i + run.length, close).replace(/\n/g, " ");
1635
- if (/^ .* $/.test(value) && value.trim()) value = value.slice(1, -1);
1636
- out.push({ type: "code", value });
1637
- i = close + run.length;
1638
- continue;
1205
+ let insert = 0;
1206
+ if (j < blocks.length) {
1207
+ for (let d = 1; d <= LOOKAHEAD && i + d < content.length; d += 1) {
1208
+ if (matchesAt(keys, i + d, blocks[j])) {
1209
+ insert = d;
1210
+ break;
1211
+ }
1639
1212
  }
1640
- pushText(out, run);
1641
- i += run.length;
1213
+ }
1214
+ if (skip && (!insert || skip <= insert)) {
1215
+ j += skip;
1642
1216
  continue;
1643
1217
  }
1644
- if (ch === "!" && text[i + 1] === "[") {
1645
- const end = findBracketEnd(text, i + 1);
1646
- const dest = end > 0 ? readDestination(text, end + 1) : null;
1647
- if (dest) {
1648
- const alt = plainText(parseInline(text.slice(i + 2, end), depth + 1));
1649
- out.push(dest.title === void 0 ? { type: "image", src: dest.url, alt } : { type: "image", src: dest.url, alt, title: dest.title });
1650
- i = dest.end + 1;
1651
- continue;
1218
+ let text = serializeNode(manager, content[i]);
1219
+ if (eol !== "\n") text = text.replace(/\n/g, eol);
1220
+ const inPlace = !insert && j < blocks.length && blocks[j].keys.length === 1;
1221
+ pieces.push(inPlace ? { text, slot: j, edited: true } : { text, edited: true });
1222
+ i += 1;
1223
+ if (!insert && j < blocks.length) j += 1;
1224
+ }
1225
+ const blank = eol + eol;
1226
+ const emitted = /* @__PURE__ */ new Set();
1227
+ let out = "";
1228
+ pieces.forEach((piece, n) => {
1229
+ const prev = pieces[n - 1];
1230
+ const slot = piece.slot;
1231
+ const lead = slot !== void 0 ? blocks[slot].lead : "";
1232
+ if (n === 0) {
1233
+ if (slot === 0) {
1234
+ out += lead;
1235
+ emitted.add(0);
1652
1236
  }
1237
+ } else if (slot !== void 0 && prev?.slot === slot - 1 && // Next to a re-serialized block, only a separator with a blank line is safe.
1238
+ (!(piece.edited || prev.edited) || /\n[ \t]*\r?\n/.test(lead))) {
1239
+ out += lead;
1240
+ emitted.add(slot);
1241
+ } else {
1242
+ out += blank;
1653
1243
  }
1654
- if (ch === "[") {
1655
- const end = findBracketEnd(text, i);
1656
- const dest = end > 0 ? readDestination(text, end + 1) : null;
1657
- if (dest) {
1658
- const children = parseInline(text.slice(i + 1, end), depth + 1);
1659
- out.push(dest.title === void 0 ? { type: "link", href: dest.url, children } : { type: "link", href: dest.url, title: dest.title, children });
1660
- i = dest.end + 1;
1661
- continue;
1662
- }
1244
+ out += piece.text;
1245
+ });
1246
+ const last = pieces[pieces.length - 1];
1247
+ const tailKept = last.slot === blocks.length - 1;
1248
+ const lost = [
1249
+ ...blocks.flatMap((block, n) => emitted.has(n) ? [] : block.defs),
1250
+ ...tailKept ? [] : origin.tailDefs
1251
+ ];
1252
+ if (lost.length) out += blank + lost.join(eol);
1253
+ if (tailKept) out += origin.tail;
1254
+ else if (/\n$/.test(origin.tail)) out += eol;
1255
+ return out;
1256
+ }
1257
+ function createPipeline(options = {}) {
1258
+ const extensions = createSchemaExtensions(options);
1259
+ const schema = (0, import_core2.getSchema)(extensions);
1260
+ const manager = createManager(extensions);
1261
+ return {
1262
+ extensions,
1263
+ schema,
1264
+ manager,
1265
+ read: (source) => readMarkdown(source, manager, schema),
1266
+ write: (doc, origin) => writeMarkdown(doc, origin, manager)
1267
+ };
1268
+ }
1269
+
1270
+ // src/components/Composer.tsx
1271
+ var import_react2 = require("react");
1272
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1273
+ function Composer({ children, className, label = "Add a comment", ...props }) {
1274
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx("uix-composer", className), ...props, children: import_react2.Children.map(children, (child) => {
1275
+ if (!(0, import_react2.isValidElement)(child) || child.type !== "textarea") return child;
1276
+ const p = child.props;
1277
+ return p["aria-label"] || p["aria-labelledby"] || p.id ? child : (0, import_react2.cloneElement)(child, { "aria-label": label });
1278
+ }) });
1279
+ }
1280
+ function ComposerBar({ children, className, ...props }) {
1281
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx("uix-composer__bar", className), ...props, children });
1282
+ }
1283
+
1284
+ // src/components/EditorIcons.tsx
1285
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1286
+ var PATHS = {
1287
+ heading1: "M4 12h8M4 18V6M12 18V6M17 12l3-2v8",
1288
+ heading2: "M4 12h8M4 18V6M12 18V6M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1",
1289
+ 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",
1290
+ 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",
1291
+ italic: "M19 4h-9M14 20H5M15 4 9 20",
1292
+ strike: "M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6M4 12h16",
1293
+ code: "m16 18 6-6-6-6M8 6l-6 6 6 6",
1294
+ bulletList: "M3 12h.01M3 18h.01M3 6h.01M8 12h13M8 18h13M8 6h13",
1295
+ orderedList: "M10 12h11M10 18h11M10 6h11M4 10h2M4 6h1v4M6 18H4c0-1 2-2 2-3s-1-1.5-2-1",
1296
+ taskList: "m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8",
1297
+ blockquote: "M17 6H3M21 12H8M21 18H8M3 12v6",
1298
+ 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",
1299
+ horizontalRule: "M5 12h14",
1300
+ 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",
1301
+ 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",
1302
+ 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",
1303
+ 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",
1304
+ 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",
1305
+ 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",
1306
+ 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",
1307
+ 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",
1308
+ 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",
1309
+ addReaction: "M22 11v1a10 10 0 1 1-9-10M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01M16 5h6M19 2v6",
1310
+ undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
1311
+ redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13",
1312
+ 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",
1313
+ 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",
1314
+ clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM12 6v6l4 2",
1315
+ grip: "M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01",
1316
+ more: "M5 12h.01M12 12h.01M19 12h.01"
1317
+ };
1318
+ function EditorIcon({ name, ...props }) {
1319
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1320
+ "svg",
1321
+ {
1322
+ viewBox: "0 0 24 24",
1323
+ fill: "none",
1324
+ stroke: "currentColor",
1325
+ strokeWidth: 2,
1326
+ strokeLinecap: "round",
1327
+ strokeLinejoin: "round",
1328
+ "aria-hidden": "true",
1329
+ focusable: "false",
1330
+ ...props,
1331
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: PATHS[name] })
1663
1332
  }
1664
- if (ch === "<") {
1665
- const br = BR_RE.exec(rest);
1666
- if (br) {
1667
- out.push({ type: "br" });
1668
- i += br[0].length;
1669
- continue;
1670
- }
1671
- const auto = AUTOLINK_RE.exec(rest);
1672
- if (auto) {
1673
- out.push({ type: "link", href: auto[1], children: [{ type: "text", value: auto[1].replace(/^mailto:/i, "") }], bare: true });
1674
- i += auto[0].length;
1675
- continue;
1676
- }
1333
+ );
1334
+ }
1335
+
1336
+ // src/components/EmojiPicker.tsx
1337
+ var import_react5 = require("react");
1338
+
1339
+ // src/components/Popover.tsx
1340
+ var import_react4 = require("react");
1341
+
1342
+ // src/hooks/useAnchoredPosition.ts
1343
+ var import_react3 = require("react");
1344
+ var useIsomorphicLayoutEffect = typeof document !== "undefined" ? import_react3.useLayoutEffect : import_react3.useEffect;
1345
+ var resolveAnchor = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
1346
+ var anchoringSupported;
1347
+ function supportsAnchoring() {
1348
+ if (anchoringSupported === void 0) {
1349
+ try {
1350
+ const css = typeof CSS !== "undefined" ? CSS : void 0;
1351
+ anchoringSupported = !!css && typeof css.supports === "function" && css.supports("anchor-name", "--uix-a") && css.supports("position-anchor", "--uix-a") && css.supports("top", "round(anchor(bottom) + 1px, 1px)");
1352
+ } catch {
1353
+ anchoringSupported = false;
1677
1354
  }
1678
- if (ch === "&") {
1679
- const entity = ENTITY_RE.exec(rest);
1680
- const decoded = entity ? decodeEntity(entity) : null;
1681
- if (entity && decoded !== null) {
1682
- pushText(out, decoded);
1683
- i += entity[0].length;
1684
- continue;
1355
+ }
1356
+ return anchoringSupported;
1357
+ }
1358
+ var ANCHOR_BAND = 64;
1359
+ var anchorCount = 0;
1360
+ var namesOf = (value) => value.split(",").map((n) => n.trim()).filter((n) => n.startsWith("--"));
1361
+ function addAnchorName(el, name) {
1362
+ const names = namesOf(getComputedStyle(el).getPropertyValue("anchor-name"));
1363
+ if (!names.includes(name)) names.push(name);
1364
+ el.style.setProperty("anchor-name", names.join(", "));
1365
+ }
1366
+ function removeAnchorName(el, name) {
1367
+ const names = namesOf(el.style.getPropertyValue("anchor-name")).filter((n) => n !== name);
1368
+ if (names.length) el.style.setProperty("anchor-name", names.join(", "));
1369
+ else el.style.removeProperty("anchor-name");
1370
+ }
1371
+ function translationOf(el) {
1372
+ const t = getComputedStyle(el).transform;
1373
+ if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
1374
+ try {
1375
+ const m = new DOMMatrixReadOnly(t);
1376
+ return { x: m.m41, y: m.m42 };
1377
+ } catch {
1378
+ return { x: 0, y: 0 };
1379
+ }
1380
+ }
1381
+ var signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
1382
+ var prepared = /* @__PURE__ */ new WeakSet();
1383
+ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8, shiftMainAxis = true, capHeight = false, onAnchorHidden }) {
1384
+ const opts = (0, import_react3.useRef)({ placement, offset, padding, shiftMainAxis, capHeight });
1385
+ opts.current = { placement, offset, padding, shiftMainAxis, capHeight };
1386
+ const onAnchorHiddenRef = (0, import_react3.useRef)(onAnchorHidden);
1387
+ onAnchorHiddenRef.current = onAnchorHidden;
1388
+ const heightCap = (0, import_react3.useRef)(null);
1389
+ const session = (0, import_react3.useRef)(false);
1390
+ const side = (0, import_react3.useRef)(null);
1391
+ const written = (0, import_react3.useRef)("");
1392
+ const anchoring = (0, import_react3.useRef)(null);
1393
+ const anchoringFailed = (0, import_react3.useRef)(false);
1394
+ const anchorName = (0, import_react3.useRef)("");
1395
+ const release = (0, import_react3.useCallback)((keepPlace) => {
1396
+ const current = anchoring.current;
1397
+ if (!current) return;
1398
+ anchoring.current = null;
1399
+ written.current = "";
1400
+ const floating = floatingRef.current;
1401
+ if (floating) {
1402
+ const s = floating.style;
1403
+ if (keepPlace && current.anchor.isConnected) {
1404
+ const a = current.anchor.getBoundingClientRect();
1405
+ s.top = `${Math.round((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy)}px`;
1406
+ s.left = `${Math.round((current.xEdge === "right" ? a.right : a.left) + current.dx)}px`;
1685
1407
  }
1408
+ s.removeProperty("position-anchor");
1686
1409
  }
1687
- if (ch === "~" && text[i + 1] === "~" && text[i + 2] && !/\s/u.test(text[i + 2])) {
1688
- const close = findCloser(text, i + 2, "~~", misses);
1689
- if (close > 0) {
1690
- out.push({ type: "del", children: parseInline(text.slice(i + 2, close), depth + 1) });
1691
- i = close + 2;
1692
- continue;
1693
- }
1410
+ removeAnchorName(current.anchor, current.name);
1411
+ }, [floatingRef]);
1412
+ const uncap = (0, import_react3.useCallback)(() => {
1413
+ const cap = heightCap.current;
1414
+ if (!cap) return;
1415
+ heightCap.current = null;
1416
+ const floating = floatingRef.current;
1417
+ if (!floating) return;
1418
+ floating.style.maxHeight = cap.inlineMaxHeight;
1419
+ if (cap.setOverflow) floating.style.overflowY = cap.inlineOverflowY;
1420
+ }, [floatingRef]);
1421
+ const reposition = (0, import_react3.useCallback)(() => {
1422
+ const anchorEl = resolveAnchor(anchor);
1423
+ const floating = floatingRef.current;
1424
+ if (!anchorEl || !floating) return;
1425
+ const s = floating.style;
1426
+ if (!prepared.has(floating)) {
1427
+ s.position = "fixed";
1428
+ s.inset = "auto";
1429
+ s.margin = "0";
1430
+ prepared.add(floating);
1431
+ written.current = "";
1694
1432
  }
1695
- if (ch === "*" || ch === "_") {
1696
- const leftOk = ch === "*" || !WORD.test(text[i - 1] ?? "");
1697
- const double = text[i + 1] === ch;
1698
- const delim = double ? ch + ch : ch;
1699
- const after = text[i + delim.length];
1700
- if (leftOk && after && !/\s/u.test(after)) {
1701
- const close = findCloser(text, i + delim.length, delim, misses);
1702
- if (close > 0) {
1703
- const children = parseInline(text.slice(i + delim.length, close), depth + 1);
1704
- out.push(double ? { type: "strong", children } : { type: "em", children });
1705
- i = close + delim.length;
1706
- continue;
1707
- }
1433
+ const { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, capHeight: cap } = opts.current;
1434
+ if (cap) {
1435
+ if (!heightCap.current) {
1436
+ const own = getComputedStyle(floating);
1437
+ const setOverflow = !own.overflowY || own.overflowY === "visible";
1438
+ heightCap.current = {
1439
+ inlineMaxHeight: s.maxHeight,
1440
+ inlineOverflowY: s.overflowY,
1441
+ ownMaxHeight: own.maxHeight && own.maxHeight !== "none" ? own.maxHeight : "",
1442
+ setOverflow,
1443
+ written: ""
1444
+ };
1445
+ if (setOverflow) s.overflowY = "auto";
1708
1446
  }
1709
- const run = new RegExp(`^\\${ch}+`).exec(rest)[0];
1710
- pushText(out, run);
1711
- i += run.length;
1712
- continue;
1713
- }
1714
- if (ch === "h" && !WORD.test(text[i - 1] ?? "")) {
1715
- const bare = BARE_URL_RE.exec(rest);
1716
- if (bare) {
1717
- const url = bare[0].replace(/[.,;:!?'"]+$/, "");
1718
- out.push({ type: "link", href: url, children: [{ type: "text", value: url }], bare: true });
1719
- i += url.length;
1720
- continue;
1447
+ const room = `${Math.max(0, window.innerHeight - 2 * pad)}px`;
1448
+ const max = heightCap.current.ownMaxHeight ? `min(${room}, ${heightCap.current.ownMaxHeight})` : room;
1449
+ if (max !== heightCap.current.written) {
1450
+ s.maxHeight = max;
1451
+ heightCap.current.written = max;
1721
1452
  }
1453
+ } else if (heightCap.current) {
1454
+ uncap();
1722
1455
  }
1723
- pushText(out, ch);
1724
- i += 1;
1725
- }
1726
- return out;
1727
- }
1728
- function plainText(nodes) {
1729
- return nodes.map((n) => {
1730
- switch (n.type) {
1731
- case "text":
1732
- case "code":
1733
- return n.value;
1734
- case "br":
1735
- return " ";
1736
- case "image":
1737
- return n.alt;
1738
- default:
1739
- return plainText(n.children);
1740
- }
1741
- }).join("");
1742
- }
1743
- var FENCE_RE = /^(`{3,}|~{3,})\s*([^`\s]*)/;
1744
- var HEADING_RE = /^(#{1,6})\s+(.*?)(?:\s+#+)?$/;
1745
- var RULE_RE = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
1746
- var BULLET_RE = /^(\s*)([-*+])\s+(.*)$/;
1747
- var ORDERED_RE = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
1748
- var TASK_RE = /^\[([ xX])\]\s+(.*)$/;
1749
- var DELIMITER_ROW_RE = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/;
1750
- var indentOf = (s) => s.replace(/\t/g, " ").length;
1751
- function splitTableRow(line) {
1752
- let row = line.trim();
1753
- if (row.startsWith("|")) row = row.slice(1);
1754
- if (row.endsWith("|") && !row.endsWith("\\|")) row = row.slice(0, -1);
1755
- const cells = [];
1756
- let cell = "";
1757
- let inCode = false;
1758
- for (let i = 0; i < row.length; i += 1) {
1759
- const ch = row[i];
1760
- if (ch === "\\" && row[i + 1] === "|") {
1761
- cell += "|";
1762
- i += 1;
1763
- continue;
1456
+ const a = anchorEl.getBoundingClientRect();
1457
+ const size = { width: floating.offsetWidth, height: floating.offsetHeight };
1458
+ const pos = computePosition(
1459
+ { x: a.x, y: a.y, width: a.width, height: a.height },
1460
+ size,
1461
+ { width: window.innerWidth, height: window.innerHeight },
1462
+ { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
1463
+ );
1464
+ const measured = size.width > 0 || size.height > 0;
1465
+ if (measured && session.current) side.current = pos.side;
1466
+ if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
1467
+ const writeFixed = (at) => {
1468
+ const left2 = `${Math.round(at.x)}px`;
1469
+ const top2 = `${Math.round(at.y)}px`;
1470
+ const key2 = `fixed ${left2} ${top2}`;
1471
+ if (key2 === written.current) return;
1472
+ s.left = left2;
1473
+ s.top = top2;
1474
+ written.current = key2;
1475
+ };
1476
+ const limit = pos.mainAxisRange;
1477
+ const nearLimit = limit !== null && (pos.mainAxisNatural < limit[0] + ANCHOR_BAND || pos.mainAxisNatural > limit[1] - ANCHOR_BAND);
1478
+ if (anchoringFailed.current || nearLimit || !supportsAnchoring()) {
1479
+ if (anchoring.current) release(false);
1480
+ writeFixed(pos);
1481
+ return;
1764
1482
  }
1765
- if (ch === "`") inCode = !inCode;
1766
- if (ch === "|" && !inCode) {
1767
- cells.push(cell.trim());
1768
- cell = "";
1769
- continue;
1483
+ if (anchoring.current && anchoring.current.anchor !== anchorEl) release(false);
1484
+ if (!anchoring.current) {
1485
+ anchorName.current || (anchorName.current = `--uix-anchor-${++anchorCount}`);
1486
+ addAnchorName(anchorEl, anchorName.current);
1487
+ s.setProperty("position-anchor", anchorName.current);
1488
+ anchoring.current = { anchor: anchorEl, name: anchorName.current, yEdge: "bottom", xEdge: "left", dy: 0, dx: 0, verified: false };
1489
+ written.current = "";
1770
1490
  }
1771
- cell += ch;
1772
- }
1773
- cells.push(cell.trim());
1774
- return cells;
1491
+ const current = anchoring.current;
1492
+ current.yEdge = pos.side === "bottom" ? "bottom" : "top";
1493
+ current.xEdge = pos.side === "right" ? "right" : "left";
1494
+ current.dy = pos.y - (current.yEdge === "bottom" ? a.bottom : a.top);
1495
+ current.dx = pos.x - (current.xEdge === "right" ? a.right : a.left);
1496
+ const top = `round(anchor(${current.yEdge}) ${signed(current.dy)}, 1px)`;
1497
+ const left = `round(anchor(${current.xEdge}) ${signed(current.dx)}, 1px)`;
1498
+ const key = `anchored ${top} ${left}`;
1499
+ if (key !== written.current) {
1500
+ s.top = top;
1501
+ s.left = left;
1502
+ written.current = key;
1503
+ }
1504
+ if (!current.verified && measured) {
1505
+ current.verified = true;
1506
+ const box = floating.getBoundingClientRect();
1507
+ const t = translationOf(floating);
1508
+ const dxc = box.left + box.width / 2 - t.x - (pos.x + size.width / 2);
1509
+ const dyc = box.top + box.height / 2 - t.y - (pos.y + size.height / 2);
1510
+ if (Math.abs(dxc) > 2 || Math.abs(dyc) > 2) {
1511
+ anchoringFailed.current = true;
1512
+ release(false);
1513
+ writeFixed(pos);
1514
+ }
1515
+ }
1516
+ }, [anchor, floatingRef, release, uncap]);
1517
+ useIsomorphicLayoutEffect(() => {
1518
+ if (!open) return;
1519
+ session.current = true;
1520
+ reposition();
1521
+ const anchorEl = resolveAnchor(anchor);
1522
+ const floating = floatingRef.current;
1523
+ const onScroll = (event) => {
1524
+ const target = event.target;
1525
+ if (target && typeof target.contains === "function") {
1526
+ if (floating?.contains(target)) return;
1527
+ if (anchorEl && !target.contains(anchorEl)) return;
1528
+ }
1529
+ reposition();
1530
+ };
1531
+ const onResize = () => reposition();
1532
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
1533
+ window.addEventListener("resize", onResize);
1534
+ const observer = typeof ResizeObserver === "function" ? new ResizeObserver(() => reposition()) : null;
1535
+ if (floating) observer?.observe(floating);
1536
+ if (anchorEl) observer?.observe(anchorEl);
1537
+ return () => {
1538
+ window.removeEventListener("scroll", onScroll, true);
1539
+ window.removeEventListener("resize", onResize);
1540
+ observer?.disconnect();
1541
+ session.current = false;
1542
+ side.current = null;
1543
+ anchoringFailed.current = false;
1544
+ release(true);
1545
+ uncap();
1546
+ };
1547
+ }, [open, reposition, anchor, floatingRef, release, uncap]);
1548
+ const watchAnchor = open && !!onAnchorHidden;
1549
+ (0, import_react3.useEffect)(() => {
1550
+ const anchorEl = resolveAnchor(anchor);
1551
+ if (!watchAnchor || !anchorEl || typeof IntersectionObserver !== "function") return;
1552
+ const observer = new IntersectionObserver((entries) => {
1553
+ const latest = entries[entries.length - 1];
1554
+ if (latest && !latest.isIntersecting) onAnchorHiddenRef.current?.();
1555
+ });
1556
+ observer.observe(anchorEl);
1557
+ return () => observer.disconnect();
1558
+ }, [watchAnchor, anchor]);
1559
+ (0, import_react3.useEffect)(() => () => release(false), [release]);
1560
+ return reposition;
1775
1561
  }
1776
- var alignOf = (cell) => {
1777
- const c = cell.trim();
1778
- const left = c.startsWith(":");
1779
- const right = c.endsWith(":");
1780
- return left && right ? "center" : right ? "right" : left ? "left" : null;
1781
- };
1782
- var matchListLine = (line) => {
1783
- const bullet = BULLET_RE.exec(line);
1784
- if (bullet && !RULE_RE.test(line.trim())) return { indent: indentOf(bullet[1]), ordered: false, number: 1, text: bullet[3] };
1785
- const ordered = ORDERED_RE.exec(line);
1786
- if (ordered) return { indent: indentOf(ordered[1]), ordered: true, number: Number(ordered[2]), text: ordered[3] };
1787
- return null;
1562
+
1563
+ // src/components/Popover.tsx
1564
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1565
+ var useIsomorphicLayoutEffect2 = typeof document !== "undefined" ? import_react4.useLayoutEffect : import_react4.useEffect;
1566
+ var PLACING = "data-uix-placing";
1567
+ var isPopoverOpen = (el) => {
1568
+ try {
1569
+ return el.matches(":popover-open");
1570
+ } catch {
1571
+ return false;
1572
+ }
1788
1573
  };
1789
- var toItem = (text) => {
1790
- const task = TASK_RE.exec(text);
1791
- return task ? { checked: task[1] !== " ", children: parseInline(task[2]), lists: [] } : { checked: null, children: parseInline(text), lists: [] };
1574
+ var settle = (el) => {
1575
+ if (!el.hasAttribute(PLACING)) return;
1576
+ getComputedStyle(el).getPropertyValue("transform");
1577
+ el.removeAttribute(PLACING);
1792
1578
  };
1793
- var buildList = (lines) => {
1794
- const lists = [];
1795
- const stack = [];
1796
- for (const line of lines) {
1797
- while (stack.length && line.indent < stack[stack.length - 1].indent) stack.pop();
1798
- let top = stack[stack.length - 1];
1799
- if (top && line.indent > top.indent) {
1800
- const parentItem = top.list.items[top.list.items.length - 1];
1801
- const nested = { type: "list", ordered: line.ordered, start: line.number, items: [] };
1802
- parentItem.lists.push(nested);
1803
- stack.push({ list: nested, indent: line.indent });
1804
- top = stack[stack.length - 1];
1805
- } else if (!top || top.list.ordered !== line.ordered) {
1806
- const list = { type: "list", ordered: line.ordered, start: line.number, items: [] };
1807
- if (stack.length <= 1) {
1808
- lists.push(list);
1809
- stack.length = 0;
1810
- } else {
1811
- stack.pop();
1812
- const parent = stack[stack.length - 1].list;
1813
- parent.items[parent.items.length - 1].lists.push(list);
1579
+ function Popover({
1580
+ popover: popoverProp,
1581
+ anchor,
1582
+ placement = "bottom-start",
1583
+ offset = 6,
1584
+ closeWhenAnchorHidden = false,
1585
+ onAnchorHidden,
1586
+ capHeight = false,
1587
+ openOnHover = false,
1588
+ className,
1589
+ children,
1590
+ ...props
1591
+ }) {
1592
+ const hover = !!openOnHover && !!anchor;
1593
+ const popover = popoverProp ?? (hover ? "manual" : "auto");
1594
+ const openDelay = typeof openOnHover === "object" ? openOnHover.openDelay ?? 300 : 300;
1595
+ const closeDelay = typeof openOnHover === "object" ? openOnHover.closeDelay ?? 150 : 150;
1596
+ const ref = (0, import_react4.useRef)(null);
1597
+ const [open, setOpen] = (0, import_react4.useState)(false);
1598
+ const openRef = (0, import_react4.useRef)(false);
1599
+ const onAnchorHiddenRef = (0, import_react4.useRef)(onAnchorHidden);
1600
+ onAnchorHiddenRef.current = onAnchorHidden;
1601
+ const popoverAttr = { popover };
1602
+ const reposition = useAnchoredPosition(anchor ?? null, ref, {
1603
+ open: open && !!anchor,
1604
+ placement,
1605
+ offset,
1606
+ capHeight,
1607
+ onAnchorHidden: closeWhenAnchorHidden ? () => {
1608
+ const el = ref.current;
1609
+ if (!el || !isPopoverOpen(el)) return;
1610
+ onAnchorHiddenRef.current?.();
1611
+ const active = el.ownerDocument.activeElement;
1612
+ if (active && el.contains(active)) active.blur();
1613
+ el.hidePopover();
1614
+ } : void 0
1615
+ });
1616
+ (0, import_react4.useEffect)(() => {
1617
+ const el = ref.current;
1618
+ if (!el || !anchor) return;
1619
+ const hold = (event) => {
1620
+ if (event.newState !== "open") return;
1621
+ el.setAttribute(PLACING, "");
1622
+ window.setTimeout(() => settle(el), 150);
1623
+ };
1624
+ const sync = () => {
1625
+ if (!isPopoverOpen(el)) {
1626
+ el.removeAttribute(PLACING);
1627
+ setOpen(false);
1628
+ return;
1629
+ }
1630
+ reposition();
1631
+ window.setTimeout(() => {
1632
+ if (!isPopoverOpen(el)) return;
1633
+ if (!openRef.current) {
1634
+ setOpen(true);
1635
+ return;
1636
+ }
1637
+ reposition();
1638
+ settle(el);
1639
+ }, 0);
1640
+ };
1641
+ el.addEventListener("beforetoggle", hold);
1642
+ el.addEventListener("toggle", sync);
1643
+ return () => {
1644
+ el.removeEventListener("beforetoggle", hold);
1645
+ el.removeEventListener("toggle", sync);
1646
+ el.removeAttribute(PLACING);
1647
+ };
1648
+ }, [anchor, reposition]);
1649
+ (0, import_react4.useEffect)(() => {
1650
+ const el = ref.current;
1651
+ const anchorEl = resolveAnchor(anchor);
1652
+ if (!hover || !el || !anchorEl) return;
1653
+ let timer;
1654
+ const inside = (node) => !!node && (anchorEl.contains(node) || el.contains(node));
1655
+ const stillWanted = () => {
1656
+ try {
1657
+ return anchorEl.matches(":hover") || el.matches(":hover") || inside(el.ownerDocument.activeElement);
1658
+ } catch {
1659
+ return false;
1660
+ }
1661
+ };
1662
+ const show = (delay) => {
1663
+ window.clearTimeout(timer);
1664
+ timer = window.setTimeout(() => {
1665
+ if (!isPopoverOpen(el)) {
1666
+ try {
1667
+ el.showPopover?.();
1668
+ } catch {
1669
+ }
1670
+ }
1671
+ }, delay);
1672
+ };
1673
+ const hide = (delay) => {
1674
+ window.clearTimeout(timer);
1675
+ timer = window.setTimeout(() => {
1676
+ if (isPopoverOpen(el) && !stillWanted()) {
1677
+ try {
1678
+ el.hidePopover?.();
1679
+ } catch {
1680
+ }
1681
+ }
1682
+ }, delay);
1683
+ };
1684
+ const onEnter = (event) => {
1685
+ if (event.pointerType !== "touch") show(openDelay);
1686
+ };
1687
+ const onLeave = (event) => {
1688
+ if (event.pointerType !== "touch") hide(closeDelay);
1689
+ };
1690
+ const onFocusIn = () => show(0);
1691
+ const onFocusOut = (event) => {
1692
+ if (!inside(event.relatedTarget)) hide(closeDelay);
1693
+ };
1694
+ const onKeyDown = (event) => {
1695
+ if (event.key !== "Escape" || !isPopoverOpen(el)) return;
1696
+ window.clearTimeout(timer);
1697
+ const hadFocus = el.contains(el.ownerDocument.activeElement);
1698
+ try {
1699
+ el.hidePopover?.();
1700
+ } catch {
1814
1701
  }
1815
- stack.push({ list, indent: line.indent });
1816
- top = stack[stack.length - 1];
1702
+ if (hadFocus) {
1703
+ anchorEl.removeEventListener("focusin", onFocusIn);
1704
+ anchorEl.focus();
1705
+ anchorEl.addEventListener("focusin", onFocusIn);
1706
+ }
1707
+ };
1708
+ for (const node of [anchorEl, el]) {
1709
+ node.addEventListener("pointerenter", onEnter);
1710
+ node.addEventListener("pointerleave", onLeave);
1711
+ node.addEventListener("focusout", onFocusOut);
1712
+ node.addEventListener("keydown", onKeyDown);
1817
1713
  }
1818
- top.list.items.push(toItem(line.text));
1819
- }
1820
- return lists;
1714
+ anchorEl.addEventListener("focusin", onFocusIn);
1715
+ return () => {
1716
+ window.clearTimeout(timer);
1717
+ for (const node of [anchorEl, el]) {
1718
+ node.removeEventListener("pointerenter", onEnter);
1719
+ node.removeEventListener("pointerleave", onLeave);
1720
+ node.removeEventListener("focusout", onFocusOut);
1721
+ node.removeEventListener("keydown", onKeyDown);
1722
+ }
1723
+ anchorEl.removeEventListener("focusin", onFocusIn);
1724
+ };
1725
+ }, [hover, anchor, openDelay, closeDelay]);
1726
+ useIsomorphicLayoutEffect2(() => {
1727
+ openRef.current = open;
1728
+ if (open && ref.current) settle(ref.current);
1729
+ }, [open]);
1730
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
1731
+ }
1732
+
1733
+ // src/components/EmojiPicker.tsx
1734
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1735
+ var DEFAULT_EMOJI_PICKER_LABELS = {
1736
+ dialog: "Choose an emoji",
1737
+ search: "Search emoji",
1738
+ recent: "Recently used",
1739
+ quickPicks: "Frequently used",
1740
+ categories: "Emoji categories",
1741
+ noResults: "No emoji found",
1742
+ loading: "Loading emoji\u2026",
1743
+ loadFailed: "Emoji could not be loaded."
1821
1744
  };
1822
- function parseMarkdown(source) {
1823
- const lines = source.replace(/\r\n?/g, "\n").split("\n");
1824
- const blocks = [];
1825
- let paragraph = [];
1826
- let i = 0;
1827
- const flush = () => {
1828
- if (paragraph.length) {
1829
- blocks.push({ type: "paragraph", children: parseInline(paragraph.join("\n")) });
1830
- paragraph = [];
1745
+ var EMOJI_GRID_COLUMNS = 8;
1746
+ var useIsomorphicLayoutEffect3 = typeof document !== "undefined" ? import_react5.useLayoutEffect : import_react5.useEffect;
1747
+ function revealInGrid(grid, cell) {
1748
+ const g = grid.getBoundingClientRect();
1749
+ const c = cell.getBoundingClientRect();
1750
+ const top = g.top + (parseFloat(getComputedStyle(grid).scrollPaddingTop) || 0);
1751
+ if (c.top < top) grid.scrollTop -= top - c.top;
1752
+ else if (c.bottom > g.bottom) grid.scrollTop += c.bottom - g.bottom;
1753
+ }
1754
+ function EmojiPicker({
1755
+ onSelect,
1756
+ trigger,
1757
+ labels: labelOverrides,
1758
+ locale = "en",
1759
+ quickPicks,
1760
+ loadData = loadEmojiData,
1761
+ recentStorageKey = DEFAULT_RECENT_KEY,
1762
+ placement = "bottom-start",
1763
+ onOpenChange,
1764
+ className,
1765
+ emojiImageBaseUrl
1766
+ }) {
1767
+ const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
1768
+ const labels = { ...DEFAULT_EMOJI_PICKER_LABELS, ...labelOverrides };
1769
+ const id = (0, import_react5.useId)();
1770
+ const popoverId = `${id}-picker`;
1771
+ const anchorRef = (0, import_react5.useRef)(null);
1772
+ const searchRef = (0, import_react5.useRef)(null);
1773
+ const gridRef = (0, import_react5.useRef)(null);
1774
+ const pointerWasOpen = (0, import_react5.useRef)(null);
1775
+ const closedByAnchor = (0, import_react5.useRef)(false);
1776
+ const pendingJump = (0, import_react5.useRef)(null);
1777
+ const [open, setOpen] = (0, import_react5.useState)(false);
1778
+ const [data, setData] = (0, import_react5.useState)(null);
1779
+ const [failed, setFailed] = (0, import_react5.useState)(false);
1780
+ const [query, setQuery] = (0, import_react5.useState)("");
1781
+ const [recent, setRecent] = (0, import_react5.useState)([]);
1782
+ const [active, setActive] = (0, import_react5.useState)(0);
1783
+ const popoverEl = () => typeof document === "undefined" ? null : document.getElementById(popoverId);
1784
+ const isOpen = (el) => {
1785
+ try {
1786
+ return !!el?.matches(":popover-open");
1787
+ } catch {
1788
+ return false;
1831
1789
  }
1832
1790
  };
1833
- while (i < lines.length) {
1834
- const line = lines[i];
1835
- const trimmed = line.trim();
1836
- if (trimmed === "") {
1837
- flush();
1838
- i += 1;
1839
- continue;
1840
- }
1841
- const fence = FENCE_RE.exec(trimmed);
1842
- if (fence) {
1843
- flush();
1844
- const marker = fence[1];
1845
- const body = [];
1846
- i += 1;
1847
- while (i < lines.length && !lines[i].trim().startsWith(marker)) {
1848
- body.push(lines[i]);
1849
- i += 1;
1791
+ (0, import_react5.useEffect)(() => {
1792
+ const el = popoverEl();
1793
+ if (!el) return;
1794
+ const onToggle = (event) => {
1795
+ const { newState } = event;
1796
+ const nowOpen = newState ? newState === "open" : isOpen(el);
1797
+ setOpen(nowOpen);
1798
+ onOpenChange?.(nowOpen);
1799
+ if (nowOpen) {
1800
+ closedByAnchor.current = false;
1801
+ pendingJump.current = null;
1802
+ setRecent(readRecentEmoji(recentStorageKey));
1803
+ setQuery("");
1804
+ setActive(0);
1805
+ requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
1806
+ } else if (closedByAnchor.current) {
1807
+ closedByAnchor.current = false;
1808
+ } else if (el.contains(document.activeElement) || document.activeElement === document.body) {
1809
+ focusTrigger();
1850
1810
  }
1851
- i += 1;
1852
- blocks.push({ type: "code", lang: fence[2] ?? "", value: body.join("\n") });
1853
- continue;
1811
+ };
1812
+ el.addEventListener("toggle", onToggle);
1813
+ return () => el.removeEventListener("toggle", onToggle);
1814
+ }, [popoverId, recentStorageKey, onOpenChange]);
1815
+ (0, import_react5.useEffect)(() => {
1816
+ if (!open || data?.locale === locale) return;
1817
+ let live = true;
1818
+ setFailed(false);
1819
+ loadData(locale).then(
1820
+ (loaded) => {
1821
+ if (live) setData(loaded);
1822
+ },
1823
+ () => {
1824
+ if (live) setFailed(true);
1825
+ }
1826
+ );
1827
+ return () => {
1828
+ live = false;
1829
+ };
1830
+ }, [open, locale, loadData, data?.locale]);
1831
+ const focusTrigger = () => {
1832
+ const el = anchorRef.current?.querySelector("button, [href], [tabindex]");
1833
+ el?.focus();
1834
+ };
1835
+ const hide = () => {
1836
+ const el = popoverEl();
1837
+ if (el && isOpen(el)) el.hidePopover();
1838
+ };
1839
+ const browseSections = (0, import_react5.useMemo)(() => {
1840
+ const list = [];
1841
+ if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
1842
+ if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
1843
+ if (data) {
1844
+ data.groups.forEach((group, index) => {
1845
+ list.push({ key: group.key, title: group.name, emojis: data.emojis.filter((e) => e.group === index).map((e) => e.emoji) });
1846
+ });
1854
1847
  }
1855
- const heading = HEADING_RE.exec(trimmed);
1856
- if (heading) {
1857
- flush();
1858
- blocks.push({ type: "heading", level: heading[1].length, children: parseInline(heading[2]) });
1859
- i += 1;
1860
- continue;
1848
+ return list;
1849
+ }, [data, quickPicks, recent, labels.quickPicks, labels.recent]);
1850
+ const sections = (0, import_react5.useMemo)(() => {
1851
+ if (!query.trim()) return browseSections;
1852
+ return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
1853
+ }, [query, data, browseSections, labels.search]);
1854
+ const flat = (0, import_react5.useMemo)(() => sections.flatMap((s) => s.emojis), [sections]);
1855
+ const sectionStart = (0, import_react5.useMemo)(() => {
1856
+ const starts = /* @__PURE__ */ new Map();
1857
+ let at = 0;
1858
+ for (const section of sections) {
1859
+ starts.set(section.key, at);
1860
+ at += section.emojis.length;
1861
1861
  }
1862
- if (RULE_RE.test(trimmed)) {
1863
- flush();
1864
- blocks.push({ type: "rule" });
1865
- i += 1;
1866
- continue;
1862
+ return starts;
1863
+ }, [sections]);
1864
+ const activeIndex = Math.min(active, Math.max(flat.length - 1, 0));
1865
+ const choose = (emoji) => {
1866
+ setRecent(pushRecentEmoji(emoji, recentStorageKey));
1867
+ onSelect(emoji);
1868
+ hide();
1869
+ };
1870
+ const focusCell = (0, import_react5.useCallback)((index, reveal = true) => {
1871
+ setActive(index);
1872
+ const find = () => gridRef.current?.querySelector(`[data-emoji-index="${index}"]`);
1873
+ const move = (cell2) => {
1874
+ cell2.focus({ preventScroll: true });
1875
+ if (reveal && gridRef.current) revealInGrid(gridRef.current, cell2);
1876
+ };
1877
+ const cell = find();
1878
+ if (cell) move(cell);
1879
+ else requestAnimationFrame(() => {
1880
+ const later = find();
1881
+ if (later) move(later);
1882
+ });
1883
+ }, []);
1884
+ const showSection = (key) => {
1885
+ const grid = gridRef.current;
1886
+ const section = grid?.querySelector(`[data-section="${key}"]`);
1887
+ if (!grid || !section) return;
1888
+ grid.scrollTop += section.getBoundingClientRect().top - grid.getBoundingClientRect().top;
1889
+ const start = sectionStart.get(key);
1890
+ if (start !== void 0) focusCell(start, false);
1891
+ };
1892
+ const onCategory = (key) => {
1893
+ if (!query.trim()) {
1894
+ showSection(key);
1895
+ return;
1896
+ }
1897
+ pendingJump.current = key;
1898
+ setQuery("");
1899
+ setActive(0);
1900
+ };
1901
+ useIsomorphicLayoutEffect3(() => {
1902
+ const key = pendingJump.current;
1903
+ if (key === null || query.trim()) return;
1904
+ pendingJump.current = null;
1905
+ showSection(key);
1906
+ }, [query, sections]);
1907
+ const onGridKeyDown = (event) => {
1908
+ const target = event.target;
1909
+ const index = Number(target.dataset.emojiIndex ?? -1);
1910
+ if (index < 0) return;
1911
+ const next = emojiGridMove(index, event.key, flat.length, EMOJI_GRID_COLUMNS);
1912
+ if (event.key === "ArrowUp" && next === null) {
1913
+ event.preventDefault();
1914
+ searchRef.current?.focus({ preventScroll: true });
1915
+ return;
1867
1916
  }
1868
- if (trimmed.startsWith(">")) {
1869
- flush();
1870
- const quote = [];
1871
- while (i < lines.length && lines[i].trim().startsWith(">")) {
1872
- quote.push(lines[i].trim().replace(/^>\s?/, ""));
1873
- i += 1;
1874
- }
1875
- blocks.push({ type: "quote", children: parseInline(quote.join(" ")) });
1876
- continue;
1917
+ if (next === null) return;
1918
+ event.preventDefault();
1919
+ focusCell(next);
1920
+ };
1921
+ const onSearchKeyDown = (event) => {
1922
+ if (event.key === "Escape") {
1923
+ event.preventDefault();
1924
+ hide();
1925
+ } else if (event.key === "ArrowDown" && flat.length) {
1926
+ event.preventDefault();
1927
+ focusCell(0);
1928
+ } else if (event.key === "Enter" && query.trim() && flat[0]) {
1929
+ event.preventDefault();
1930
+ choose(flat[0]);
1877
1931
  }
1878
- if (trimmed.includes("|") && i + 1 < lines.length && DELIMITER_ROW_RE.test(lines[i + 1]) && lines[i + 1].includes("-")) {
1879
- const head = splitTableRow(line);
1880
- const delimiters = splitTableRow(lines[i + 1]);
1881
- if (delimiters.length === head.length) {
1882
- flush();
1883
- const rows = [];
1884
- i += 2;
1885
- while (i < lines.length && lines[i].trim() !== "" && lines[i].includes("|")) {
1886
- const cells = splitTableRow(lines[i]);
1887
- rows.push(head.map((_, c) => parseInline(cells[c] ?? "")));
1888
- i += 1;
1889
- }
1890
- blocks.push({ type: "table", align: delimiters.map(alignOf), head: head.map((c) => parseInline(c)), rows });
1891
- continue;
1892
- }
1932
+ };
1933
+ const toggleFromTrigger = (event) => {
1934
+ const el = popoverEl();
1935
+ if (!el || event.defaultPrevented) return;
1936
+ const was = pointerWasOpen.current;
1937
+ pointerWasOpen.current = null;
1938
+ if (was === true) return;
1939
+ if (isOpen(el)) el.hidePopover();
1940
+ else el.showPopover();
1941
+ };
1942
+ const triggerProps = (0, import_react5.isValidElement)(trigger) ? trigger.props : {};
1943
+ const triggerEl = (0, import_react5.isValidElement)(trigger) ? (0, import_react5.cloneElement)(trigger, {
1944
+ "aria-haspopup": "dialog",
1945
+ "aria-expanded": open,
1946
+ "aria-controls": popoverId,
1947
+ onPointerDown: (event) => {
1948
+ pointerWasOpen.current = isOpen(popoverEl());
1949
+ triggerProps.onPointerDown?.(event);
1950
+ },
1951
+ onClick: (event) => {
1952
+ triggerProps.onClick?.(event);
1953
+ toggleFromTrigger(event);
1893
1954
  }
1894
- const first = matchListLine(line);
1895
- if (first) {
1896
- flush();
1897
- const listLines = [];
1898
- while (i < lines.length) {
1899
- const current = lines[i];
1900
- const item = matchListLine(current);
1901
- if (item) {
1902
- if (listLines.length && item.indent <= listLines[0].indent && item.ordered !== listLines[0].ordered) break;
1903
- listLines.push(item);
1904
- } else if (current.trim() !== "" && indentOf(current) > listLines[0].indent && listLines.length) {
1905
- listLines[listLines.length - 1].text += `
1906
- ${current.trim()}`;
1907
- } else {
1908
- break;
1909
- }
1910
- i += 1;
1955
+ }) : trigger;
1956
+ const names = data?.names;
1957
+ let offset = 0;
1958
+ const status = failed ? labels.loadFailed : !data && open ? labels.loading : query.trim() && data && flat.length === 0 ? labels.noResults : "";
1959
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { ref: anchorRef, className: cx("uix-emoji-picker__anchor", className), children: [
1960
+ triggerEl,
1961
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1962
+ Popover,
1963
+ {
1964
+ id: popoverId,
1965
+ anchor: anchorRef,
1966
+ placement,
1967
+ className: "uix-emoji-picker-popover",
1968
+ role: "dialog",
1969
+ "aria-label": labels.dialog,
1970
+ closeWhenAnchorHidden: true,
1971
+ onAnchorHidden: () => {
1972
+ closedByAnchor.current = true;
1973
+ },
1974
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uix-emoji-picker uix-emoji-picker--full", lang: locale, children: [
1975
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1976
+ "input",
1977
+ {
1978
+ ref: searchRef,
1979
+ type: "search",
1980
+ className: "uix-input uix-emoji-picker__search",
1981
+ "aria-label": labels.search,
1982
+ placeholder: labels.search,
1983
+ value: query,
1984
+ onChange: (event) => {
1985
+ setQuery(event.target.value);
1986
+ setActive(0);
1987
+ },
1988
+ onKeyDown: onSearchKeyDown,
1989
+ autoComplete: "off",
1990
+ spellCheck: false
1991
+ }
1992
+ ),
1993
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1994
+ "div",
1995
+ {
1996
+ className: "uix-emoji-picker__nav",
1997
+ role: data ? "group" : void 0,
1998
+ "aria-label": data ? labels.categories : void 0,
1999
+ children: data ? browseSections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2000
+ "button",
2001
+ {
2002
+ type: "button",
2003
+ className: "uix-emoji-picker__nav-btn",
2004
+ "aria-label": section.title,
2005
+ title: section.title,
2006
+ onClick: () => onCategory(section.key),
2007
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
2008
+ },
2009
+ section.key
2010
+ )) : null
2011
+ }
2012
+ ),
2013
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: [
2014
+ sections.map((section) => {
2015
+ const start = offset;
2016
+ offset += section.emojis.length;
2017
+ if (!section.emojis.length) return null;
2018
+ const headingId = `${id}-${section.key}`;
2019
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
2020
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
2021
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
2022
+ const index = start + i;
2023
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2024
+ "button",
2025
+ {
2026
+ type: "button",
2027
+ className: "uix-emoji-picker__btn",
2028
+ "data-emoji-index": index,
2029
+ tabIndex: index === activeIndex ? 0 : -1,
2030
+ "aria-label": names?.get(emoji) ?? emoji,
2031
+ title: names?.get(emoji),
2032
+ onClick: () => choose(emoji),
2033
+ onFocus: () => setActive(index),
2034
+ children: renderEmoji(emoji)
2035
+ },
2036
+ `${section.key}-${emoji}`
2037
+ );
2038
+ }) })
2039
+ ] }, section.key);
2040
+ }),
2041
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "uix-emoji-picker__status", role: "status", children: status })
2042
+ ] })
2043
+ ] }) : null
1911
2044
  }
1912
- const base = Math.min(...listLines.map((l) => l.indent));
1913
- blocks.push(...buildList(listLines.map((l) => ({ ...l, indent: l.indent - base }))));
1914
- continue;
1915
- }
1916
- paragraph.push(trimmed);
1917
- i += 1;
1918
- }
1919
- flush();
1920
- return blocks;
2045
+ )
2046
+ ] });
1921
2047
  }
1922
2048
 
1923
2049
  // src/components/Markdown.tsx
@@ -1953,6 +2079,13 @@ function renderInlines(nodes, policy) {
1953
2079
  i
1954
2080
  );
1955
2081
  }
2082
+ case "mention": {
2083
+ const name = policy.resolveMention?.(node.id, node.label) ?? node.label;
2084
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "uix-mention", "data-mention-id": node.id, children: [
2085
+ "@",
2086
+ name
2087
+ ] }, i);
2088
+ }
1956
2089
  case "image": {
1957
2090
  const resolved = policy.resolveImageSrc?.(node.src) ?? null;
1958
2091
  if (resolved) {
@@ -2011,10 +2144,17 @@ function renderBlock(block, policy, headingOffset, key) {
2011
2144
  );
2012
2145
  }
2013
2146
  }
2014
- function Markdown({ children, className, isSafeUrl = defaultMarkdownIsSafeUrl, resolveImageSrc, headingOffset = 2 }) {
2147
+ function Markdown({
2148
+ children,
2149
+ className,
2150
+ isSafeUrl = defaultMarkdownIsSafeUrl,
2151
+ resolveImageSrc,
2152
+ headingOffset = 2,
2153
+ resolveMention
2154
+ }) {
2015
2155
  const source = typeof children === "string" ? children : "";
2016
2156
  if (source.trim() === "") return null;
2017
- const policy = { isSafeUrl, resolveImageSrc };
2157
+ const policy = { isSafeUrl, resolveImageSrc, resolveMention };
2018
2158
  const blocks = parseMarkdown(source);
2019
2159
  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)) });
2020
2160
  }
@@ -2069,6 +2209,10 @@ var DEFAULT_RICH_TEXT_LABELS = {
2069
2209
  imagesUnsupported: "Images can't be added here.",
2070
2210
  emoji: "Emoji",
2071
2211
  emojiSuggestions: "Emoji suggestions",
2212
+ mentionSuggestions: "People",
2213
+ mentionSearching: "Searching\u2026",
2214
+ mentionEmpty: "No matching people",
2215
+ mentionFailed: "People could not be loaded.",
2072
2216
  undo: "Undo",
2073
2217
  redo: "Redo",
2074
2218
  sourceMode: "Show markdown",
@@ -2099,6 +2243,8 @@ function suggestEmoji(query, limit = 8) {
2099
2243
  }
2100
2244
  return [...starts, ...tagged].slice(0, limit);
2101
2245
  }
2246
+ var mentionSuggestionKey = new import_state.PluginKey("uixMentionSuggestion");
2247
+ var usableCandidates = (items) => items.filter((c) => isMentionId(c.id) && c.label.trim() !== "").slice(0, 20);
2102
2248
  var htmlAttr = (value) => value === void 0 || value === false || value === "" ? void 0 : String(value);
2103
2249
  var popoverOpen = (el) => {
2104
2250
  try {
@@ -2197,8 +2343,10 @@ function RichTextEditor(props) {
2197
2343
  className,
2198
2344
  onBlur,
2199
2345
  emojiImageBaseUrl,
2200
- imagesUnavailableReason
2346
+ imagesUnavailableReason,
2347
+ onMentionSearch
2201
2348
  } = props;
2349
+ const mentionsOn = typeof onMentionSearch === "function";
2202
2350
  const emojiBase = (0, import_react7.useMemo)(() => normalizeEmojiImageBaseUrl(emojiImageBaseUrl), [emojiImageBaseUrl]);
2203
2351
  const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
2204
2352
  const labels = (0, import_react7.useMemo)(() => ({ ...DEFAULT_RICH_TEXT_LABELS, ...labelOverrides }), [labelOverrides]);
@@ -2206,10 +2354,11 @@ function RichTextEditor(props) {
2206
2354
  const counterId = `${uid}-counter`;
2207
2355
  const statusId = `${uid}-status`;
2208
2356
  const suggestId = `${uid}-emoji`;
2357
+ const mentionId = `${uid}-people`;
2209
2358
  const linkPopoverId = `${uid}-link`;
2210
2359
  const controls = PRESETS[features];
2211
2360
  const has = (control) => controls.has(control) && (control !== "emoji" || emoji) && (control !== "image" || !!onUploadImage);
2212
- const latest = (0, import_react7.useRef)({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false, imagesOffText: "" });
2361
+ const latest = (0, import_react7.useRef)({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, onMentionSearch, placeholder, imagesOn: false, imagesOffText: "" });
2213
2362
  latest.current = {
2214
2363
  onChange,
2215
2364
  onSubmitShortcut,
@@ -2217,6 +2366,7 @@ function RichTextEditor(props) {
2217
2366
  isSafeUrl,
2218
2367
  resolveImageSrc,
2219
2368
  onUploadImage,
2369
+ onMentionSearch,
2220
2370
  placeholder,
2221
2371
  imagesOn: controls.has("image") && !!onUploadImage,
2222
2372
  imagesOffText: imagesUnavailableReason?.trim() || labels.imagesUnsupported
@@ -2271,6 +2421,59 @@ function RichTextEditor(props) {
2271
2421
  return false;
2272
2422
  }
2273
2423
  };
2424
+ const [mention, setMention] = (0, import_react7.useState)(null);
2425
+ const [mentionIndex, setMentionIndex] = (0, import_react7.useState)(0);
2426
+ const mentionState = (0, import_react7.useRef)({ mention, mentionIndex });
2427
+ mentionState.current = { mention, mentionIndex };
2428
+ const mentionSeq = (0, import_react7.useRef)(0);
2429
+ const mentionBridge = (0, import_react7.useRef)({ update: () => {
2430
+ }, keyDown: () => false });
2431
+ mentionBridge.current = {
2432
+ update: (p) => {
2433
+ const seq = ++mentionSeq.current;
2434
+ const search = latest.current.onMentionSearch;
2435
+ if (!p || !search) {
2436
+ setMention(null);
2437
+ return;
2438
+ }
2439
+ const rect = p.clientRect?.() ?? null;
2440
+ const base = { query: p.query, command: p.command, rect };
2441
+ setMention((prev) => ({ ...base, status: "loading", items: prev?.query && p.query.startsWith(prev.query) ? prev.items : [] }));
2442
+ setMentionIndex(0);
2443
+ Promise.resolve().then(() => search(p.query)).then(
2444
+ (found) => {
2445
+ if (seq !== mentionSeq.current) return;
2446
+ setMention({ ...base, status: "ready", items: usableCandidates(found ?? []) });
2447
+ },
2448
+ () => {
2449
+ if (seq !== mentionSeq.current) return;
2450
+ setMention({ ...base, status: "error", items: [] });
2451
+ }
2452
+ );
2453
+ },
2454
+ keyDown: ({ event }) => {
2455
+ const { mention: open, mentionIndex: index } = mentionState.current;
2456
+ if (!open) return false;
2457
+ if (event.key === "Escape") {
2458
+ mentionSeq.current += 1;
2459
+ setMention(null);
2460
+ return true;
2461
+ }
2462
+ const picks = open.status === "ready" ? open.items : [];
2463
+ if (picks.length === 0) return false;
2464
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
2465
+ const delta = event.key === "ArrowDown" ? 1 : -1;
2466
+ setMentionIndex((index + delta + picks.length) % picks.length);
2467
+ return true;
2468
+ }
2469
+ if (event.key === "Enter" || event.key === "Tab") {
2470
+ const item = picks[Math.min(index, picks.length - 1)];
2471
+ if (item) open.command(item);
2472
+ return true;
2473
+ }
2474
+ return false;
2475
+ }
2476
+ };
2274
2477
  const headingKey = [...headingLevels].sort().join(",");
2275
2478
  const extensions = (0, import_react7.useMemo)(() => {
2276
2479
  const image = import_extension_image2.Image.extend({
@@ -2287,7 +2490,7 @@ function RichTextEditor(props) {
2287
2490
  renderHTML({ node, HTMLAttributes }) {
2288
2491
  const src = String(node.attrs.src ?? "");
2289
2492
  const resolved = latest.current.resolveImageSrc?.(src) ?? null;
2290
- if (resolved) return ["img", (0, import_core2.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes, { src: resolved })];
2493
+ if (resolved) return ["img", (0, import_core3.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes, { src: resolved })];
2291
2494
  return ["span", { class: "uix-rich-text__image-link", "data-src": src, title: src }, node.attrs.alt || src];
2292
2495
  }
2293
2496
  }).configure({ inline: true, allowBase64: false });
@@ -2302,7 +2505,7 @@ function RichTextEditor(props) {
2302
2505
  return findShortcode(name2)?.emoji ?? (name2 ? `:${name2}:` : "");
2303
2506
  },
2304
2507
  addInputRules() {
2305
- return [new import_core2.InputRule({
2508
+ return [new import_core3.InputRule({
2306
2509
  find: SHORTCODE_RE,
2307
2510
  handler: ({ range, match, chain }) => {
2308
2511
  const item = findShortcode(match[1] ?? "");
@@ -2337,7 +2540,7 @@ function RichTextEditor(props) {
2337
2540
  })
2338
2541
  }
2339
2542
  });
2340
- const shortcuts = import_core2.Extension.create({
2543
+ const shortcuts = import_core3.Extension.create({
2341
2544
  name: "uixRichTextShortcuts",
2342
2545
  addKeyboardShortcuts() {
2343
2546
  return {
@@ -2355,7 +2558,7 @@ function RichTextEditor(props) {
2355
2558
  };
2356
2559
  }
2357
2560
  });
2358
- const emojiImages = import_core2.Extension.create({
2561
+ const emojiImages = import_core3.Extension.create({
2359
2562
  name: "uixEmojiImages",
2360
2563
  addProseMirrorPlugins() {
2361
2564
  const key = new import_state.PluginKey("uixEmojiImages");
@@ -2393,6 +2596,30 @@ function RichTextEditor(props) {
2393
2596
  })];
2394
2597
  }
2395
2598
  });
2599
+ const mentionSuggest = import_core3.Extension.create({
2600
+ name: "uixMentionSuggestion",
2601
+ addProseMirrorPlugins() {
2602
+ return [(0, import_suggestion.Suggestion)({
2603
+ editor: this.editor,
2604
+ char: "@",
2605
+ pluginKey: mentionSuggestionKey,
2606
+ items: () => [],
2607
+ command: ({ editor: ed, range, props: item }) => {
2608
+ ed.chain().focus().insertContentAt(range, [
2609
+ { type: "mention", attrs: { id: item.id, label: item.label.replace(/\s+/gu, " ").trim() } },
2610
+ { type: "text", text: " " }
2611
+ ]).run();
2612
+ },
2613
+ allow: ({ state: state2, range }) => !state2.doc.resolve(range.from).parent.type.spec.code,
2614
+ render: () => ({
2615
+ onStart: (p) => mentionBridge.current.update(p),
2616
+ onUpdate: (p) => mentionBridge.current.update(p),
2617
+ onExit: () => mentionBridge.current.update(null),
2618
+ onKeyDown: (p) => mentionBridge.current.keyDown(p)
2619
+ })
2620
+ })];
2621
+ }
2622
+ });
2396
2623
  return [
2397
2624
  ...createSchemaExtensions({
2398
2625
  headingLevels: headingKey.split(",").map(Number),
@@ -2403,9 +2630,10 @@ function RichTextEditor(props) {
2403
2630
  import_extensions.Placeholder.configure({ placeholder: () => latest.current.placeholder ?? "" }),
2404
2631
  shortcuts,
2405
2632
  ...emoji ? [unicodeEmoji] : [],
2406
- ...emojiBase ? [emojiImages] : []
2633
+ ...emojiBase ? [emojiImages] : [],
2634
+ ...mentionsOn ? [mentionSuggest] : []
2407
2635
  ];
2408
- }, [headingKey, emoji, emojiBase]);
2636
+ }, [headingKey, emoji, emojiBase, mentionsOn]);
2409
2637
  const managerOf = (ed) => {
2410
2638
  const manager = ed.markdown ?? ed.storage.markdown.manager;
2411
2639
  return patchManager(manager);
@@ -2487,6 +2715,8 @@ function RichTextEditor(props) {
2487
2715
  const invalid = props["aria-invalid"] || over;
2488
2716
  const describedBy = [props["aria-describedby"], maxLength !== void 0 ? counterId : void 0, status ? statusId : void 0].filter(Boolean).join(" ") || void 0;
2489
2717
  const suggestOpen = !!suggest && mode === "rich";
2718
+ const mentionOpen = !suggestOpen && !!mention && mode === "rich";
2719
+ const mentionActive = mentionOpen && mention?.status === "ready" && mention.items.length > 0 ? `${mentionId}-${Math.min(mentionIndex, mention.items.length - 1)}` : void 0;
2490
2720
  const rows = minRows ?? (variant === "composer" ? 3 : 4);
2491
2721
  (0, import_react7.useEffect)(() => {
2492
2722
  if (!editor || editor.isDestroyed) return;
@@ -2504,9 +2734,9 @@ function RichTextEditor(props) {
2504
2734
  "aria-required": htmlAttr(props["aria-required"]),
2505
2735
  "aria-disabled": htmlAttr(disabled),
2506
2736
  "aria-readonly": htmlAttr(readOnly),
2507
- "aria-autocomplete": emoji ? "list" : void 0,
2508
- "aria-controls": suggestOpen ? suggestId : void 0,
2509
- "aria-activedescendant": suggestOpen ? `${suggestId}-${suggestIndex}` : void 0,
2737
+ "aria-autocomplete": emoji || mentionsOn ? "list" : void 0,
2738
+ "aria-controls": suggestOpen ? suggestId : mentionOpen ? mentionId : void 0,
2739
+ "aria-activedescendant": suggestOpen ? `${suggestId}-${suggestIndex}` : mentionActive,
2510
2740
  class: "uix-rich-text__content uix-prose",
2511
2741
  style: `min-height: calc(${rows} * 1lh + 2 * var(--uix-space-3))`
2512
2742
  }).filter(([, v]) => v !== void 0))
@@ -2527,7 +2757,11 @@ function RichTextEditor(props) {
2527
2757
  suggestOpen,
2528
2758
  suggestId,
2529
2759
  suggestIndex,
2530
- rows
2760
+ rows,
2761
+ mentionsOn,
2762
+ mentionOpen,
2763
+ mentionId,
2764
+ mentionActive
2531
2765
  ]);
2532
2766
  const state = (0, import_react8.useEditorState)({
2533
2767
  editor,
@@ -2881,6 +3115,16 @@ function RichTextEditor(props) {
2881
3115
  onPick: (item) => suggest.command(item),
2882
3116
  renderEmoji
2883
3117
  }
3118
+ ) : null,
3119
+ mentionOpen && mention ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3120
+ MentionSuggestions,
3121
+ {
3122
+ id: mentionId,
3123
+ labels,
3124
+ state: mention,
3125
+ active: Math.min(mentionIndex, Math.max(mention.items.length - 1, 0)),
3126
+ onPick: (item) => mention.command(item)
3127
+ }
2884
3128
  ) : null
2885
3129
  ] });
2886
3130
  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;
@@ -2915,8 +3159,7 @@ function RichTextEditor(props) {
2915
3159
  hidden
2916
3160
  ] });
2917
3161
  }
2918
- function EmojiSuggestions({ id, label, items, query, active, rect, onPick, renderEmoji }) {
2919
- const listRef = (0, import_react7.useRef)(null);
3162
+ function useCaretPosition(listRef, rect, size) {
2920
3163
  const [pos, setPos] = (0, import_react7.useState)(null);
2921
3164
  (0, import_react7.useEffect)(() => {
2922
3165
  const el = listRef.current;
@@ -2929,7 +3172,71 @@ function EmojiSuggestions({ id, label, items, query, active, rect, onPick, rende
2929
3172
  { placement: "bottom-start", offset: 4, padding: 8 }
2930
3173
  );
2931
3174
  setPos({ x: result.x, y: result.y });
2932
- }, [rect, items.length]);
3175
+ }, [listRef, rect, size]);
3176
+ return pos;
3177
+ }
3178
+ function MentionSuggestions({ id, labels, state, active, onPick }) {
3179
+ const listRef = (0, import_react7.useRef)(null);
3180
+ const items = state.status === "ready" ? state.items : [];
3181
+ const pos = useCaretPosition(listRef, state.rect, `${state.status}:${items.length}`);
3182
+ (0, import_react7.useEffect)(() => {
3183
+ document.getElementById(`${id}-${active}`)?.scrollIntoView?.({ block: "nearest" });
3184
+ }, [active, id]);
3185
+ const style = { left: pos?.x ?? -9999, top: pos?.y ?? -9999 };
3186
+ if (items.length === 0) {
3187
+ const note = state.status === "loading" ? labels.mentionSearching : state.status === "error" ? labels.mentionFailed : labels.mentionEmpty;
3188
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3189
+ "div",
3190
+ {
3191
+ ref: listRef,
3192
+ id,
3193
+ role: "status",
3194
+ "aria-label": labels.mentionSuggestions,
3195
+ className: "uix-rich-text__suggest",
3196
+ "data-mention-list": "",
3197
+ "data-state": state.status === "ready" ? "empty" : state.status,
3198
+ style,
3199
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "uix-rich-text__suggest-note", children: note })
3200
+ }
3201
+ );
3202
+ }
3203
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3204
+ "div",
3205
+ {
3206
+ ref: listRef,
3207
+ id,
3208
+ role: "listbox",
3209
+ "aria-label": labels.mentionSuggestions,
3210
+ "aria-busy": state.status === "loading" || void 0,
3211
+ className: "uix-rich-text__suggest",
3212
+ "data-mention-list": "",
3213
+ "data-state": "ready",
3214
+ style,
3215
+ children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3216
+ "div",
3217
+ {
3218
+ id: `${id}-${i}`,
3219
+ role: "option",
3220
+ "aria-selected": i === active,
3221
+ className: "uix-rich-text__suggest-item uix-rich-text__suggest-item--person",
3222
+ "data-mention-id": item.id,
3223
+ onMouseDown: (event) => {
3224
+ event.preventDefault();
3225
+ onPick(item);
3226
+ },
3227
+ children: [
3228
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "uix-rich-text__suggest-label", children: item.label }),
3229
+ item.description ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "uix-rich-text__suggest-detail", children: item.description }) : null
3230
+ ]
3231
+ },
3232
+ item.id
3233
+ ))
3234
+ }
3235
+ );
3236
+ }
3237
+ function EmojiSuggestions({ id, label, items, query, active, rect, onPick, renderEmoji }) {
3238
+ const listRef = (0, import_react7.useRef)(null);
3239
+ const pos = useCaretPosition(listRef, rect, items.length);
2933
3240
  (0, import_react7.useEffect)(() => {
2934
3241
  document.getElementById(`${id}-${active}`)?.scrollIntoView?.({ block: "nearest" });
2935
3242
  }, [active, id]);
@@ -2979,8 +3286,13 @@ async function roundTripMarkdown(markdown, options) {
2979
3286
  // Annotate the CommonJS export names for ESM import in node:
2980
3287
  0 && (module.exports = {
2981
3288
  DEFAULT_RICH_TEXT_LABELS,
3289
+ MENTION_SCHEME,
2982
3290
  RichTextEditor,
2983
3291
  RichTextEditorFallback,
2984
3292
  emojiImageFileName,
3293
+ findMentions,
3294
+ formatMention,
3295
+ isMentionId,
3296
+ readMention,
2985
3297
  roundTripMarkdown
2986
3298
  });