@keboola/design 13.0.2 → 13.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,32 +9,35 @@ import { useStableCallback } from "./internal/hooks/useStableCallback.js";
9
9
  import { TextInput } from "./TextInput-BWYXbfs4.js";
10
10
  import { FormGroup } from "./FormGroup-CmCppxOn.js";
11
11
  import { Skeleton } from "./internal/components/Skeleton/Skeleton.js";
12
+ import { Kbd } from "./Kbd-J4n1J6i4.js";
12
13
  import { toast } from "./toast-CyXEdaCZ.js";
13
- import { Suspense, createContext, lazy, use, useCallback, useEffect, useRef, useState } from "react";
14
+ import { Suspense, createContext, lazy, use, useCallback, useEffect, useId, useRef, useState } from "react";
14
15
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
15
16
  import { createPortal } from "react-dom";
17
+ import { DismissableLayer } from "@radix-ui/react-dismissable-layer";
16
18
  import { $createCodeNode, $isCodeNode, CodeNode } from "@lexical/code";
17
- import { $createLinkNode, $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
18
- import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
19
- import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS } from "@lexical/markdown";
20
19
  import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
20
+ import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
21
21
  import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
22
22
  import { ClickableLinkPlugin } from "@lexical/react/LexicalClickableLinkPlugin";
23
23
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
24
24
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
25
25
  import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
26
26
  import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
27
- import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
28
27
  import { HorizontalRulePlugin } from "@lexical/react/LexicalHorizontalRulePlugin";
29
28
  import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
30
29
  import { ListPlugin } from "@lexical/react/LexicalListPlugin";
31
30
  import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
32
31
  import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
33
32
  import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
34
- import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
35
- import { $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isRangeSelection, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, DROP_COMMAND, DecoratorNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, KEY_DOWN_COMMAND, PASTE_COMMAND } from "lexical";
36
33
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
37
- import { $getNearestBlockElementAncestorOrThrow } from "@lexical/utils";
34
+ import { $addUpdateTag, $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isParagraphNode, $isRangeSelection, $isTextNode, $parseSerializedNode, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, DROP_COMMAND, DecoratorNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, HISTORY_PUSH_TAG, KEY_DOWN_COMMAND, PASTE_COMMAND, PASTE_TAG, createEditor } from "lexical";
35
+ import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from "@lexical/utils";
36
+ import { $createLinkNode, $isAutoLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
37
+ import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
38
+ import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
39
+ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
40
+ import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS } from "@lexical/markdown";
38
41
  //#region src/blocks/MarkdownEditor/context.ts
39
42
  const ImageRendererContext = createContext(null);
40
43
  const ImageRendererProvider = ImageRendererContext;
@@ -140,73 +143,6 @@ const $isImageNode = (node) => {
140
143
  return node instanceof ImageNode;
141
144
  };
142
145
  //#endregion
143
- //#region src/blocks/MarkdownEditor/nodes/MermaidNodeComponent.tsx
144
- const MermaidDiagram = lazy(() => import("./MermaidDiagram-B30x2yV7.js").then((n) => n.MermaidDiagram_exports).then((m) => ({ default: m.MermaidDiagram }), (error) => {
145
- console.error("[MarkdownEditor] failed to load MermaidDiagram chunk", error);
146
- return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
147
- }));
148
- const MermaidNodeComponent = ({ code }) => {
149
- return /* @__PURE__ */ jsx(Suspense, {
150
- fallback: /* @__PURE__ */ jsx("div", {
151
- className: "tw:my-3 tw:overflow-hidden tw:rounded-card tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6",
152
- children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" })
153
- }),
154
- children: /* @__PURE__ */ jsx(MermaidDiagram, { code })
155
- });
156
- };
157
- //#endregion
158
- //#region src/blocks/MarkdownEditor/nodes/MermaidNode.tsx
159
- var MermaidNode = class MermaidNode extends DecoratorNode {
160
- __code;
161
- static getType() {
162
- return "mermaid";
163
- }
164
- static clone(node) {
165
- return new MermaidNode(node.__code, node.__key);
166
- }
167
- constructor(code, key) {
168
- super(key);
169
- this.__code = code;
170
- }
171
- getCode() {
172
- return this.__code;
173
- }
174
- setCode(code) {
175
- const writable = this.getWritable();
176
- writable.__code = code;
177
- }
178
- createDOM() {
179
- const div = document.createElement("div");
180
- div.setAttribute("data-lexical-mermaid", "true");
181
- return div;
182
- }
183
- updateDOM() {
184
- return false;
185
- }
186
- decorate() {
187
- return /* @__PURE__ */ jsx(MermaidNodeComponent, { code: this.__code });
188
- }
189
- isInline() {
190
- return false;
191
- }
192
- static importJSON(serializedNode) {
193
- return $createMermaidNode(serializedNode.code);
194
- }
195
- exportJSON() {
196
- return {
197
- type: "mermaid",
198
- version: 1,
199
- code: this.__code
200
- };
201
- }
202
- };
203
- const $createMermaidNode = (code) => {
204
- return new MermaidNode(code);
205
- };
206
- const $isMermaidNode = (node) => {
207
- return node instanceof MermaidNode;
208
- };
209
- //#endregion
210
146
  //#region src/blocks/MarkdownEditor/plugins/ImagePastePlugin.tsx
211
147
  const ACCEPTED_IMAGE_TYPES = [
212
148
  "image/png",
@@ -339,6 +275,186 @@ const KeyboardPlugin = ({ onSave, onCancel, singleLine, blurOnEscape, skipBlurSa
339
275
  ]);
340
276
  return null;
341
277
  };
