@keboola/design 13.0.1 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +2 -2
  4. package/dist/{AIKit-VB7CXeQk.js → AIKit-BVCWP9Ks.js} +261 -69
  5. package/dist/AIKit-BVCWP9Ks.js.map +1 -0
  6. package/dist/{CodeEditor-jZMw1ioj.js → CodeEditor-Df7VVx08.js} +2 -2
  7. package/dist/{CodeEditor-jZMw1ioj.js.map → CodeEditor-Df7VVx08.js.map} +1 -1
  8. package/dist/{ColorModeSwitch-BBzn1D0u.js → ColorModeSwitch-D8Cki137.js} +2 -2
  9. package/dist/{ColorModeSwitch-BBzn1D0u.js.map → ColorModeSwitch-D8Cki137.js.map} +1 -1
  10. package/dist/{DropdownMenu-C5FLPkvR.js → DropdownMenu-BJGLjpVc.js} +3 -3
  11. package/dist/{DropdownMenu-C5FLPkvR.js.map → DropdownMenu-BJGLjpVc.js.map} +1 -1
  12. package/dist/{SectionNav-BTBiitiQ.js → SectionNav-I2p2uXTd.js} +2 -2
  13. package/dist/{SectionNav-BTBiitiQ.js.map → SectionNav-I2p2uXTd.js.map} +1 -1
  14. package/dist/{Sidebar-BTv636Cq.js → Sidebar-D9ytIs8V.js} +2 -2
  15. package/dist/{Sidebar-BTv636Cq.js.map → Sidebar-D9ytIs8V.js.map} +1 -1
  16. package/dist/Toast-NrOdDDru.js +76 -0
  17. package/dist/Toast-NrOdDDru.js.map +1 -0
  18. package/dist/editors.js +1 -1
  19. package/dist/index.d.ts +46 -12
  20. package/dist/index.js +9 -8
  21. package/dist/internal/blocks/AIKit.js +1 -1
  22. package/dist/internal/blocks/ColorModeSwitch.js +1 -1
  23. package/dist/internal/blocks/ExportMenu.js +1 -1
  24. package/dist/internal/blocks/SectionNav.js +1 -1
  25. package/dist/internal/blocks/SplitButton.js +1 -1
  26. package/dist/internal/components/DropdownMenu.js +1 -1
  27. package/dist/internal/components/Sidebar.js +1 -1
  28. package/dist/internal/components/Toast.js +1 -1
  29. package/dist/internal/hooks/useAltDigitHotkey.js +51 -0
  30. package/dist/internal/hooks/useAltDigitHotkey.js.map +1 -0
  31. package/dist/markdown-editor.d.ts +22 -1
  32. package/dist/markdown-editor.js +443 -201
  33. package/dist/markdown-editor.js.map +1 -1
  34. package/dist/rjsf.js +1 -1
  35. package/dist/styles.css +1 -1
  36. package/package.json +1 -1
  37. package/dist/AIKit-VB7CXeQk.js.map +0 -1
  38. package/dist/Toast-PfQf3vj-.js +0 -49
  39. package/dist/Toast-PfQf3vj-.js.map +0 -1
@@ -3,7 +3,7 @@ import { Tooltip } from "./utils-Bd28bWgm.js";
3
3
  import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, ChevronDown, Code, Heading1, Heading2, Heading3, Italic, Link, List, ListOrdered, Quote, SquareCode, Strikethrough, Type, Underline } from "./icons-wcA70iQx.js";
4
4
  import { Button } from "./Button-B21oVcPU.js";
5
5
  import { ButtonGroup } from "./internal/components/ButtonGroup.js";
6
- import { DropdownMenu } from "./DropdownMenu-C5FLPkvR.js";
6
+ import { DropdownMenu } from "./DropdownMenu-BJGLjpVc.js";
7
7
  import { Label } from "./internal/components/Label.js";
8
8
  import { useStableCallback } from "./internal/hooks/useStableCallback.js";
9
9
  import { TextInput } from "./TextInput-BWYXbfs4.js";
