@mieweb/ui 0.11.0-dev.23 → 0.11.0-dev.25

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.
@@ -533,6 +533,26 @@ function getSpeechRecognition() {
533
533
  return w.SpeechRecognition || w.webkitSpeechRecognition;
534
534
  }
535
535
  var BLOCK_BOUNDARY = /^(BR|DIV|P|LI|UL|OL|TABLE|TR|TD|TH|H[1-6]|BLOCKQUOTE|PRE|HR)$/;
536
+ var MARKDOWN_BLOCK_RULES = {
537
+ "#": { command: "formatBlock", value: "h1" },
538
+ "##": { command: "formatBlock", value: "h2" },
539
+ "###": { command: "formatBlock", value: "h3" },
540
+ "-": { command: "insertUnorderedList" },
541
+ "*": { command: "insertUnorderedList" },
542
+ "1.": { command: "insertOrderedList" },
543
+ ">": { command: "formatBlock", value: "blockquote" }
544
+ };
545
+ function nodeRect(node) {
546
+ if (node.nodeType === Node.ELEMENT_NODE) {
547
+ return node.getBoundingClientRect();
548
+ }
549
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").length > 0) {
550
+ const range = document.createRange();
551
+ range.selectNodeContents(node);
552
+ return range.getBoundingClientRect();
553
+ }
554
+ return null;
555
+ }
536
556
  function lastTextWithin(node) {
537
557
  if (node.nodeType === Node.TEXT_NODE) {
538
558
  return (node.textContent ?? "").length > 0 ? node : null;
@@ -579,6 +599,12 @@ var RichTextEditor = React2__namespace.forwardRef(
579
599
  defaultMacrosOpen = false,
580
600
  macroLabels,
581
601
  onCreateMacro,
602
+ toolbar,
603
+ defaultToolbar = "visible",
604
+ onToolbarChange,
605
+ blockMenu = false,
606
+ blockMenuLabels,
607
+ markdownShortcuts = false,
582
608
  "aria-label": ariaLabel
583
609
  }, ref) => {
584
610
  const editorRef = React2__namespace.useRef(null);
@@ -593,6 +619,19 @@ var RichTextEditor = React2__namespace.forwardRef(
593
619
  const hasVariables = Boolean(variableGroups && variableGroups.length > 0);
594
620
  const hasMacros = Boolean(macros);
595
621
  const isEmpty = chunkC7RVKV25_cjs.isHtmlEmpty(value);
622
+ const [uncontrolledToolbar, setUncontrolledToolbar] = React2__namespace.useState(defaultToolbar);
623
+ const toolbarVisible = (toolbar ?? uncontrolledToolbar) !== "hidden";
624
+ const setToolbarVisibility = (next) => {
625
+ if (toolbar === void 0) setUncontrolledToolbar(next);
626
+ onToolbarChange?.(next);
627
+ };
628
+ const toolbarVisibleRef = React2__namespace.useRef(toolbarVisible);
629
+ const [blockMenuOpen, setBlockMenuOpen] = React2__namespace.useState(false);
630
+ const [gutterTop, setGutterTop] = React2__namespace.useState(16);
631
+ const gutterWrapperRef = React2__namespace.useRef(null);
632
+ const hoverBlockNodeRef = React2__namespace.useRef(null);
633
+ const blockTargetRangeRef = React2__namespace.useRef(null);
634
+ const firstBlockItemRef = React2__namespace.useRef(null);
596
635
  React2__namespace.useImperativeHandle(ref, () => editorRef.current);
597
636
  React2__namespace.useEffect(() => {
598
637
  setSpeechSupported(Boolean(getSpeechRecognition()));
@@ -606,6 +645,16 @@ var RichTextEditor = React2__namespace.forwardRef(
606
645
  }
607
646
  };
608
647
  }, []);
648
+ React2__namespace.useEffect(() => {
649
+ toolbarVisibleRef.current = toolbarVisible;
650
+ if (toolbarVisible) return;
651
+ if (recognitionRef.current && isListeningRef.current) {
652
+ shouldRestartRef.current = false;
653
+ isListeningRef.current = false;
654
+ recognitionRef.current.stop();
655
+ setIsListening(false);
656
+ }
657
+ }, [toolbarVisible]);
609
658
  React2__namespace.useEffect(() => {
610
659
  if (editorRef.current && editorRef.current.innerHTML !== value) {
611
660
  editorRef.current.innerHTML = value;
@@ -744,22 +793,18 @@ var RichTextEditor = React2__namespace.forwardRef(
744
793
  saveSelection();
745
794
  updateContent();
746
795
  };
747
- const expandTypedShortcut = (event) => {
748
- if (disabled || event.key !== " " || // Space during IME composition selects/converts a candidate; never
749
- // hijack it for macro expansion.
750
- event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey || !macros)
751
- return;
796
+ const collectTokenBeforeCaret = () => {
752
797
  const selection = window.getSelection();
753
798
  if (!selection || selection.rangeCount === 0 || !selection.isCollapsed)
754
- return;
799
+ return null;
755
800
  const caret = selection.getRangeAt(0);
756
801
  const node = caret.startContainer;
757
802
  if (node.nodeType !== Node.TEXT_NODE || !editorRef.current?.contains(node))
758
- return;
803
+ return null;
759
804
  const typed = /(\S+)$/.exec(
760
805
  (node.textContent ?? "").slice(0, caret.startOffset)
761
806
  );
762
- if (!typed) return;
807
+ if (!typed) return null;
763
808
  let token = typed[1];
764
809
  let startNode = node;
765
810
  let startOffset = caret.startOffset - typed[1].length;
@@ -773,17 +818,164 @@ var RichTextEditor = React2__namespace.forwardRef(
773
818
  startNode = prev;
774
819
  startOffset = text.length - continued[1].length;
775
820
  }
776
- const lowered = token.toLowerCase();
821
+ const atLineStart = startOffset === 0 && !previousVisibleText(startNode, editorRef.current);
822
+ return {
823
+ token,
824
+ startNode,
825
+ startOffset,
826
+ caretNode: node,
827
+ caretOffset: caret.startOffset,
828
+ atLineStart,
829
+ selection
830
+ };
831
+ };
832
+ const selectCollectedToken = (collected) => {
833
+ const replaceRange = document.createRange();
834
+ replaceRange.setStart(collected.startNode, collected.startOffset);
835
+ replaceRange.setEnd(collected.caretNode, collected.caretOffset);
836
+ collected.selection.removeAllRanges();
837
+ collected.selection.addRange(replaceRange);
838
+ return replaceRange;
839
+ };
840
+ const expandTypedShortcut = (event) => {
841
+ if (!macros) return;
842
+ const collected = collectTokenBeforeCaret();
843
+ if (!collected) return;
844
+ const lowered = collected.token.toLowerCase();
777
845
  const macro = macros.flatMap((group) => group.macros).find((candidate) => candidate.shortcut?.toLowerCase() === lowered);
778
846
  if (!macro) return;
779
847
  event.preventDefault();
780
- const replaceRange = document.createRange();
781
- replaceRange.setStart(startNode, startOffset);
782
- replaceRange.setEnd(node, caret.startOffset);
783
- selection.removeAllRanges();
784
- selection.addRange(replaceRange);
848
+ selectCollectedToken(collected);
785
849
  insertTextAtSelection(macro.content);
786
850
  };
851
+ const applyMarkdownShortcut = (event) => {
852
+ const collected = collectTokenBeforeCaret();
853
+ if (!collected || !collected.atLineStart) return false;
854
+ if (!Object.hasOwn(MARKDOWN_BLOCK_RULES, collected.token)) return false;
855
+ const rule = MARKDOWN_BLOCK_RULES[collected.token];
856
+ event.preventDefault();
857
+ const replaceRange = selectCollectedToken(collected);
858
+ let deleted = false;
859
+ try {
860
+ deleted = document.execCommand("delete", false);
861
+ } catch {
862
+ deleted = false;
863
+ }
864
+ if (!deleted) {
865
+ replaceRange.deleteContents();
866
+ replaceRange.collapse(true);
867
+ collected.selection.removeAllRanges();
868
+ collected.selection.addRange(replaceRange);
869
+ }
870
+ try {
871
+ document.execCommand(rule.command, false, rule.value);
872
+ } catch {
873
+ }
874
+ editorRef.current?.focus();
875
+ saveSelection();
876
+ updateContent();
877
+ return true;
878
+ };
879
+ const handleEditorKeyDown = (event) => {
880
+ if (disabled) return;
881
+ if (blockMenu && event.key === "/" && (event.ctrlKey || event.metaKey) && !event.altKey) {
882
+ event.preventDefault();
883
+ openBlockMenuAtCaret();
884
+ return;
885
+ }
886
+ if (event.key !== " " || // Space during IME composition selects/converts a candidate; never
887
+ // hijack it for shortcut expansion.
888
+ event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey)
889
+ return;
890
+ if (markdownShortcuts && applyMarkdownShortcut(event)) return;
891
+ expandTypedShortcut(event);
892
+ };
893
+ const handleGutterMouseMove = (event) => {
894
+ if (blockMenuOpen || !editorRef.current || !gutterWrapperRef.current)
895
+ return;
896
+ let target = null;
897
+ let top = null;
898
+ for (const child of Array.from(editorRef.current.childNodes)) {
899
+ const rect = nodeRect(child);
900
+ if (!rect) continue;
901
+ if (rect.width === 0 && rect.height === 0) continue;
902
+ if (target === null || rect.top <= event.clientY) {
903
+ target = child;
904
+ top = rect.top;
905
+ }
906
+ }
907
+ if (!target || top === null) return;
908
+ hoverBlockNodeRef.current = target;
909
+ const wrapperTop = gutterWrapperRef.current.getBoundingClientRect().top;
910
+ const next = Math.max(Math.round(top - wrapperTop), 4);
911
+ setGutterTop((current) => current === next ? current : next);
912
+ };
913
+ const rangeAtStartOf = (node) => {
914
+ const range = document.createRange();
915
+ range.setStart(node, 0);
916
+ range.collapse(true);
917
+ return range;
918
+ };
919
+ const handleBlockMenuOpenChange = (open) => {
920
+ if (open) {
921
+ const block = hoverBlockNodeRef.current ?? editorRef.current?.firstChild ?? null;
922
+ blockTargetRangeRef.current = block ? rangeAtStartOf(block) : null;
923
+ } else if (editorRef.current && document.activeElement?.closest('[role="menu"]')) {
924
+ const editor = editorRef.current;
925
+ editor.focus();
926
+ const target = blockTargetRangeRef.current;
927
+ const selection = window.getSelection();
928
+ if (target && selection && editor.contains(target.startContainer)) {
929
+ selection.removeAllRanges();
930
+ selection.addRange(target);
931
+ saveSelection();
932
+ }
933
+ }
934
+ setBlockMenuOpen(open);
935
+ };
936
+ const openBlockMenuAtCaret = () => {
937
+ const selection = window.getSelection();
938
+ if (selection && selection.rangeCount > 0 && editorRef.current?.contains(selection.getRangeAt(0).startContainer)) {
939
+ const caret = selection.getRangeAt(0);
940
+ blockTargetRangeRef.current = caret.cloneRange();
941
+ let node = caret.startContainer;
942
+ while (node.parentNode && node.parentNode !== editorRef.current) {
943
+ node = node.parentNode;
944
+ }
945
+ const rect = nodeRect(node);
946
+ const wrapperRect = gutterWrapperRef.current?.getBoundingClientRect();
947
+ if (rect && wrapperRect) {
948
+ setGutterTop(Math.max(Math.round(rect.top - wrapperRect.top), 4));
949
+ }
950
+ } else {
951
+ blockTargetRangeRef.current = null;
952
+ }
953
+ setBlockMenuOpen(true);
954
+ };
955
+ const applyBlockFormat = (command, value2) => {
956
+ if (disabled || !editorRef.current) return;
957
+ setBlockMenuOpen(false);
958
+ editorRef.current.focus();
959
+ const target = blockTargetRangeRef.current;
960
+ const selection = window.getSelection();
961
+ if (target && selection && editorRef.current.contains(target.startContainer)) {
962
+ selection.removeAllRanges();
963
+ selection.addRange(target);
964
+ }
965
+ try {
966
+ document.execCommand(command, false, value2);
967
+ } catch {
968
+ }
969
+ saveSelection();
970
+ updateContent();
971
+ };
972
+ React2__namespace.useEffect(() => {
973
+ if (!blockMenuOpen) return;
974
+ const frame = requestAnimationFrame(
975
+ () => firstBlockItemRef.current?.focus()
976
+ );
977
+ return () => cancelAnimationFrame(frame);
978
+ }, [blockMenuOpen]);
787
979
  const toggleDictation = async () => {
788
980
  const SpeechRecognition = getSpeechRecognition();
789
981
  if (!SpeechRecognition) return;
@@ -805,6 +997,7 @@ var RichTextEditor = React2__namespace.forwardRef(
805
997
  );
806
998
  return;
807
999
  }
1000
+ if (!toolbarVisibleRef.current) return;
808
1001
  const recognition = new SpeechRecognition();
809
1002
  recognition.continuous = true;
810
1003
  recognition.interimResults = true;
@@ -881,6 +1074,43 @@ var RichTextEditor = React2__namespace.forwardRef(
881
1074
  }
882
1075
  };
883
1076
  const iconButtonClass = "h-8 w-8 p-0";
1077
+ const blockFormatItems = [
1078
+ { key: "text", label: "Text", command: "formatBlock", value: "p" },
1079
+ {
1080
+ key: "heading1",
1081
+ label: "Heading 1",
1082
+ command: "formatBlock",
1083
+ value: "h1"
1084
+ },
1085
+ {
1086
+ key: "heading2",
1087
+ label: "Heading 2",
1088
+ command: "formatBlock",
1089
+ value: "h2"
1090
+ },
1091
+ {
1092
+ key: "heading3",
1093
+ label: "Heading 3",
1094
+ command: "formatBlock",
1095
+ value: "h3"
1096
+ },
1097
+ {
1098
+ key: "bulletList",
1099
+ label: "Bullet list",
1100
+ command: "insertUnorderedList"
1101
+ },
1102
+ {
1103
+ key: "numberedList",
1104
+ label: "Numbered list",
1105
+ command: "insertOrderedList"
1106
+ },
1107
+ {
1108
+ key: "quote",
1109
+ label: "Quote",
1110
+ command: "formatBlock",
1111
+ value: "blockquote"
1112
+ }
1113
+ ];
884
1114
  return /* @__PURE__ */ jsxRuntime.jsxs(
885
1115
  "div",
886
1116
  {
@@ -890,7 +1120,7 @@ var RichTextEditor = React2__namespace.forwardRef(
890
1120
  className
891
1121
  ),
892
1122
  children: [
893
- /* @__PURE__ */ jsxRuntime.jsxs(
1123
+ toolbarVisible && /* @__PURE__ */ jsxRuntime.jsxs(
894
1124
  "div",
895
1125
  {
896
1126
  "data-slot": "rich-text-editor-toolbar",
@@ -1078,7 +1308,10 @@ var RichTextEditor = React2__namespace.forwardRef(
1078
1308
  size: "sm",
1079
1309
  onClick: toggleDictation,
1080
1310
  disabled: disabled || !speechSupported,
1081
- className: chunkS5RVSXDV_cjs.cn("h-8 gap-1 px-2", isListening && "animate-pulse"),
1311
+ className: chunkS5RVSXDV_cjs.cn(
1312
+ "h-8 gap-1 px-2",
1313
+ isListening && "animate-pulse"
1314
+ ),
1082
1315
  title: !speechSupported ? "Speech recognition not supported in this browser" : isListening ? "Stop dictation" : "Start dictation",
1083
1316
  children: [
1084
1317
  isListening ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: "h-4 w-4" }),
@@ -1115,16 +1348,98 @@ var RichTextEditor = React2__namespace.forwardRef(
1115
1348
  /* @__PURE__ */ jsxRuntime.jsxs(
1116
1349
  "div",
1117
1350
  {
1351
+ ref: gutterWrapperRef,
1118
1352
  className: chunkS5RVSXDV_cjs.cn(
1119
1353
  "relative flex-1",
1354
+ blockMenu && "group/rte-body",
1120
1355
  hasMacros && macrosOpen && "md:me-72"
1121
1356
  ),
1357
+ onMouseMove: blockMenu && !disabled ? handleGutterMouseMove : void 0,
1122
1358
  children: [
1359
+ blockMenu && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
1360
+ "div",
1361
+ {
1362
+ "data-slot": "rich-text-editor-block-gutter",
1363
+ className: "absolute start-1 z-10",
1364
+ style: { top: gutterTop },
1365
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1366
+ chunkPINIBEIU_cjs.Dropdown,
1367
+ {
1368
+ open: blockMenuOpen,
1369
+ onOpenChange: handleBlockMenuOpenChange,
1370
+ placement: "bottom-start",
1371
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(
1372
+ chunkANMX7YDL_cjs.Button,
1373
+ {
1374
+ type: "button",
1375
+ variant: "ghost",
1376
+ size: "sm",
1377
+ className: chunkS5RVSXDV_cjs.cn(
1378
+ "h-6 w-6 p-0",
1379
+ // Revealed on hover/focus; always visible on touch,
1380
+ // which has no hover to reveal it with.
1381
+ "opacity-0 transition-opacity",
1382
+ "group-hover/rte-body:opacity-100",
1383
+ "focus-visible:opacity-100",
1384
+ "[@media(hover:none)]:opacity-100",
1385
+ blockMenuOpen && "opacity-100"
1386
+ ),
1387
+ "aria-label": blockMenuLabels?.blockMenu ?? "Block menu",
1388
+ title: blockMenuLabels?.blockMenu ?? "Block menu",
1389
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" })
1390
+ }
1391
+ ),
1392
+ children: [
1393
+ blockFormatItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
1394
+ chunkPINIBEIU_cjs.DropdownItem,
1395
+ {
1396
+ ref: index === 0 ? firstBlockItemRef : void 0,
1397
+ onClick: () => applyBlockFormat(item.command, item.value),
1398
+ children: blockMenuLabels?.[item.key] ?? item.label
1399
+ },
1400
+ item.key
1401
+ )),
1402
+ /* @__PURE__ */ jsxRuntime.jsx(chunkPINIBEIU_cjs.DropdownSeparator, {}),
1403
+ hasMacros && /* @__PURE__ */ jsxRuntime.jsx(
1404
+ chunkPINIBEIU_cjs.DropdownItem,
1405
+ {
1406
+ onClick: () => {
1407
+ setBlockMenuOpen(false);
1408
+ if (blockTargetRangeRef.current) {
1409
+ savedRangeRef.current = blockTargetRangeRef.current;
1410
+ }
1411
+ setMacrosOpen(true);
1412
+ focusRestoringSelection();
1413
+ },
1414
+ children: blockMenuLabels?.insertMacro ?? "Insert macro\u2026"
1415
+ }
1416
+ ),
1417
+ /* @__PURE__ */ jsxRuntime.jsx(
1418
+ chunkPINIBEIU_cjs.DropdownItem,
1419
+ {
1420
+ onClick: () => {
1421
+ setBlockMenuOpen(false);
1422
+ setToolbarVisibility(
1423
+ toolbarVisible ? "hidden" : "visible"
1424
+ );
1425
+ editorRef.current?.focus();
1426
+ },
1427
+ children: toolbarVisible ? blockMenuLabels?.hideToolbar ?? "Hide toolbar" : blockMenuLabels?.showToolbar ?? "Show toolbar"
1428
+ }
1429
+ )
1430
+ ]
1431
+ }
1432
+ )
1433
+ }
1434
+ ),
1123
1435
  isEmpty && placeholder && /* @__PURE__ */ jsxRuntime.jsx(
1124
1436
  "div",
1125
1437
  {
1126
1438
  "aria-hidden": "true",
1127
- className: "text-muted-foreground pointer-events-none absolute start-0 top-0 p-4",
1439
+ className: chunkS5RVSXDV_cjs.cn(
1440
+ "text-muted-foreground pointer-events-none absolute start-0 top-0 p-4",
1441
+ blockMenu && "ps-10"
1442
+ ),
1128
1443
  children: placeholder
1129
1444
  }
1130
1445
  ),
@@ -1141,12 +1456,23 @@ var RichTextEditor = React2__namespace.forwardRef(
1141
1456
  onPaste: handlePaste,
1142
1457
  onDrop: handleDrop,
1143
1458
  onBlur: hasMacros ? saveSelection : void 0,
1144
- onKeyDown: hasMacros ? expandTypedShortcut : void 0,
1459
+ onKeyDown: hasMacros || markdownShortcuts || blockMenu ? handleEditorKeyDown : void 0,
1145
1460
  onKeyUp: hasMacros ? saveSelection : void 0,
1146
1461
  onMouseUp: hasMacros ? saveSelection : void 0,
1147
1462
  className: chunkS5RVSXDV_cjs.cn(
1148
1463
  "min-h-[250px] overflow-y-auto p-4 focus:outline-none",
1149
1464
  "prose prose-sm max-w-none",
1465
+ // Baseline block styles: Tailwind preflight strips heading,
1466
+ // list and quote styling, and the `prose` classes above only
1467
+ // work when the host installs @tailwindcss/typography —
1468
+ // without these, block formats would apply invisibly.
1469
+ "[&_h1]:my-3 [&_h1]:text-2xl [&_h1]:font-bold",
1470
+ "[&_h2]:my-3 [&_h2]:text-xl [&_h2]:font-bold",
1471
+ "[&_h3]:my-2 [&_h3]:text-lg [&_h3]:font-semibold",
1472
+ "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:ps-6",
1473
+ "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:ps-6",
1474
+ "[&_blockquote]:border-border [&_blockquote]:text-muted-foreground [&_blockquote]:my-2 [&_blockquote]:border-s-4 [&_blockquote]:ps-3",
1475
+ blockMenu && "ps-10",
1150
1476
  hasMacros && macrosOpen && "md:min-h-[400px]",
1151
1477
  disabled && "bg-muted/20 cursor-not-allowed text-neutral-700 dark:text-neutral-300"
1152
1478
  ),
@@ -1184,5 +1510,5 @@ exports.MacroLibrary = MacroLibrary;
1184
1510
  exports.RichTextEditor = RichTextEditor;
1185
1511
  exports.convertAngleBracketsToMustache = convertAngleBracketsToMustache;
1186
1512
  exports.processDictation = processDictation;
1187
- //# sourceMappingURL=chunk-7BA7ENN5.cjs.map
1188
- //# sourceMappingURL=chunk-7BA7ENN5.cjs.map
1513
+ //# sourceMappingURL=chunk-3S5LK2EW.cjs.map
1514
+ //# sourceMappingURL=chunk-3S5LK2EW.cjs.map