278
+ //#endregion
279
+ //#region src/blocks/MarkdownEditor/nodes/MermaidNodeComponent.tsx
280
+ const MermaidDiagram = lazy(() => import("./MermaidDiagram-B30x2yV7.js").then((n) => n.MermaidDiagram_exports).then((m) => ({ default: m.MermaidDiagram }), (error) => {
281
+ console.error("[MarkdownEditor] failed to load MermaidDiagram chunk", error);
282
+ return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
283
+ }));
284
+ const MermaidNodeComponent = ({ code }) => {
285
+ return /* @__PURE__ */ jsx(Suspense, {
286
+ fallback: /* @__PURE__ */ jsx("div", {
287
+ className: "tw:my-3 tw:overflow-hidden tw:rounded-card tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6",
288
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" })
289
+ }),
290
+ children: /* @__PURE__ */ jsx(MermaidDiagram, { code })
291
+ });
292
+ };
293
+ //#endregion
294
+ //#region src/blocks/MarkdownEditor/nodes/MermaidNode.tsx
295
+ var MermaidNode = class MermaidNode extends DecoratorNode {
296
+ __code;
297
+ static getType() {
298
+ return "mermaid";
299
+ }
300
+ static clone(node) {
301
+ return new MermaidNode(node.__code, node.__key);
302
+ }
303
+ constructor(code, key) {
304
+ super(key);
305
+ this.__code = code;
306
+ }
307
+ getCode() {
308
+ return this.__code;
309
+ }
310
+ setCode(code) {
311
+ const writable = this.getWritable();
312
+ writable.__code = code;
313
+ }
314
+ createDOM() {
315
+ const div = document.createElement("div");
316
+ div.setAttribute("data-lexical-mermaid", "true");
317
+ return div;
318
+ }
319
+ updateDOM() {
320
+ return false;
321
+ }
322
+ decorate() {
323
+ return /* @__PURE__ */ jsx(MermaidNodeComponent, { code: this.__code });
324
+ }
325
+ isInline() {
326
+ return false;
327
+ }
328
+ static importJSON(serializedNode) {
329
+ return $createMermaidNode(serializedNode.code);
330
+ }
331
+ exportJSON() {
332
+ return {
333
+ type: "mermaid",
334
+ version: 1,
335
+ code: this.__code
336
+ };
337
+ }
338
+ };
339
+ const $createMermaidNode = (code) => {
340
+ return new MermaidNode(code);
341
+ };
342
+ const $isMermaidNode = (node) => {
343
+ return node instanceof MermaidNode;
344
+ };
345
+ //#endregion
346
+ //#region src/blocks/MarkdownEditor/editorNodes.ts
347
+ /**
348
+ * Every node the editor registers, shared by the composer and the paste parser.
349
+ *
350
+ * - `AutoLinkNode` is registered always; only `autoLink` mounts the plugin that creates one.
351
+ */
352
+ const EDITOR_NODES = [
353
+ ImageNode,
354
+ MermaidNode,
355
+ HeadingNode,
356
+ QuoteNode,
357
+ ListNode,
358
+ ListItemNode,
359
+ CodeNode,
360
+ LinkNode,
361
+ AutoLinkNode,
362
+ HorizontalRuleNode
363
+ ];
364
+ //#endregion
365
+ //#region src/blocks/MarkdownEditor/bareUrls.ts
366
+ const DOMAIN_CHAR = String.raw`(?:[^\s\p{P}\p{S}]|-)`;
367
+ const DOMAIN = String.raw`${DOMAIN_CHAR}+(?:[._]${DOMAIN_CHAR}+)*`;
368
+ const URL_START = String.raw`(?:[hH][tT][tT][pP][sS]?:\/\/|[wW][wW][wW]\.)`;
369
+ const URL_BODY = String.raw`${URL_START}${DOMAIN}[^\s<]*`;
370
+ const EMAIL_BODY = String.raw`[\w.+-]+@[\w-]+(?:\.[\w-]+)+`;
371
+ const CANDIDATE = new RegExp(String.raw`(?<![a-zA-Z])(?=[hH])${URL_BODY}|(?<=^|[\s(*_[\]~])(?=[wW])${URL_BODY}|(?<![\w.+/-])${EMAIL_BODY}`, "gu");
372
+ const URL_DOMAIN = new RegExp(String.raw`^${URL_START}(${DOMAIN})`, "u");
373
+ const TRAILING_PUNCTUATION = /[!"'*,.:;?_~\]]$/;
374
+ const TRAILING_ENTITY = /&[a-zA-Z][a-zA-Z0-9]*;$/;
375
+ const count = (text, char) => text.split(char).length - 1;
376
+ const trimTrailing = (candidate) => {
377
+ let text = candidate;
378
+ for (;;) {
379
+ const entity = TRAILING_ENTITY.exec(text);
380
+ if (entity) text = text.slice(0, -entity[0].length);
381
+ else if (TRAILING_PUNCTUATION.test(text)) text = text.slice(0, -1);
382
+ else if (text.endsWith(")") && count(text, ")") > count(text, "(")) text = text.slice(0, -1);
383
+ else return text;
384
+ }
385
+ };
386
+ const hasLinkableDomain = (url) => {
387
+ const domain = URL_DOMAIN.exec(url)?.[1];
388
+ return domain !== void 0 && !domain.split(".").slice(-2).some((part) => part.includes("_"));
389
+ };
390
+ /**
391
+ * The bare URLs and email addresses in `text`, as GFM's autolink extension finds them.
392
+ *
393
+ * - `http://`, `https://` or `www.` and a domain, then anything up to whitespace or `<`.
394
+ * - Trailing `!"'*,.:;?_~]`, an unbalanced `)` and an entity like `&amp;` stay outside.
395
+ * - `http(s)://` starts after anything but a letter, `www.` only after whitespace or `(*_[]~`.
396
+ * - An email ending in `-` or `_` is not one.
397
+ */
398
+ const findBareUrls = (text, limit = Infinity) => {
399
+ const found = [];
400
+ for (const match of text.matchAll(CANDIDATE)) {
401
+ const source = match[0];
402
+ if (/^(?:https?:\/\/|www\.)/i.test(source)) {
403
+ const trimmed = trimTrailing(source);
404
+ if (!hasLinkableDomain(trimmed)) continue;
405
+ const url = /^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed;
406
+ found.push({
407
+ index: match.index,
408
+ text: trimmed,
409
+ url
410
+ });
411
+ } else if (!/[-_]$/.test(source)) found.push({
412
+ index: match.index,
413
+ text: source,
414
+ url: `mailto:${source}`
415
+ });
416
+ if (found.length >= limit) break;
417
+ }
418
+ return found;
419
+ };
420
+ /**
421
+ * Every stretch of `text` GFM starts scanning as a URL or email, linked in the end or not.
422
+ *
423
+ * - Export writes these raw: GFM scans the run before trimming its end, and an escape moves where
424
+ * it ends a domain, so `https://a\_b.c\_d` is linked with its backslashes, `https://a_b.c_d` is not.
425
+ */
426
+ const findUrlRuns = (text) => Array.from(text.matchAll(CANDIDATE), (match) => ({
427
+ index: match.index,
428
+ source: match[0]
429
+ }));
430
+ /**
431
+ * A `RegExp` whose `String#match` is the first URL `findBareUrls` finds, for Lexical's `importRegExp`.
432
+ *
433
+ * - Lexical's markdown import reads `importRegExp` only through `text.match(importRegExp)`.
434
+ * - So a candidate GFM rejects (`https://a_b.c_d`) never hides a valid URL after it.
435
+ */
436
+ var FirstBareUrl = class extends RegExp {
437
+ [Symbol.match](text) {
438
+ const [url] = findBareUrls(text, 1);
439
+ if (!url) return null;
440
+ const match = [url.text];
441
+ return Object.assign(match, {
442
+ index: url.index,
443
+ input: text
444
+ });
445
+ }
446
+ };
447
+ const FIRST_BARE_URL = new FirstBareUrl(CANDIDATE.source, "u");
448
+ /** Lexical `AutoLinkPlugin` matcher: the first bare URL in the text, as `findBareUrls` reads it. */
449
+ const matchBareUrl = (text) => {
450
+ const [first] = findBareUrls(text, 1);
451
+ return first ? {
452
+ index: first.index,
453
+ length: first.text.length,
454
+ text: first.text,
455
+ url: first.url
456
+ } : null;
457
+ };
342
458
  /**
343
459
  * Our transformers plus Lexical's built-ins; order matters.
344
460
  *
@@ -409,6 +525,44 @@ const CUSTOM_TRANSFORMERS = [
409
525
  },
410
526
  ...TRANSFORMERS
411
527
  ];
528
+ const urlPlaceholder = (index) => `\uE000${index}\uE001`;
529
+ const URL_PLACEHOLDER = /\uE000(\d+)\uE001/g;
530
+ /** `CUSTOM_TRANSFORMERS` plus bare URLs, for `autoLink`. */
531
+ const AUTO_LINK_TRANSFORMERS = [{
532
+ dependencies: [],
533
+ importRegExp: FIRST_BARE_URL,
534
+ replace: (node) => {
535
+ const literal = $createTextNode(unescapeMarkdownText(node.getTextContent()));
536
+ literal.setFormat(node.getFormat());
537
+ node.replace(literal);
538
+ },
539
+ export: (node, _exportChildren, exportFormat) => {
540
+ if (!$isTextNode(node) || node.hasFormat("code")) return null;
541
+ const parent = node.getParent();
542
+ if ($isLinkNode(parent) && !$isAutoLinkNode(parent)) return null;
543
+ const text = node.getTextContent();
544
+ if (text.includes("")) return null;
545
+ const lead = $isAutoLinkNode(node.getPreviousSibling()) ? /^[^\s<]+/.exec(text)?.[0] ?? "" : "";
546
+ const urls = $isAutoLinkNode(parent) ? [{
547
+ index: 0,
548
+ source: text
549
+ }] : [...lead ? [{
550
+ index: 0,
551
+ source: lead
552
+ }] : [], ...findUrlRuns(text).filter((url) => url.index >= lead.length)];
553
+ if (urls.length === 0) return null;
554
+ let masked = "";
555
+ let end = 0;
556
+ urls.forEach((url, i) => {
557
+ masked += text.slice(end, url.index) + urlPlaceholder(i);
558
+ end = url.index + url.source.length;
559
+ });
560
+ masked += text.slice(end);
561
+ return exportFormat(node, masked).replace(URL_PLACEHOLDER, (_, i) => urls[Number(i)]?.source ?? "");
562
+ },
563
+ regExp: /(?!)/,
564
+ type: "text-match"
565
+ }, ...CUSTOM_TRANSFORMERS];
412
566
  /**
413
567
  * No transformers: `singleLine` mode edits plain-text entity names, not markdown.
414
568
  *
@@ -456,6 +610,10 @@ const preprocessMarkdown = (markdown) => {
456
610
  if (tableLines.length > 0) flushTable();
457
611
  return result.join("\n");
458
612
  };
613
+ /** Replaces the root with `markdown`: the one import a load, a `value` change and a paste share. */
614
+ const $importMarkdown = (markdown, transformers) => {
615
+ $convertFromMarkdownString(preprocessMarkdown(markdown), transformers);
616
+ };
459
617
  /** Unwrap code-fenced tables back to raw GFM table markdown. */
460
618
  const postprocessMarkdown = (markdown) => {
461
619
  const regex = new RegExp(`\`\`\`${TABLE_FENCE_LANG}\\n([\\s\\S]*?)\\n\`\`\``, "g");
@@ -464,6 +622,81 @@ const postprocessMarkdown = (markdown) => {
464
622
  /** Reverses Lexical's export escaping of `* _ ` ~ \`, for `singleLine` plain-text names. */
465
623
  const unescapeMarkdownText = (markdown) => markdown.replace(/\\([*_`~\\])/g, "$1");
466
624
  //#endregion
625
+ //#region src/blocks/MarkdownEditor/plugins/MarkdownPastePlugin.tsx
626
+ /**
627
+ * `markdown` as the top-level nodes the editor's own import makes of it.
628
+ *
629
+ * - Runs in a scratch editor: `$importMarkdown` replaces the root it imports into,
630
+ * and only nodes attached to a root get their lines merged into one paragraph.
631
+ */
632
+ const parseMarkdown = (markdown, transformers) => {
633
+ const scratch = createEditor({
634
+ namespace: "MarkdownEditorPaste",
635
+ nodes: EDITOR_NODES,
636
+ onError: (error) => {
637
+ throw error;
638
+ }
639
+ });
640
+ scratch.update(() => {
641
+ $importMarkdown(markdown, transformers);
642
+ }, { discrete: true });
643
+ return scratch.getEditorState().toJSON().root.children;
644
+ };
645
+ /**
646
+ * Inserts parsed blocks at the selection, keeping a first or last block that is not a paragraph whole.
647
+ *
648
+ * - `insertNodes` merges the first block into a non-empty line and the rest of the line into the
649
+ * last block, which would flatten a fence pasted after `Logs:` into plain text.
650
+ * - An empty paragraph first is never merged, so the block after it goes in whole; it is then removed.
651
+ * - An empty paragraph last takes the rest of the line, and is removed when there was none.
652
+ */
653
+ const $insertBlocks = (selection, nodes) => {
654
+ const first = nodes[0];
655
+ const last = nodes.at(-1);
656
+ if (!first || !last) return;
657
+ const head = $isParagraphNode(first) ? null : $createParagraphNode();
658
+ const tail = $isParagraphNode(last) ? null : $createParagraphNode();
659
+ selection.insertNodes([
660
+ head,
661
+ ...nodes,
662
+ tail
663
+ ].filter((node) => node !== null));
664
+ head?.remove();
665
+ if (!tail) return;
666
+ if (tail.isEmpty()) {
667
+ tail.remove();
668
+ last.selectEnd();
669
+ } else tail.selectStart();
670
+ };
671
+ /**
672
+ * Parses a plain-text paste as markdown and inserts the result at the selection.
673
+ *
674
+ * - Only for a clipboard with `text/plain` and neither `text/html` nor files; those keep
675
+ * Lexical's own paste, and files stay for the image uploader or the consumer.
676
+ * - Inside a code block the text goes in literally, through the same fallback.
677
+ * - One undo step: the update is tagged `history-push`.
678
+ */
679
+ const MarkdownPastePlugin = ({ transformers }) => {
680
+ const [editor] = useLexicalComposerContext();
681
+ useEffect(() => {
682
+ return editor.registerCommand(PASTE_COMMAND, (event) => {
683
+ const data = "clipboardData" in event ? event.clipboardData : null;
684
+ if (!data || data.files.length > 0 || data.types.includes("text/html")) return false;
685
+ const text = data.getData("text/plain").replace(/\r\n?/g, "\n");
686
+ if (!text) return false;
687
+ const selection = $getSelection();
688
+ if (!$isRangeSelection(selection)) return false;
689
+ if ([selection.anchor, selection.focus].some((point) => $findMatchingParent(point.getNode(), $isCodeNode))) return false;
690
+ event.preventDefault();
691
+ $addUpdateTag(PASTE_TAG);
692
+ $addUpdateTag(HISTORY_PUSH_TAG);
693
+ $insertBlocks(selection, parseMarkdown(text, transformers).map($parseSerializedNode));
694
+ return true;
695
+ }, COMMAND_PRIORITY_LOW);
696
+ }, [editor, transformers]);
697
+ return null;
698
+ };
699
+ //#endregion
467
700
  //#region src/blocks/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx
468
701
  const IMPORT_TAG = "markdown-import";
469
702
  const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_TRANSFORMERS, singleLine = false }) => {
@@ -481,7 +714,7 @@ const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_T
481
714
  if (value === lastExportedRef.current) return;
482
715
  lastExportedRef.current = value;
483
716
  editor.update(() => {
484
- $convertFromMarkdownString(preprocessMarkdown(value), transformersRef.current);
717
+ $importMarkdown(value, transformersRef.current);
485
718
  }, { tag: IMPORT_TAG });
486
719
  }, [editor, value]);
487
720
  useEffect(() => {
@@ -523,6 +756,7 @@ const ALIGNMENT_OPTIONS = [
523
756
  icon: AlignJustify
524
757
  }
525
758
  ];
759
+ const TRIGGER_LABEL = "Text alignment";
526
760
  const ALIGNMENT_ICONS = Object.fromEntries(ALIGNMENT_OPTIONS.map(({ value, icon }) => [value, icon]));
527
761
  const AlignmentDropdown = ({ editor, alignment }) => {
528
762
  const savedSelectionRef = useRef(null);
@@ -542,13 +776,14 @@ const AlignmentDropdown = ({ editor, alignment }) => {
542
776
  return /* @__PURE__ */ jsxs(DropdownMenu, {
543
777
  onOpenChange: handleOpenChange,
544
778
  children: [/* @__PURE__ */ jsx(Tooltip, {
545
- tooltip: "Text alignment",
779
+ tooltip: TRIGGER_LABEL,
546
780
  placement: "top",
547
781
  triggerAsChild: true,
548
782
  children: /* @__PURE__ */ jsx(DropdownMenu.Trigger, {
549
783
  asChild: true,
550
784
  children: /* @__PURE__ */ jsxs("button", {
551
785
  type: "button",
786
+ "aria-label": TRIGGER_LABEL,
552
787
  className: cn("tw:flex tw:h-7 tw:cursor-pointer tw:items-center tw:gap-1 tw:rounded tw:border-0 tw:bg-transparent tw:px-1.5 tw:text-xs tw:text-muted-foreground", "tw:hover:bg-accent"),
553
788
  children: [/* @__PURE__ */ jsx(CurrentAlignmentIcon, { className: "tw:size-4" }), /* @__PURE__ */ jsx(ChevronDown, { className: "tw:size-3" })]
554
789
  })
@@ -675,12 +910,39 @@ const BlockTypeDropdown = ({ editor, blockType }) => {
675
910
  };
676
911
  //#endregion
677
912
  //#region src/blocks/MarkdownEditor/plugins/toolbar/ToolbarButton.tsx
678
- const ToolbarButton = ({ active, disabled, onClick, tooltip, children, className: extraClassName }) => /* @__PURE__ */ jsx(Tooltip, {
679
- tooltip,
913
+ /**
914
+ * The modifier Lexical binds its format keys to: Cmd on Apple platforms, Ctrl elsewhere.
915
+ *
916
+ * - Read from Lexical's own `IS_APPLE`, which counts iPhone and iPad where `isMac` does not.
917
+ */
918
+ const MOD_KEY = IS_APPLE ? {
919
+ aria: "Meta",
920
+ symbol: "⌘"
921
+ } : {
922
+ aria: "Control",
923
+ symbol: "Ctrl"
924
+ };
925
+ const ToolbarButton = ({ active, disabled, onClick, tooltip, shortcutKey, children, className: extraClassName }) => /* @__PURE__ */ jsx(Tooltip, {
926
+ tooltip: shortcutKey ? /* @__PURE__ */ jsxs("span", {
927
+ className: "tw:inline-flex tw:items-center tw:gap-2",
928
+ children: [tooltip, /* @__PURE__ */ jsxs(Kbd.Group, {
929
+ "aria-hidden": true,
930
+ separator: "+",
931
+ children: [/* @__PURE__ */ jsx(Kbd, {
932
+ variant: "primary",
933
+ children: MOD_KEY.symbol
934
+ }), /* @__PURE__ */ jsx(Kbd, {
935
+ variant: "primary",
936
+ children: shortcutKey
937
+ })]
938
+ })]
939
+ }) : tooltip,
680
940
  placement: "top",
681
941
  triggerAsChild: true,
682
942
  children: /* @__PURE__ */ jsx("button", {
683
943
  type: "button",
944
+ "aria-label": tooltip,
945
+ "aria-keyshortcuts": shortcutKey ? `${MOD_KEY.aria}+${shortcutKey}` : void 0,
684
946
  disabled,
685
947
  onClick,
686
948
  className: cn("tw:flex tw:h-7 tw:min-w-7 tw:cursor-pointer tw:items-center tw:justify-center tw:rounded tw:border-0 tw:bg-transparent tw:px-1 tw:text-xs tw:text-muted-foreground", "tw:hover:bg-accent tw:disabled:cursor-not-allowed tw:disabled:opacity-30", active && "tw:bg-secondary-100 tw:text-secondary-500", extraClassName),
@@ -712,6 +974,8 @@ const LinkPanel = ({ editor, isLink }) => {
712
974
  const [label, setLabel] = useState("");
713
975
  const savedSelectionRef = useRef(null);
714
976
  const panelRef = useRef(null);
977
+ const labelId = useId();
978
+ const urlId = useId();
715
979
  const handleOpen = () => {
716
980
  editor.getEditorState().read(() => {
717
981
  const selection = $getSelection();
@@ -723,13 +987,27 @@ const LinkPanel = ({ editor, isLink }) => {
723
987
  });
724
988
  setOpen(true);
725
989
  };
726
- const handleClose = () => {
990
+ const resetPanel = () => {
727
991
  setOpen(false);
728
992
  setUrl("");
729
993
  setLabel("");
730
994
  savedSelectionRef.current = null;
995
+ };
996
+ const handleClose = () => {
997
+ resetPanel();
731
998
  editor.focus();
732
999
  };
1000
+ /**
1001
+ * Closes the panel on Escape pressed anywhere while it is open.
1002
+ *
1003
+ * - `preventDefault` claims the key from a surface outside Radix's stack (`FullScreenModal`).
1004
+ * - Focus goes back to the editor only from inside the panel; anywhere else it stays put.
1005
+ */
1006
+ const handleEscape = (event) => {
1007
+ event.preventDefault();
1008
+ if (panelRef.current?.contains(document.activeElement)) handleClose();
1009
+ else resetPanel();
1010
+ };
733
1011
  const handleSubmit = () => {
734
1012
  const trimmedUrl = url.trim();
735
1013
  if (!trimmedUrl) return;
@@ -741,10 +1019,6 @@ const LinkPanel = ({ editor, isLink }) => {
741
1019
  e.preventDefault();
742
1020
  handleSubmit();
743
1021
  }
744
- if (e.key === "Escape") {
745
- e.preventDefault();
746
- handleClose();
747
- }
748
1022
  };
749
1023
  if (isLink) return /* @__PURE__ */ jsx(ToolbarButton, {
750
1024
  active: true,
@@ -759,18 +1033,19 @@ const LinkPanel = ({ editor, isLink }) => {
759
1033
  onClick: handleOpen,
760
1034
  tooltip: "Insert Link",
761
1035
  children: /* @__PURE__ */ jsx(Link, { className: "tw:size-4" })
762
- }), open && /* @__PURE__ */ jsx("div", {
1036
+ }), open && /* @__PURE__ */ jsx(DismissableLayer, {
763
1037
  ref: panelRef,
1038
+ onEscapeKeyDown: handleEscape,
764
1039
  className: "tw:absolute tw:left-0 tw:top-full tw:z-50 tw:mt-1 tw:w-80 tw:rounded-md tw:border tw:border-border tw:bg-popover tw:p-3 tw:text-popover-foreground tw:shadow-lg",
765
1040
  children: /* @__PURE__ */ jsxs("div", {
766
1041
  className: "tw:flex tw:flex-col tw:gap-2",
767
1042
  children: [
768
1043
  /* @__PURE__ */ jsxs(FormGroup, { children: [/* @__PURE__ */ jsx(Label, {
769
- htmlFor: "link-label",
1044
+ htmlFor: labelId,
770
1045
  className: "tw:mb-1 tw:text-xs",
771
1046
  children: "Label"
772
1047
  }), /* @__PURE__ */ jsx(TextInput, {
773
- id: "link-label",
1048
+ id: labelId,
774
1049
  autoFocus: true,
775
1050
  placeholder: "Link text",
776
1051
  value: label,
@@ -779,11 +1054,11 @@ const LinkPanel = ({ editor, isLink }) => {
779
1054
  variant: "secondary"
780
1055
  })] }),
781
1056
  /* @__PURE__ */ jsxs(FormGroup, { children: [/* @__PURE__ */ jsx(Label, {
782
- htmlFor: "link-url",
1057
+ htmlFor: urlId,
783
1058
  className: "tw:mb-1 tw:text-xs",
784
1059
  children: "URL"
785
1060
  }), /* @__PURE__ */ jsx(TextInput, {
786
- id: "link-url",
1061
+ id: urlId,
787
1062
  placeholder: "https://…",
788
1063
  value: url,
789
1064
  onValueChange: setUrl,
@@ -822,7 +1097,17 @@ const ELEMENT_FORMAT_TYPES = [
822
1097
  ];
823
1098
  const isElementFormatType = (value) => ELEMENT_FORMAT_TYPES.includes(value);
824
1099
  const toElementFormatType = (value) => isElementFormatType(value) ? value : "left";
825
- const ToolbarPlugin = () => {
1100
+ const TOOLBAR_BASE_CLASS = "tw:flex tw:items-center tw:gap-0.5 tw:rounded-lg tw:border tw:border-border tw:px-1.5 tw:py-1";
1101
+ /**
1102
+ * Keeps focus, and the editor selection with it, where it was when a toolbar control is pressed.
1103
+ *
1104
+ * - A link panel field takes focus as usual, so a click there lands the caret in it.
1105
+ */
1106
+ const keepEditorFocus = (event) => {
1107
+ if (event.target instanceof HTMLInputElement) return;
1108
+ event.preventDefault();
1109
+ };
1110
+ const ToolbarPlugin = ({ mode = "floating" }) => {
826
1111
  const [editor] = useLexicalComposerContext();
827
1112
  const [isBold, setIsBold] = useState(false);
828
1113
  const [isItalic, setIsItalic] = useState(false);
@@ -837,6 +1122,7 @@ const ToolbarPlugin = () => {
837
1122
  const toolbarRef = useRef(null);
838
1123
  const isMouseDownRef = useRef(false);
839
1124
  const hasSelectionRef = useRef(false);
1125
+ const isFloating = mode === "floating";
840
1126
  const updatePosition = useCallback(() => {
841
1127
  const nativeSelection = window.getSelection();
842
1128
  if (!nativeSelection || nativeSelection.rangeCount === 0) return;
@@ -850,6 +1136,7 @@ const ToolbarPlugin = () => {
850
1136
  }
851
1137
  }, []);
852
1138
  useEffect(() => {
1139
+ if (!isFloating) return;
853
1140
  const handleMouseDown = () => {
854
1141
  isMouseDownRef.current = true;
855
1142
  };
@@ -863,7 +1150,7 @@ const ToolbarPlugin = () => {
863
1150
  document.removeEventListener("mousedown", handleMouseDown);
864
1151
  document.removeEventListener("mouseup", handleMouseUp);
865
1152
  };
866
- }, []);
1153
+ }, [isFloating]);
867
1154
  const updateToolbar = useCallback(() => {
868
1155
  const selection = $getSelection();
869
1156
  if (!$isRangeSelection(selection)) {
@@ -904,116 +1191,127 @@ const ToolbarPlugin = () => {
904
1191
  unregisterUpdate();
905
1192
  };
906
1193
  }, [editor, updateToolbar]);
907
- if (!isVisible || !position) return null;
908
- return createPortal(/* @__PURE__ */ jsxs("div", {
1194
+ if (isFloating && (!isVisible || !position)) return null;
1195
+ const buttons = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1196
+ /* @__PURE__ */ jsx(BlockTypeDropdown, {
1197
+ editor,
1198
+ blockType
1199
+ }),
1200
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1201
+ /* @__PURE__ */ jsx(ToolbarButton, {
1202
+ active: isBold,
1203
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
1204
+ tooltip: "Bold",
1205
+ shortcutKey: "B",
1206
+ children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
1207
+ }),
1208
+ /* @__PURE__ */ jsx(ToolbarButton, {
1209
+ active: isItalic,
1210
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
1211
+ tooltip: "Italic",
1212
+ shortcutKey: "I",
1213
+ children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
1214
+ }),
1215
+ /* @__PURE__ */ jsx(ToolbarButton, {
1216
+ active: isUnderline,
1217
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
1218
+ tooltip: "Underline",
1219
+ shortcutKey: "U",
1220
+ children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
1221
+ }),
1222
+ /* @__PURE__ */ jsx(ToolbarButton, {
1223
+ active: isStrikethrough,
1224
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"),
1225
+ tooltip: "Strikethrough",
1226
+ children: /* @__PURE__ */ jsx(Strikethrough, { className: "tw:size-4" })
1227
+ }),
1228
+ /* @__PURE__ */ jsx(ToolbarButton, {
1229
+ active: isCode,
1230
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"),
1231
+ tooltip: "Inline Code",
1232
+ children: /* @__PURE__ */ jsx(Code, { className: "tw:size-4" })
1233
+ }),
1234
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1235
+ /* @__PURE__ */ jsx(LinkPanel, {
1236
+ editor,
1237
+ isLink
1238
+ }),
1239
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1240
+ /* @__PURE__ */ jsx(ToolbarButton, {
1241
+ active: blockType === "bullet",
1242
+ onClick: () => {
1243
+ if (blockType === "bullet") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
1244
+ else editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
1245
+ },
1246
+ tooltip: "Bullet List",
1247
+ children: /* @__PURE__ */ jsx(List, { className: "tw:size-4" })
1248
+ }),
1249
+ /* @__PURE__ */ jsx(ToolbarButton, {
1250
+ active: blockType === "number",
1251
+ onClick: () => {
1252
+ if (blockType === "number") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
1253
+ else editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
1254
+ },
1255
+ tooltip: "Numbered List",
1256
+ children: /* @__PURE__ */ jsx(ListOrdered, { className: "tw:size-4" })
1257
+ }),
1258
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1259
+ /* @__PURE__ */ jsx(AlignmentDropdown, {
1260
+ editor,
1261
+ alignment
1262
+ }),
1263
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1264
+ /* @__PURE__ */ jsx(ToolbarButton, {
1265
+ active: blockType === "quote",
1266
+ onClick: () => {
1267
+ editor.update(() => {
1268
+ const selection = $getSelection();
1269
+ if (!$isRangeSelection(selection)) return;
1270
+ const anchorNode = selection.anchor.getNode();
1271
+ const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
1272
+ const newNode = blockType === "quote" ? $createParagraphNode() : $createQuoteNode();
1273
+ element.getChildren().forEach((child) => newNode.append(child));
1274
+ element.replace(newNode);
1275
+ });
1276
+ },
1277
+ tooltip: "Quote",
1278
+ children: /* @__PURE__ */ jsx(Quote, { className: "tw:size-4" })
1279
+ }),
1280
+ /* @__PURE__ */ jsx(ToolbarButton, {
1281
+ active: blockType === "code",
1282
+ onClick: () => {
1283
+ editor.update(() => {
1284
+ const selection = $getSelection();
1285
+ if (!$isRangeSelection(selection)) return;
1286
+ const anchorNode = selection.anchor.getNode();
1287
+ const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
1288
+ const newNode = blockType === "code" ? $createParagraphNode() : $createCodeNode();
1289
+ element.getChildren().forEach((child) => newNode.append(child));
1290
+ element.replace(newNode);
1291
+ });
1292
+ },
1293
+ tooltip: "Code Block",
1294
+ children: /* @__PURE__ */ jsx(SquareCode, { className: "tw:size-4" })
1295
+ })
1296
+ ] });
1297
+ if (!isFloating) return /* @__PURE__ */ jsx("div", {
1298
+ role: "toolbar",
1299
+ "aria-label": "Formatting options",
1300
+ onMouseDown: keepEditorFocus,
1301
+ className: cn(TOOLBAR_BASE_CLASS, "tw:mb-1 tw:flex-wrap tw:bg-background"),
1302
+ children: buttons
1303
+ });
1304
+ return createPortal(/* @__PURE__ */ jsx("div", {
909
1305
  ref: toolbarRef,
910
1306
  role: "toolbar",
911
1307
  "aria-label": "Formatting options",
912
- onMouseDown: (e) => e.preventDefault(),
1308
+ onMouseDown: keepEditorFocus,
913
1309
  style: {
914
- top: position.top,
915
- left: position.left
1310
+ top: position?.top,
1311
+ left: position?.left
916
1312
  },
917
- className: "tw:absolute tw:z-[1060] tw:flex tw:-translate-y-full tw:items-center tw:gap-0.5 tw:rounded-lg tw:border tw:border-border tw:bg-popover tw:px-1.5 tw:py-1 tw:text-popover-foreground tw:shadow-lg",
918
- children: [
919
- /* @__PURE__ */ jsx(BlockTypeDropdown, {
920
- editor,
921
- blockType
922
- }),
923
- /* @__PURE__ */ jsx(ToolbarDivider, {}),
924
- /* @__PURE__ */ jsx(ToolbarButton, {
925
- active: isBold,
926
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
927
- tooltip: "Bold (Ctrl+B)",
928
- children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
929
- }),
930
- /* @__PURE__ */ jsx(ToolbarButton, {
931
- active: isItalic,
932
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
933
- tooltip: "Italic (Ctrl+I)",
934
- children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
935
- }),
936
- /* @__PURE__ */ jsx(ToolbarButton, {
937
- active: isUnderline,
938
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
939
- tooltip: "Underline (Ctrl+U)",
940
- children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
941
- }),
942
- /* @__PURE__ */ jsx(ToolbarButton, {
943
- active: isStrikethrough,
944
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"),
945
- tooltip: "Strikethrough",
946
- children: /* @__PURE__ */ jsx(Strikethrough, { className: "tw:size-4" })
947
- }),
948
- /* @__PURE__ */ jsx(ToolbarButton, {
949
- active: isCode,
950
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"),
951
- tooltip: "Inline Code",
952
- children: /* @__PURE__ */ jsx(Code, { className: "tw:size-4" })
953
- }),
954
- /* @__PURE__ */ jsx(ToolbarDivider, {}),
955
- /* @__PURE__ */ jsx(LinkPanel, {
956
- editor,
957
- isLink
958
- }),
959
- /* @__PURE__ */ jsx(ToolbarDivider, {}),
960
- /* @__PURE__ */ jsx(ToolbarButton, {
961
- active: blockType === "bullet",
962
- onClick: () => {
963
- if (blockType === "bullet") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
964
- else editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
965
- },
966
- tooltip: "Bullet List",
967
- children: /* @__PURE__ */ jsx(List, { className: "tw:size-4" })
968
- }),
969
- /* @__PURE__ */ jsx(ToolbarButton, {
970
- active: blockType === "number",
971
- onClick: () => {
972
- if (blockType === "number") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
973
- else editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
974
- },
975
- tooltip: "Numbered List",
976
- children: /* @__PURE__ */ jsx(ListOrdered, { className: "tw:size-4" })
977
- }),
978
- /* @__PURE__ */ jsx(ToolbarDivider, {}),
979
- /* @__PURE__ */ jsx(AlignmentDropdown, {
980
- editor,
981
- alignment
982
- }),
983
- /* @__PURE__ */ jsx(ToolbarDivider, {}),
984
- /* @__PURE__ */ jsx(ToolbarButton, {
985
- active: blockType === "quote",
986
- onClick: () => {
987
- editor.update(() => {
988
- const selection = $getSelection();
989
- if (!$isRangeSelection(selection)) return;
990
- const anchorNode = selection.anchor.getNode();
991
- const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
992
- const newNode = blockType === "quote" ? $createParagraphNode() : $createQuoteNode();
993
- element.getChildren().forEach((child) => newNode.append(child));
994
- element.replace(newNode);
995
- });
996
- },
997
- tooltip: "Quote",
998
- children: /* @__PURE__ */ jsx(Quote, { className: "tw:size-4" })
999
- }),
1000
- /* @__PURE__ */ jsx(ToolbarButton, {
1001
- active: blockType === "code",
1002
- onClick: () => {
1003
- editor.update(() => {
1004
- const selection = $getSelection();
1005
- if (!$isRangeSelection(selection)) return;
1006
- const anchorNode = selection.anchor.getNode();
1007
- const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
1008
- const newNode = blockType === "code" ? $createParagraphNode() : $createCodeNode();
1009
- element.getChildren().forEach((child) => newNode.append(child));
1010
- element.replace(newNode);
1011
- });
1012
- },
1013
- tooltip: "Code Block",
1014
- children: /* @__PURE__ */ jsx(SquareCode, { className: "tw:size-4" })
1015
- })
1016
- ]
1313
+ className: cn(TOOLBAR_BASE_CLASS, "tw:absolute tw:z-[1060] tw:-translate-y-full tw:bg-popover tw:text-popover-foreground tw:shadow-lg"),
1314
+ children: buttons
1017
1315
  }), document.body);
1018
1316
  };
1019
1317
  //#endregion
@@ -1057,21 +1355,15 @@ const editorTheme = {
1057
1355
  };
1058
1356
  //#endregion
1059
1357
  //#region src/blocks/MarkdownEditor/MarkdownEditor.tsx
1060
- const EDITOR_NODES = [
1061
- ImageNode,
1062
- MermaidNode,
1063
- HeadingNode,
1064
- QuoteNode,
1065
- ListNode,
1066
- ListItemNode,
1067
- CodeNode,
1068
- LinkNode,
1069
- HorizontalRuleNode
1070
- ];
1071
- const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFocus = false, disabled = false, onSave, onCancel, onImageUpload, className, imageRenderer, showToolbar = false, compact = false, saveOnBlur = false, singleLine = false, clampDisplay = false, minRows }) => {
1358
+ const AUTO_LINK_MATCHERS = [matchBareUrl];
1359
+ const AUTO_LINK_EXCLUDED_PARENTS = [$isCodeNode];
1360
+ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFocus = false, disabled = false, onSave, onCancel, onImageUpload, className, imageRenderer, showToolbar = false, toolbarMode = "floating", compact = false, saveOnBlur = false, singleLine = false, clampDisplay = false, minRows, autoLink = false, pasteMarkdown = false }) => {
1072
1361
  const editable = !readOnly && !disabled;
1073
- const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : CUSTOM_TRANSFORMERS;
1362
+ const linkify = autoLink && !singleLine;
1363
+ const markdownPaste = pasteMarkdown && !singleLine;
1364
+ const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : linkify ? AUTO_LINK_TRANSFORMERS : CUSTOM_TRANSFORMERS;
1074
1365
  const useDefaultMinHeight = !compact && minRows === void 0;
1366
+ const contentPadding = compact ? "tw:px-1.5 tw:py-0.5" : "tw:p-2";
1075
1367
  const customMinHeight = !compact && minRows !== void 0 ? { minHeight: `${minRows * 1.75}em` } : void 0;
1076
1368
  const [initialConfig] = useState(() => ({
1077
1369
  namespace: "MarkdownEditor",
@@ -1082,7 +1374,7 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
1082
1374
  },
1083
1375
  editable,
1084
1376
  editorState: () => {
1085
- $convertFromMarkdownString(preprocessMarkdown(value), transformers);
1377
+ $importMarkdown(value, transformers);
1086
1378
  }
1087
1379
  }));
1088
1380
  const skipBlurSaveRef = useRef(false);
@@ -1101,16 +1393,17 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
1101
1393
  className: cn(!compact && "tw:prose", compact && "tw:[&_p]:m-0 tw:[&_p+p]:mt-2", clampDisplay && "tw:[&:not(:focus-within)]:line-clamp-2", className),
1102
1394
  onBlur: handleBlur,
1103
1395
  children: [
1104
- showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, {}),
1396
+ showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, { mode: toolbarMode }),
1105
1397
  /* @__PURE__ */ jsx("div", {
1106
- className: cn("tw:relative tw:rounded-md tw:transition-colors", compact ? "tw:px-1.5 tw:py-0.5 tw:-mx-1.5 tw:-my-0.5" : "tw:p-2", editable && "tw:cursor-text tw:hover:bg-muted tw:focus-within:ring-2 tw:focus-within:ring-ring/40"),
1398
+ className: cn("tw:relative tw:rounded-md tw:transition-colors", contentPadding, compact && "tw:-mx-1.5 tw:-my-0.5", editable && "tw:cursor-text tw:hover:bg-muted tw:focus-within:ring-2 tw:focus-within:ring-ring/40"),
1107
1399
  children: /* @__PURE__ */ jsx(RichTextPlugin, {
1108
1400
  contentEditable: /* @__PURE__ */ jsx(ContentEditable, {
1109
- className: cn("tw:outline-hidden", useDefaultMinHeight && "tw:min-h-[100px]"),
1401
+ className: cn("tw:outline-hidden", !compact && "tw:[&>*:first-child]:mt-0 tw:[&>*:last-child]:mb-0", useDefaultMinHeight && "tw:min-h-[100px]"),
1110
1402
  style: customMinHeight,
1111
1403
  "aria-placeholder": placeholder ?? "",
1112
1404
  placeholder: placeholder ? /* @__PURE__ */ jsx("div", {
1113
- className: cn("tw:pointer-events-none tw:absolute tw:select-none tw:text-muted-foreground", compact ? "tw:left-1.5 tw:top-0.5" : "tw:left-2 tw:top-[calc(0.5rem+0.35em)]"),
1405
+ "data-testid": "markdown-editor-placeholder",
1406
+ className: cn("tw:pointer-events-none tw:absolute tw:inset-0 tw:select-none tw:text-muted-foreground", contentPadding),
1114
1407
  children: placeholder
1115
1408
  }) : /* @__PURE__ */ jsx("div", {})
1116
1409
  }),
@@ -1133,6 +1426,11 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
1133
1426
  singleLine
1134
1427
  }),
1135
1428
  /* @__PURE__ */ jsx(ImagePastePlugin, { onImageUpload }),
1429
+ linkify && /* @__PURE__ */ jsx(AutoLinkPlugin, {
1430
+ matchers: AUTO_LINK_MATCHERS,
1431
+ excludeParents: AUTO_LINK_EXCLUDED_PARENTS
1432
+ }),
1433
+ markdownPaste && /* @__PURE__ */ jsx(MarkdownPastePlugin, { transformers }),
1136
1434
  /* @__PURE__ */ jsx(KeyboardPlugin, {
1137
1435
  onSave,
1138
1436
  onCancel,