@@ -14,27 +14,28 @@ import { Suspense, createContext, lazy, use, useCallback, useEffect, useRef, use
14
14
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
15
15
  import { createPortal } from "react-dom";
16
16
  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
17
  import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
18
+ import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
21
19
  import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
22
20
  import { ClickableLinkPlugin } from "@lexical/react/LexicalClickableLinkPlugin";
23
21
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
24
22
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
25
23
  import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
26
24
  import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
27
- import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
28
25
  import { HorizontalRulePlugin } from "@lexical/react/LexicalHorizontalRulePlugin";
29
26
  import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
30
27
  import { ListPlugin } from "@lexical/react/LexicalListPlugin";
31
28
  import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
32
29
  import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
33
30
  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
31
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
37
- import { $getNearestBlockElementAncestorOrThrow } from "@lexical/utils";
32
+ 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";
33
+ import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow } from "@lexical/utils";
34
+ import { $createLinkNode, $isAutoLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
35
+ import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
36
+ import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
37
+ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
38
+ import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS } from "@lexical/markdown";
38
39
  //#region src/blocks/MarkdownEditor/context.ts
39
40
  const ImageRendererContext = createContext(null);
40
41
  const ImageRendererProvider = ImageRendererContext;
@@ -140,73 +141,6 @@ const $isImageNode = (node) => {
140
141
  return node instanceof ImageNode;
141
142
  };
142
143
  //#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
144
  //#region src/blocks/MarkdownEditor/plugins/ImagePastePlugin.tsx
211
145
  const ACCEPTED_IMAGE_TYPES = [
212
146
  "image/png",
@@ -339,6 +273,186 @@ const KeyboardPlugin = ({ onSave, onCancel, singleLine, blurOnEscape, skipBlurSa
339
273
  ]);
340
274
  return null;
341
275
  };
276
+ //#endregion
277
+ //#region src/blocks/MarkdownEditor/nodes/MermaidNodeComponent.tsx
278
+ const MermaidDiagram = lazy(() => import("./MermaidDiagram-B30x2yV7.js").then((n) => n.MermaidDiagram_exports).then((m) => ({ default: m.MermaidDiagram }), (error) => {
279
+ console.error("[MarkdownEditor] failed to load MermaidDiagram chunk", error);
280
+ return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
281
+ }));
282
+ const MermaidNodeComponent = ({ code }) => {
283
+ return /* @__PURE__ */ jsx(Suspense, {
284
+ fallback: /* @__PURE__ */ jsx("div", {
285
+ className: "tw:my-3 tw:overflow-hidden tw:rounded-card tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6",
286
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" })
287
+ }),
288
+ children: /* @__PURE__ */ jsx(MermaidDiagram, { code })
289
+ });
290
+ };
291
+ //#endregion
292
+ //#region src/blocks/MarkdownEditor/nodes/MermaidNode.tsx
293
+ var MermaidNode = class MermaidNode extends DecoratorNode {
294
+ __code;
295
+ static getType() {
296
+ return "mermaid";
297
+ }
298
+ static clone(node) {
299
+ return new MermaidNode(node.__code, node.__key);
300
+ }
301
+ constructor(code, key) {
302
+ super(key);
303
+ this.__code = code;
304
+ }
305
+ getCode() {
306
+ return this.__code;
307
+ }
308
+ setCode(code) {
309
+ const writable = this.getWritable();
310
+ writable.__code = code;
311
+ }
312
+ createDOM() {
313
+ const div = document.createElement("div");
314
+ div.setAttribute("data-lexical-mermaid", "true");
315
+ return div;
316
+ }
317
+ updateDOM() {
318
+ return false;
319
+ }
320
+ decorate() {
321
+ return /* @__PURE__ */ jsx(MermaidNodeComponent, { code: this.__code });
322
+ }
323
+ isInline() {
324
+ return false;
325
+ }
326
+ static importJSON(serializedNode) {
327
+ return $createMermaidNode(serializedNode.code);
328
+ }
329
+ exportJSON() {
330
+ return {
331
+ type: "mermaid",
332
+ version: 1,
333
+ code: this.__code
334
+ };
335
+ }
336
+ };
337
+ const $createMermaidNode = (code) => {
338
+ return new MermaidNode(code);
339
+ };
340
+ const $isMermaidNode = (node) => {
341
+ return node instanceof MermaidNode;
342
+ };
343
+ //#endregion
344
+ //#region src/blocks/MarkdownEditor/editorNodes.ts
345
+ /**
346
+ * Every node the editor registers, shared by the composer and the paste parser.
347
+ *
348
+ * - `AutoLinkNode` is registered always; only `autoLink` mounts the plugin that creates one.
349
+ */
350
+ const EDITOR_NODES = [
351
+ ImageNode,
352
+ MermaidNode,
353
+ HeadingNode,
354
+ QuoteNode,
355
+ ListNode,
356
+ ListItemNode,
357
+ CodeNode,
358
+ LinkNode,
359
+ AutoLinkNode,
360
+ HorizontalRuleNode
361
+ ];
362
+ //#endregion
363
+ //#region src/blocks/MarkdownEditor/bareUrls.ts
364
+ const DOMAIN_CHAR = String.raw`(?:[^\s\p{P}\p{S}]|-)`;
365
+ const DOMAIN = String.raw`${DOMAIN_CHAR}+(?:[._]${DOMAIN_CHAR}+)*`;
366
+ const URL_START = String.raw`(?:[hH][tT][tT][pP][sS]?:\/\/|[wW][wW][wW]\.)`;
367
+ const URL_BODY = String.raw`${URL_START}${DOMAIN}[^\s<]*`;
368
+ const EMAIL_BODY = String.raw`[\w.+-]+@[\w-]+(?:\.[\w-]+)+`;
369
+ const CANDIDATE = new RegExp(String.raw`(?<![a-zA-Z])(?=[hH])${URL_BODY}|(?<=^|[\s(*_[\]~])(?=[wW])${URL_BODY}|(?<![\w.+/-])${EMAIL_BODY}`, "gu");
370
+ const URL_DOMAIN = new RegExp(String.raw`^${URL_START}(${DOMAIN})`, "u");
371
+ const TRAILING_PUNCTUATION = /[!"'*,.:;?_~\]]$/;
372
+ const TRAILING_ENTITY = /&[a-zA-Z][a-zA-Z0-9]*;$/;
373
+ const count = (text, char) => text.split(char).length - 1;
374
+ const trimTrailing = (candidate) => {
375
+ let text = candidate;
376
+ for (;;) {
377
+ const entity = TRAILING_ENTITY.exec(text);
378
+ if (entity) text = text.slice(0, -entity[0].length);
379
+ else if (TRAILING_PUNCTUATION.test(text)) text = text.slice(0, -1);
380
+ else if (text.endsWith(")") && count(text, ")") > count(text, "(")) text = text.slice(0, -1);
381
+ else return text;
382
+ }
383
+ };
384
+ const hasLinkableDomain = (url) => {
385
+ const domain = URL_DOMAIN.exec(url)?.[1];
386
+ return domain !== void 0 && !domain.split(".").slice(-2).some((part) => part.includes("_"));
387
+ };
388
+ /**
389
+ * The bare URLs and email addresses in `text`, as GFM's autolink extension finds them.
390
+ *
391
+ * - `http://`, `https://` or `www.` and a domain, then anything up to whitespace or `<`.
392
+ * - Trailing `!"'*,.:;?_~]`, an unbalanced `)` and an entity like `&amp;` stay outside.
393
+ * - `http(s)://` starts after anything but a letter, `www.` only after whitespace or `(*_[]~`.
394
+ * - An email ending in `-` or `_` is not one.
395
+ */
396
+ const findBareUrls = (text, limit = Infinity) => {
397
+ const found = [];
398
+ for (const match of text.matchAll(CANDIDATE)) {
399
+ const source = match[0];
400
+ if (/^(?:https?:\/\/|www\.)/i.test(source)) {
401
+ const trimmed = trimTrailing(source);
402
+ if (!hasLinkableDomain(trimmed)) continue;
403
+ const url = /^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed;
404
+ found.push({
405
+ index: match.index,
406
+ text: trimmed,
407
+ url
408
+ });
409
+ } else if (!/[-_]$/.test(source)) found.push({
410
+ index: match.index,
411
+ text: source,
412
+ url: `mailto:${source}`
413
+ });
414
+ if (found.length >= limit) break;
415
+ }
416
+ return found;
417
+ };
418
+ /**
419
+ * Every stretch of `text` GFM starts scanning as a URL or email, linked in the end or not.
420
+ *
421
+ * - Export writes these raw: GFM scans the run before trimming its end, and an escape moves where
422
+ * it ends a domain, so `https://a\_b.c\_d` is linked with its backslashes, `https://a_b.c_d` is not.
423
+ */
424
+ const findUrlRuns = (text) => Array.from(text.matchAll(CANDIDATE), (match) => ({
425
+ index: match.index,
426
+ source: match[0]
427
+ }));
428
+ /**
429
+ * A `RegExp` whose `String#match` is the first URL `findBareUrls` finds, for Lexical's `importRegExp`.
430
+ *
431
+ * - Lexical's markdown import reads `importRegExp` only through `text.match(importRegExp)`.
432
+ * - So a candidate GFM rejects (`https://a_b.c_d`) never hides a valid URL after it.
433
+ */
434
+ var FirstBareUrl = class extends RegExp {
435
+ [Symbol.match](text) {
436
+ const [url] = findBareUrls(text, 1);
437
+ if (!url) return null;
438
+ const match = [url.text];
439
+ return Object.assign(match, {
440
+ index: url.index,
441
+ input: text
442
+ });
443
+ }
444
+ };
445
+ const FIRST_BARE_URL = new FirstBareUrl(CANDIDATE.source, "u");
446
+ /** Lexical `AutoLinkPlugin` matcher: the first bare URL in the text, as `findBareUrls` reads it. */
447
+ const matchBareUrl = (text) => {
448
+ const [first] = findBareUrls(text, 1);
449
+ return first ? {
450
+ index: first.index,
451
+ length: first.text.length,
452
+ text: first.text,
453
+ url: first.url
454
+ } : null;
455
+ };
342
456
  /**
343
457
  * Our transformers plus Lexical's built-ins; order matters.
344
458
  *
@@ -409,6 +523,44 @@ const CUSTOM_TRANSFORMERS = [
409
523
  },
410
524
  ...TRANSFORMERS
411
525
  ];
526
+ const urlPlaceholder = (index) => `\uE000${index}\uE001`;
527
+ const URL_PLACEHOLDER = /\uE000(\d+)\uE001/g;
528
+ /** `CUSTOM_TRANSFORMERS` plus bare URLs, for `autoLink`. */
529
+ const AUTO_LINK_TRANSFORMERS = [{
530
+ dependencies: [],
531
+ importRegExp: FIRST_BARE_URL,
532
+ replace: (node) => {
533
+ const literal = $createTextNode(unescapeMarkdownText(node.getTextContent()));
534
+ literal.setFormat(node.getFormat());
535
+ node.replace(literal);
536
+ },
537
+ export: (node, _exportChildren, exportFormat) => {
538
+ if (!$isTextNode(node) || node.hasFormat("code")) return null;
539
+ const parent = node.getParent();
540
+ if ($isLinkNode(parent) && !$isAutoLinkNode(parent)) return null;
541
+ const text = node.getTextContent();
542
+ if (text.includes("")) return null;
543
+ const lead = $isAutoLinkNode(node.getPreviousSibling()) ? /^[^\s<]+/.exec(text)?.[0] ?? "" : "";
544
+ const urls = $isAutoLinkNode(parent) ? [{
545
+ index: 0,
546
+ source: text
547
+ }] : [...lead ? [{
548
+ index: 0,
549
+ source: lead
550
+ }] : [], ...findUrlRuns(text).filter((url) => url.index >= lead.length)];
551
+ if (urls.length === 0) return null;
552
+ let masked = "";
553
+ let end = 0;
554
+ urls.forEach((url, i) => {
555
+ masked += text.slice(end, url.index) + urlPlaceholder(i);
556
+ end = url.index + url.source.length;
557
+ });
558
+ masked += text.slice(end);
559
+ return exportFormat(node, masked).replace(URL_PLACEHOLDER, (_, i) => urls[Number(i)]?.source ?? "");
560
+ },
561
+ regExp: /(?!)/,
562
+ type: "text-match"
563
+ }, ...CUSTOM_TRANSFORMERS];
412
564
  /**
413
565
  * No transformers: `singleLine` mode edits plain-text entity names, not markdown.
414
566
  *
@@ -456,6 +608,10 @@ const preprocessMarkdown = (markdown) => {
456
608
  if (tableLines.length > 0) flushTable();
457
609
  return result.join("\n");
458
610
  };
611
+ /** Replaces the root with `markdown`: the one import a load, a `value` change and a paste share. */
612
+ const $importMarkdown = (markdown, transformers) => {
613
+ $convertFromMarkdownString(preprocessMarkdown(markdown), transformers);
614
+ };
459
615
  /** Unwrap code-fenced tables back to raw GFM table markdown. */
460
616
  const postprocessMarkdown = (markdown) => {
461
617
  const regex = new RegExp(`\`\`\`${TABLE_FENCE_LANG}\\n([\\s\\S]*?)\\n\`\`\``, "g");
@@ -464,6 +620,81 @@ const postprocessMarkdown = (markdown) => {
464
620
  /** Reverses Lexical's export escaping of `* _ ` ~ \`, for `singleLine` plain-text names. */
465
621
  const unescapeMarkdownText = (markdown) => markdown.replace(/\\([*_`~\\])/g, "$1");
466
622
  //#endregion
623
+ //#region src/blocks/MarkdownEditor/plugins/MarkdownPastePlugin.tsx
624
+ /**
625
+ * `markdown` as the top-level nodes the editor's own import makes of it.
626
+ *
627
+ * - Runs in a scratch editor: `$importMarkdown` replaces the root it imports into,
628
+ * and only nodes attached to a root get their lines merged into one paragraph.
629
+ */
630
+ const parseMarkdown = (markdown, transformers) => {
631
+ const scratch = createEditor({
632
+ namespace: "MarkdownEditorPaste",
633
+ nodes: EDITOR_NODES,
634
+ onError: (error) => {
635
+ throw error;
636
+ }
637
+ });
638
+ scratch.update(() => {
639
+ $importMarkdown(markdown, transformers);
640
+ }, { discrete: true });
641
+ return scratch.getEditorState().toJSON().root.children;
642
+ };
643
+ /**
644
+ * Inserts parsed blocks at the selection, keeping a first or last block that is not a paragraph whole.
645
+ *
646
+ * - `insertNodes` merges the first block into a non-empty line and the rest of the line into the
647
+ * last block, which would flatten a fence pasted after `Logs:` into plain text.
648
+ * - An empty paragraph first is never merged, so the block after it goes in whole; it is then removed.
649
+ * - An empty paragraph last takes the rest of the line, and is removed when there was none.
650
+ */
651
+ const $insertBlocks = (selection, nodes) => {
652
+ const first = nodes[0];
653
+ const last = nodes.at(-1);
654
+ if (!first || !last) return;
655
+ const head = $isParagraphNode(first) ? null : $createParagraphNode();
656
+ const tail = $isParagraphNode(last) ? null : $createParagraphNode();
657
+ selection.insertNodes([
658
+ head,
659
+ ...nodes,
660
+ tail
661
+ ].filter((node) => node !== null));
662
+ head?.remove();
663
+ if (!tail) return;
664
+ if (tail.isEmpty()) {
665
+ tail.remove();
666
+ last.selectEnd();
667
+ } else tail.selectStart();
668
+ };
669
+ /**
670
+ * Parses a plain-text paste as markdown and inserts the result at the selection.
671
+ *
672
+ * - Only for a clipboard with `text/plain` and neither `text/html` nor files; those keep
673
+ * Lexical's own paste, and files stay for the image uploader or the consumer.
674
+ * - Inside a code block the text goes in literally, through the same fallback.
675
+ * - One undo step: the update is tagged `history-push`.
676
+ */
677
+ const MarkdownPastePlugin = ({ transformers }) => {
678
+ const [editor] = useLexicalComposerContext();
679
+ useEffect(() => {
680
+ return editor.registerCommand(PASTE_COMMAND, (event) => {
681
+ const data = "clipboardData" in event ? event.clipboardData : null;
682
+ if (!data || data.files.length > 0 || data.types.includes("text/html")) return false;
683
+ const text = data.getData("text/plain").replace(/\r\n?/g, "\n");
684
+ if (!text) return false;
685
+ const selection = $getSelection();
686
+ if (!$isRangeSelection(selection)) return false;
687
+ if ([selection.anchor, selection.focus].some((point) => $findMatchingParent(point.getNode(), $isCodeNode))) return false;
688
+ event.preventDefault();
689
+ $addUpdateTag(PASTE_TAG);
690
+ $addUpdateTag(HISTORY_PUSH_TAG);
691
+ $insertBlocks(selection, parseMarkdown(text, transformers).map($parseSerializedNode));
692
+ return true;
693
+ }, COMMAND_PRIORITY_LOW);
694
+ }, [editor, transformers]);
695
+ return null;
696
+ };
697
+ //#endregion
467
698
  //#region src/blocks/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx
468
699
  const IMPORT_TAG = "markdown-import";
469
700
  const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_TRANSFORMERS, singleLine = false }) => {
@@ -481,7 +712,7 @@ const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_T
481
712
  if (value === lastExportedRef.current) return;
482
713
  lastExportedRef.current = value;
483
714
  editor.update(() => {
484
- $convertFromMarkdownString(preprocessMarkdown(value), transformersRef.current);
715
+ $importMarkdown(value, transformersRef.current);
485
716
  }, { tag: IMPORT_TAG });
486
717
  }, [editor, value]);
487
718
  useEffect(() => {
@@ -822,7 +1053,8 @@ const ELEMENT_FORMAT_TYPES = [
822
1053
  ];
823
1054
  const isElementFormatType = (value) => ELEMENT_FORMAT_TYPES.includes(value);
824
1055
  const toElementFormatType = (value) => isElementFormatType(value) ? value : "left";
825
- const ToolbarPlugin = () => {
1056
+ 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";
1057
+ const ToolbarPlugin = ({ mode = "floating" }) => {
826
1058
  const [editor] = useLexicalComposerContext();
827
1059
  const [isBold, setIsBold] = useState(false);
828
1060
  const [isItalic, setIsItalic] = useState(false);
@@ -837,6 +1069,7 @@ const ToolbarPlugin = () => {
837
1069
  const toolbarRef = useRef(null);
838
1070
  const isMouseDownRef = useRef(false);
839
1071
  const hasSelectionRef = useRef(false);
1072
+ const isFloating = mode === "floating";
840
1073
  const updatePosition = useCallback(() => {
841
1074
  const nativeSelection = window.getSelection();
842
1075
  if (!nativeSelection || nativeSelection.rangeCount === 0) return;
@@ -850,6 +1083,7 @@ const ToolbarPlugin = () => {
850
1083
  }
851
1084
  }, []);
852
1085
  useEffect(() => {
1086
+ if (!isFloating) return;
853
1087
  const handleMouseDown = () => {
854
1088
  isMouseDownRef.current = true;
855
1089
  };
@@ -863,7 +1097,7 @@ const ToolbarPlugin = () => {
863
1097
  document.removeEventListener("mousedown", handleMouseDown);
864
1098
  document.removeEventListener("mouseup", handleMouseUp);
865
1099
  };
866
- }, []);
1100
+ }, [isFloating]);
867
1101
  const updateToolbar = useCallback(() => {
868
1102
  const selection = $getSelection();
869
1103
  if (!$isRangeSelection(selection)) {
@@ -904,116 +1138,124 @@ const ToolbarPlugin = () => {
904
1138
  unregisterUpdate();
905
1139
  };
906
1140
  }, [editor, updateToolbar]);
907
- if (!isVisible || !position) return null;
908
- return createPortal(/* @__PURE__ */ jsxs("div", {
1141
+ if (isFloating && (!isVisible || !position)) return null;
1142
+ const buttons = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1143
+ /* @__PURE__ */ jsx(BlockTypeDropdown, {
1144
+ editor,
1145
+ blockType
1146
+ }),
1147
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1148
+ /* @__PURE__ */ jsx(ToolbarButton, {
1149
+ active: isBold,
1150
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
1151
+ tooltip: "Bold (Ctrl+B)",
1152
+ children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
1153
+ }),
1154
+ /* @__PURE__ */ jsx(ToolbarButton, {
1155
+ active: isItalic,
1156
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
1157
+ tooltip: "Italic (Ctrl+I)",
1158
+ children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
1159
+ }),
1160
+ /* @__PURE__ */ jsx(ToolbarButton, {
1161
+ active: isUnderline,
1162
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
1163
+ tooltip: "Underline (Ctrl+U)",
1164
+ children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
1165
+ }),
1166
+ /* @__PURE__ */ jsx(ToolbarButton, {
1167
+ active: isStrikethrough,
1168
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"),
1169
+ tooltip: "Strikethrough",
1170
+ children: /* @__PURE__ */ jsx(Strikethrough, { className: "tw:size-4" })
1171
+ }),
1172
+ /* @__PURE__ */ jsx(ToolbarButton, {
1173
+ active: isCode,
1174
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"),
1175
+ tooltip: "Inline Code",
1176
+ children: /* @__PURE__ */ jsx(Code, { className: "tw:size-4" })
1177
+ }),
1178
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1179
+ /* @__PURE__ */ jsx(LinkPanel, {
1180
+ editor,
1181
+ isLink
1182
+ }),
1183
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1184
+ /* @__PURE__ */ jsx(ToolbarButton, {
1185
+ active: blockType === "bullet",
1186
+ onClick: () => {
1187
+ if (blockType === "bullet") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
1188
+ else editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
1189
+ },
1190
+ tooltip: "Bullet List",
1191
+ children: /* @__PURE__ */ jsx(List, { className: "tw:size-4" })
1192
+ }),
1193
+ /* @__PURE__ */ jsx(ToolbarButton, {
1194
+ active: blockType === "number",
1195
+ onClick: () => {
1196
+ if (blockType === "number") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
1197
+ else editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
1198
+ },
1199
+ tooltip: "Numbered List",
1200
+ children: /* @__PURE__ */ jsx(ListOrdered, { className: "tw:size-4" })
1201
+ }),
1202
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1203
+ /* @__PURE__ */ jsx(AlignmentDropdown, {
1204
+ editor,
1205
+ alignment
1206
+ }),
1207
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1208
+ /* @__PURE__ */ jsx(ToolbarButton, {
1209
+ active: blockType === "quote",
1210
+ onClick: () => {
1211
+ editor.update(() => {
1212
+ const selection = $getSelection();
1213
+ if (!$isRangeSelection(selection)) return;
1214
+ const anchorNode = selection.anchor.getNode();
1215
+ const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
1216
+ const newNode = blockType === "quote" ? $createParagraphNode() : $createQuoteNode();
1217
+ element.getChildren().forEach((child) => newNode.append(child));
1218
+ element.replace(newNode);
1219
+ });
1220
+ },
1221
+ tooltip: "Quote",
1222
+ children: /* @__PURE__ */ jsx(Quote, { className: "tw:size-4" })
1223
+ }),
1224
+ /* @__PURE__ */ jsx(ToolbarButton, {
1225
+ active: blockType === "code",
1226
+ onClick: () => {
1227
+ editor.update(() => {
1228
+ const selection = $getSelection();
1229
+ if (!$isRangeSelection(selection)) return;
1230
+ const anchorNode = selection.anchor.getNode();
1231
+ const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
1232
+ const newNode = blockType === "code" ? $createParagraphNode() : $createCodeNode();
1233
+ element.getChildren().forEach((child) => newNode.append(child));
1234
+ element.replace(newNode);
1235
+ });
1236
+ },
1237
+ tooltip: "Code Block",
1238
+ children: /* @__PURE__ */ jsx(SquareCode, { className: "tw:size-4" })
1239
+ })
1240
+ ] });
1241
+ if (!isFloating) return /* @__PURE__ */ jsx("div", {
1242
+ role: "toolbar",
1243
+ "aria-label": "Formatting options",
1244
+ onMouseDown: (event) => event.preventDefault(),
1245
+ className: cn(TOOLBAR_BASE_CLASS, "tw:mb-1 tw:flex-wrap tw:bg-background"),
1246
+ children: buttons
1247
+ });
1248
+ return createPortal(/* @__PURE__ */ jsx("div", {
909
1249
  ref: toolbarRef,
910
1250
  role: "toolbar",
911
1251
  "aria-label": "Formatting options",
912
- onMouseDown: (e) => e.preventDefault(),
1252
+ onMouseDown: (event) => event.preventDefault(),
913
1253
  style: {
914
- top: position.top,
915
- left: position.left
1254
+ top: position?.top,
1255
+ left: position?.left
916
1256
  },
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
- ]
1257
+ className: cn(TOOLBAR_BASE_CLASS, "tw:absolute tw:z-[1060] tw:-translate-y-full tw:bg-popover tw:text-popover-foreground tw:shadow-lg"),
1258
+ children: buttons
1017
1259
  }), document.body);
1018
1260
  };
1019
1261
  //#endregion
@@ -1057,21 +1299,15 @@ const editorTheme = {
1057
1299
  };
1058
1300
  //#endregion
1059
1301
  //#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 }) => {
1302
+ const AUTO_LINK_MATCHERS = [matchBareUrl];
1303
+ const AUTO_LINK_EXCLUDED_PARENTS = [$isCodeNode];
1304
+ 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
1305
  const editable = !readOnly && !disabled;
1073
- const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : CUSTOM_TRANSFORMERS;
1306
+ const linkify = autoLink && !singleLine;
1307
+ const markdownPaste = pasteMarkdown && !singleLine;
1308
+ const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : linkify ? AUTO_LINK_TRANSFORMERS : CUSTOM_TRANSFORMERS;
1074
1309
  const useDefaultMinHeight = !compact && minRows === void 0;
1310
+ const contentPadding = compact ? "tw:px-1.5 tw:py-0.5" : "tw:p-2";
1075
1311
  const customMinHeight = !compact && minRows !== void 0 ? { minHeight: `${minRows * 1.75}em` } : void 0;
1076
1312
  const [initialConfig] = useState(() => ({
1077
1313
  namespace: "MarkdownEditor",
@@ -1082,7 +1318,7 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
1082
1318
  },
1083
1319
  editable,
1084
1320
  editorState: () => {
1085
- $convertFromMarkdownString(preprocessMarkdown(value), transformers);
1321
+ $importMarkdown(value, transformers);
1086
1322
  }
1087
1323
  }));
1088
1324
  const skipBlurSaveRef = useRef(false);
@@ -1101,16 +1337,17 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
1101
1337
  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
1338
  onBlur: handleBlur,
1103
1339
  children: [
1104
- showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, {}),
1340
+ showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, { mode: toolbarMode }),
1105
1341
  /* @__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"),
1342
+ 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
1343
  children: /* @__PURE__ */ jsx(RichTextPlugin, {
1108
1344
  contentEditable: /* @__PURE__ */ jsx(ContentEditable, {
1109
- className: cn("tw:outline-hidden", useDefaultMinHeight && "tw:min-h-[100px]"),
1345
+ className: cn("tw:outline-hidden", !compact && "tw:[&>*:first-child]:mt-0 tw:[&>*:last-child]:mb-0", useDefaultMinHeight && "tw:min-h-[100px]"),
1110
1346
  style: customMinHeight,
1111
1347
  "aria-placeholder": placeholder ?? "",
1112
1348
  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)]"),
1349
+ "data-testid": "markdown-editor-placeholder",
1350
+ className: cn("tw:pointer-events-none tw:absolute tw:inset-0 tw:select-none tw:text-muted-foreground", contentPadding),
1114
1351
  children: placeholder
1115
1352
  }) : /* @__PURE__ */ jsx("div", {})
1116
1353
  }),
@@ -1133,6 +1370,11 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
1133
1370
  singleLine
1134
1371
  }),
1135
1372
  /* @__PURE__ */ jsx(ImagePastePlugin, { onImageUpload }),
1373
+ linkify && /* @__PURE__ */ jsx(AutoLinkPlugin, {
1374
+ matchers: AUTO_LINK_MATCHERS,
1375
+ excludeParents: AUTO_LINK_EXCLUDED_PARENTS
1376
+ }),
1377
+ markdownPaste && /* @__PURE__ */ jsx(MarkdownPastePlugin, { transformers }),
1136
1378
  /* @__PURE__ */ jsx(KeyboardPlugin, {
1137
1379
  onSave,
1138
1380
  onCancel,