@mieweb/ui 0.11.0-dev.24 → 0.11.0-dev.26

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 (32) hide show
  1. package/dist/{chunk-4W3QJWG2.cjs → chunk-45VC3ZMF.cjs} +32 -3
  2. package/dist/chunk-45VC3ZMF.cjs.map +1 -0
  3. package/dist/{chunk-HKLTJYRU.cjs → chunk-DUTEYAKL.cjs} +4 -4
  4. package/dist/{chunk-HKLTJYRU.cjs.map → chunk-DUTEYAKL.cjs.map} +1 -1
  5. package/dist/{chunk-IYY7NKA3.js → chunk-OEJ4TCXY.js} +304 -11
  6. package/dist/chunk-OEJ4TCXY.js.map +1 -0
  7. package/dist/{chunk-VEPARPUE.cjs → chunk-PIE6M2MT.cjs} +303 -10
  8. package/dist/chunk-PIE6M2MT.cjs.map +1 -0
  9. package/dist/{chunk-GTZUG646.js → chunk-SEE4ZPQS.js} +3 -3
  10. package/dist/{chunk-GTZUG646.js.map → chunk-SEE4ZPQS.js.map} +1 -1
  11. package/dist/{chunk-V43ZWRBT.js → chunk-YJ7MKZTI.js} +32 -3
  12. package/dist/chunk-YJ7MKZTI.js.map +1 -0
  13. package/dist/components/EmailEditor/index.cjs +11 -11
  14. package/dist/components/EmailEditor/index.js +2 -2
  15. package/dist/components/RichTextEditor/index.cjs +4 -4
  16. package/dist/components/RichTextEditor/index.d.cts +58 -5
  17. package/dist/components/RichTextEditor/index.d.ts +58 -5
  18. package/dist/components/RichTextEditor/index.js +1 -1
  19. package/dist/index.cjs +41 -18
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +79 -5
  22. package/dist/index.d.ts +79 -5
  23. package/dist/index.js +27 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/tailwind-preset.cjs +4 -4
  27. package/dist/tailwind-preset.js +1 -1
  28. package/package.json +1 -1
  29. package/dist/chunk-4W3QJWG2.cjs.map +0 -1
  30. package/dist/chunk-IYY7NKA3.js.map +0 -1
  31. package/dist/chunk-V43ZWRBT.js.map +0 -1
  32. package/dist/chunk-VEPARPUE.cjs.map +0 -1
@@ -542,6 +542,17 @@ var MARKDOWN_BLOCK_RULES = {
542
542
  "1.": { command: "insertOrderedList" },
543
543
  ">": { command: "formatBlock", value: "blockquote" }
544
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
+ }
545
556
  function lastTextWithin(node) {
546
557
  if (node.nodeType === Node.TEXT_NODE) {
547
558
  return (node.textContent ?? "").length > 0 ? node : null;
@@ -588,8 +599,13 @@ var RichTextEditor = React2__namespace.forwardRef(
588
599
  defaultMacrosOpen = false,
589
600
  macroLabels,
590
601
  onCreateMacro,
591
- toolbar = "visible",
602
+ toolbar,
603
+ defaultToolbar = "visible",
604
+ onToolbarChange,
605
+ blockMenu = false,
606
+ blockMenuLabels,
592
607
  markdownShortcuts = false,
608
+ chromeless = false,
593
609
  "aria-label": ariaLabel
594
610
  }, ref) => {
595
611
  const editorRef = React2__namespace.useRef(null);
@@ -604,8 +620,46 @@ var RichTextEditor = React2__namespace.forwardRef(
604
620
  const hasVariables = Boolean(variableGroups && variableGroups.length > 0);
605
621
  const hasMacros = Boolean(macros);
606
622
  const isEmpty = chunkC7RVKV25_cjs.isHtmlEmpty(value);
607
- const toolbarVisible = toolbar !== "hidden";
623
+ const [uncontrolledToolbar, setUncontrolledToolbar] = React2__namespace.useState(defaultToolbar);
624
+ const toolbarVisible = (toolbar ?? uncontrolledToolbar) !== "hidden";
625
+ const setToolbarVisibility = (next) => {
626
+ if (toolbar === void 0) setUncontrolledToolbar(next);
627
+ onToolbarChange?.(next);
628
+ };
608
629
  const toolbarVisibleRef = React2__namespace.useRef(toolbarVisible);
630
+ const [blockMenuOpen, setBlockMenuOpen] = React2__namespace.useState(false);
631
+ const [gutterTop, setGutterTop] = React2__namespace.useState(chromeless ? 7 : 16);
632
+ const gutterWrapperRef = React2__namespace.useRef(null);
633
+ const measureRestGutterTop = React2__namespace.useCallback(() => {
634
+ const fallback = chromeless ? 7 : 16;
635
+ if (!editorRef.current) return fallback;
636
+ const padding = parseFloat(
637
+ getComputedStyle(editorRef.current).paddingTop
638
+ );
639
+ return Number.isNaN(padding) ? fallback : Math.round(padding);
640
+ }, [chromeless]);
641
+ React2__namespace.useLayoutEffect(() => {
642
+ if (!blockMenu || !editorRef.current) return;
643
+ setGutterTop((current) => {
644
+ const next = measureRestGutterTop();
645
+ return current === next ? current : next;
646
+ });
647
+ }, [blockMenu, measureRestGutterTop]);
648
+ const hoverBlockNodeRef = React2__namespace.useRef(null);
649
+ const resetGutter = React2__namespace.useCallback(() => {
650
+ hoverBlockNodeRef.current = null;
651
+ const next = measureRestGutterTop();
652
+ setGutterTop((current) => current === next ? current : next);
653
+ }, [measureRestGutterTop]);
654
+ const reconcileGutterWithContent = React2__namespace.useCallback(() => {
655
+ if (!blockMenu || !editorRef.current) return;
656
+ const hovered = hoverBlockNodeRef.current;
657
+ if (hovered && !editorRef.current.contains(hovered) || !editorRef.current.hasChildNodes()) {
658
+ resetGutter();
659
+ }
660
+ }, [blockMenu, resetGutter]);
661
+ const blockTargetRangeRef = React2__namespace.useRef(null);
662
+ const firstBlockItemRef = React2__namespace.useRef(null);
609
663
  React2__namespace.useImperativeHandle(ref, () => editorRef.current);
610
664
  React2__namespace.useEffect(() => {
611
665
  setSpeechSupported(Boolean(getSpeechRecognition()));
@@ -632,10 +686,12 @@ var RichTextEditor = React2__namespace.forwardRef(
632
686
  React2__namespace.useEffect(() => {
633
687
  if (editorRef.current && editorRef.current.innerHTML !== value) {
634
688
  editorRef.current.innerHTML = value;
689
+ reconcileGutterWithContent();
635
690
  }
636
- }, [value]);
691
+ }, [value, reconcileGutterWithContent]);
637
692
  const updateContent = () => {
638
693
  if (editorRef.current) onChange(editorRef.current.innerHTML);
694
+ reconcileGutterWithContent();
639
695
  };
640
696
  const insertSanitized = (html) => {
641
697
  if (!sanitizeHtml) return false;
@@ -851,13 +907,108 @@ var RichTextEditor = React2__namespace.forwardRef(
851
907
  return true;
852
908
  };
853
909
  const handleEditorKeyDown = (event) => {
854
- if (disabled || event.key !== " " || // Space during IME composition selects/converts a candidate; never
910
+ if (disabled) return;
911
+ if (blockMenu && event.key === "/" && (event.ctrlKey || event.metaKey) && !event.altKey) {
912
+ event.preventDefault();
913
+ openBlockMenuAtCaret();
914
+ return;
915
+ }
916
+ if (event.key !== " " || // Space during IME composition selects/converts a candidate; never
855
917
  // hijack it for shortcut expansion.
856
918
  event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey)
857
919
  return;
858
920
  if (markdownShortcuts && applyMarkdownShortcut(event)) return;
859
921
  expandTypedShortcut(event);
860
922
  };
923
+ const handleGutterMouseMove = (event) => {
924
+ if (blockMenuOpen || !editorRef.current || !gutterWrapperRef.current)
925
+ return;
926
+ let target = null;
927
+ let top = null;
928
+ for (const child of Array.from(editorRef.current.childNodes)) {
929
+ const rect = nodeRect(child);
930
+ if (!rect) continue;
931
+ if (rect.width === 0 && rect.height === 0) continue;
932
+ if (target === null || rect.top <= event.clientY) {
933
+ target = child;
934
+ top = rect.top;
935
+ }
936
+ }
937
+ if (!target || top === null) {
938
+ resetGutter();
939
+ return;
940
+ }
941
+ hoverBlockNodeRef.current = target;
942
+ const wrapperTop = gutterWrapperRef.current.getBoundingClientRect().top;
943
+ const next = Math.max(Math.round(top - wrapperTop), 4);
944
+ setGutterTop((current) => current === next ? current : next);
945
+ };
946
+ const rangeAtStartOf = (node) => {
947
+ const range = document.createRange();
948
+ range.setStart(node, 0);
949
+ range.collapse(true);
950
+ return range;
951
+ };
952
+ const handleBlockMenuOpenChange = (open) => {
953
+ if (open) {
954
+ const block = hoverBlockNodeRef.current ?? editorRef.current?.firstChild ?? null;
955
+ blockTargetRangeRef.current = block ? rangeAtStartOf(block) : null;
956
+ } else if (editorRef.current && document.activeElement?.closest('[role="menu"]')) {
957
+ const editor = editorRef.current;
958
+ editor.focus();
959
+ const target = blockTargetRangeRef.current;
960
+ const selection = window.getSelection();
961
+ if (target && selection && editor.contains(target.startContainer)) {
962
+ selection.removeAllRanges();
963
+ selection.addRange(target);
964
+ saveSelection();
965
+ }
966
+ }
967
+ setBlockMenuOpen(open);
968
+ };
969
+ const openBlockMenuAtCaret = () => {
970
+ const selection = window.getSelection();
971
+ if (selection && selection.rangeCount > 0 && editorRef.current?.contains(selection.getRangeAt(0).startContainer)) {
972
+ const caret = selection.getRangeAt(0);
973
+ blockTargetRangeRef.current = caret.cloneRange();
974
+ let node = caret.startContainer;
975
+ while (node.parentNode && node.parentNode !== editorRef.current) {
976
+ node = node.parentNode;
977
+ }
978
+ const rect = nodeRect(node);
979
+ const wrapperRect = gutterWrapperRef.current?.getBoundingClientRect();
980
+ if (rect && wrapperRect) {
981
+ setGutterTop(Math.max(Math.round(rect.top - wrapperRect.top), 4));
982
+ }
983
+ } else {
984
+ blockTargetRangeRef.current = null;
985
+ }
986
+ setBlockMenuOpen(true);
987
+ };
988
+ const applyBlockFormat = (command, value2) => {
989
+ if (disabled || !editorRef.current) return;
990
+ setBlockMenuOpen(false);
991
+ editorRef.current.focus();
992
+ const target = blockTargetRangeRef.current;
993
+ const selection = window.getSelection();
994
+ if (target && selection && editorRef.current.contains(target.startContainer)) {
995
+ selection.removeAllRanges();
996
+ selection.addRange(target);
997
+ }
998
+ try {
999
+ document.execCommand(command, false, value2);
1000
+ } catch {
1001
+ }
1002
+ saveSelection();
1003
+ updateContent();
1004
+ };
1005
+ React2__namespace.useEffect(() => {
1006
+ if (!blockMenuOpen) return;
1007
+ const frame = requestAnimationFrame(
1008
+ () => firstBlockItemRef.current?.focus()
1009
+ );
1010
+ return () => cancelAnimationFrame(frame);
1011
+ }, [blockMenuOpen]);
861
1012
  const toggleDictation = async () => {
862
1013
  const SpeechRecognition = getSpeechRecognition();
863
1014
  if (!SpeechRecognition) return;
@@ -956,12 +1107,51 @@ var RichTextEditor = React2__namespace.forwardRef(
956
1107
  }
957
1108
  };
958
1109
  const iconButtonClass = "h-8 w-8 p-0";
1110
+ const blockFormatItems = [
1111
+ { key: "text", label: "Text", command: "formatBlock", value: "p" },
1112
+ {
1113
+ key: "heading1",
1114
+ label: "Heading 1",
1115
+ command: "formatBlock",
1116
+ value: "h1"
1117
+ },
1118
+ {
1119
+ key: "heading2",
1120
+ label: "Heading 2",
1121
+ command: "formatBlock",
1122
+ value: "h2"
1123
+ },
1124
+ {
1125
+ key: "heading3",
1126
+ label: "Heading 3",
1127
+ command: "formatBlock",
1128
+ value: "h3"
1129
+ },
1130
+ {
1131
+ key: "bulletList",
1132
+ label: "Bullet list",
1133
+ command: "insertUnorderedList"
1134
+ },
1135
+ {
1136
+ key: "numberedList",
1137
+ label: "Numbered list",
1138
+ command: "insertOrderedList"
1139
+ },
1140
+ {
1141
+ key: "quote",
1142
+ label: "Quote",
1143
+ command: "formatBlock",
1144
+ value: "blockquote"
1145
+ }
1146
+ ];
959
1147
  return /* @__PURE__ */ jsxRuntime.jsxs(
960
1148
  "div",
961
1149
  {
962
1150
  "data-slot": "rich-text-editor",
1151
+ "data-chromeless": chromeless || void 0,
963
1152
  className: chunkS5RVSXDV_cjs.cn(
964
- "border-border bg-background flex flex-col overflow-hidden rounded-lg border",
1153
+ "bg-background flex flex-col",
1154
+ !chromeless && "border-border overflow-hidden rounded-lg border",
965
1155
  className
966
1156
  ),
967
1157
  children: [
@@ -1193,16 +1383,103 @@ var RichTextEditor = React2__namespace.forwardRef(
1193
1383
  /* @__PURE__ */ jsxRuntime.jsxs(
1194
1384
  "div",
1195
1385
  {
1386
+ ref: gutterWrapperRef,
1196
1387
  className: chunkS5RVSXDV_cjs.cn(
1197
1388
  "relative flex-1",
1389
+ blockMenu && "group/rte-body",
1198
1390
  hasMacros && macrosOpen && "md:me-72"
1199
1391
  ),
1392
+ onMouseMove: blockMenu && !disabled ? handleGutterMouseMove : void 0,
1200
1393
  children: [
1394
+ blockMenu && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
1395
+ "div",
1396
+ {
1397
+ "data-slot": "rich-text-editor-block-gutter",
1398
+ className: "absolute start-1 z-10",
1399
+ style: { top: gutterTop },
1400
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1401
+ chunkPINIBEIU_cjs.Dropdown,
1402
+ {
1403
+ open: blockMenuOpen,
1404
+ onOpenChange: handleBlockMenuOpenChange,
1405
+ placement: "bottom-start",
1406
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(
1407
+ chunkANMX7YDL_cjs.Button,
1408
+ {
1409
+ type: "button",
1410
+ variant: "ghost",
1411
+ size: "sm",
1412
+ className: chunkS5RVSXDV_cjs.cn(
1413
+ "h-6 w-6 p-0",
1414
+ // Revealed on hover/focus; always visible on touch,
1415
+ // which has no hover to reveal it with.
1416
+ "opacity-0 transition-opacity",
1417
+ "group-hover/rte-body:opacity-100",
1418
+ "focus-visible:opacity-100",
1419
+ "[@media(hover:none)]:opacity-100",
1420
+ blockMenuOpen && "opacity-100"
1421
+ ),
1422
+ "aria-label": blockMenuLabels?.blockMenu ?? "Block menu",
1423
+ title: blockMenuLabels?.blockMenu ?? "Block menu",
1424
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" })
1425
+ }
1426
+ ),
1427
+ children: [
1428
+ blockFormatItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
1429
+ chunkPINIBEIU_cjs.DropdownItem,
1430
+ {
1431
+ ref: index === 0 ? firstBlockItemRef : void 0,
1432
+ onClick: () => applyBlockFormat(item.command, item.value),
1433
+ children: blockMenuLabels?.[item.key] ?? item.label
1434
+ },
1435
+ item.key
1436
+ )),
1437
+ /* @__PURE__ */ jsxRuntime.jsx(chunkPINIBEIU_cjs.DropdownSeparator, {}),
1438
+ hasMacros && /* @__PURE__ */ jsxRuntime.jsx(
1439
+ chunkPINIBEIU_cjs.DropdownItem,
1440
+ {
1441
+ onClick: () => {
1442
+ setBlockMenuOpen(false);
1443
+ if (blockTargetRangeRef.current) {
1444
+ savedRangeRef.current = blockTargetRangeRef.current;
1445
+ }
1446
+ setMacrosOpen(true);
1447
+ focusRestoringSelection();
1448
+ },
1449
+ children: blockMenuLabels?.insertMacro ?? "Insert macro\u2026"
1450
+ }
1451
+ ),
1452
+ /* @__PURE__ */ jsxRuntime.jsx(
1453
+ chunkPINIBEIU_cjs.DropdownItem,
1454
+ {
1455
+ onClick: () => {
1456
+ setBlockMenuOpen(false);
1457
+ setToolbarVisibility(
1458
+ toolbarVisible ? "hidden" : "visible"
1459
+ );
1460
+ editorRef.current?.focus();
1461
+ },
1462
+ children: toolbarVisible ? blockMenuLabels?.hideToolbar ?? "Hide toolbar" : blockMenuLabels?.showToolbar ?? "Show toolbar"
1463
+ }
1464
+ )
1465
+ ]
1466
+ }
1467
+ )
1468
+ }
1469
+ ),
1201
1470
  isEmpty && placeholder && /* @__PURE__ */ jsxRuntime.jsx(
1202
1471
  "div",
1203
1472
  {
1204
1473
  "aria-hidden": "true",
1205
- className: "text-muted-foreground pointer-events-none absolute start-0 top-0 p-4",
1474
+ className: chunkS5RVSXDV_cjs.cn(
1475
+ "text-muted-foreground pointer-events-none absolute start-0 top-0",
1476
+ // Chromeless hosts drop the wrapper's clipping, so a long
1477
+ // placeholder must stay a single ellipsized line (as a
1478
+ // native textarea clips its own) instead of wrapping out
1479
+ // of the one-row field.
1480
+ chromeless ? "end-0 truncate px-3 py-[7px] text-base" : "p-4",
1481
+ blockMenu && "ps-10"
1482
+ ),
1206
1483
  children: placeholder
1207
1484
  }
1208
1485
  ),
@@ -1214,17 +1491,33 @@ var RichTextEditor = React2__namespace.forwardRef(
1214
1491
  "aria-multiline": "true",
1215
1492
  "aria-label": ariaLabel ?? placeholder,
1216
1493
  contentEditable: !disabled,
1494
+ "data-macros-open": hasMacros && macrosOpen ? "" : void 0,
1217
1495
  tabIndex: disabled ? -1 : 0,
1218
1496
  onInput: updateContent,
1219
1497
  onPaste: handlePaste,
1220
1498
  onDrop: handleDrop,
1221
1499
  onBlur: hasMacros ? saveSelection : void 0,
1222
- onKeyDown: hasMacros || markdownShortcuts ? handleEditorKeyDown : void 0,
1500
+ onKeyDown: hasMacros || markdownShortcuts || blockMenu ? handleEditorKeyDown : void 0,
1223
1501
  onKeyUp: hasMacros ? saveSelection : void 0,
1224
1502
  onMouseUp: hasMacros ? saveSelection : void 0,
1225
1503
  className: chunkS5RVSXDV_cjs.cn(
1226
- "min-h-[250px] overflow-y-auto p-4 focus:outline-none",
1504
+ "overflow-y-auto focus:outline-none",
1505
+ // Chromeless surfaces start one Input row tall (Textarea md
1506
+ // metrics: 24px line + 7px padding + the host's 1px borders
1507
+ // = h-10) and auto-grow with content.
1508
+ chromeless ? "min-h-[38px] px-3 py-[7px] text-base" : "min-h-[250px] p-4",
1227
1509
  "prose prose-sm max-w-none",
1510
+ // Baseline block styles: Tailwind preflight strips heading,
1511
+ // list and quote styling, and the `prose` classes above only
1512
+ // work when the host installs @tailwindcss/typography —
1513
+ // without these, block formats would apply invisibly.
1514
+ "[&_h1]:my-3 [&_h1]:text-2xl [&_h1]:font-bold",
1515
+ "[&_h2]:my-3 [&_h2]:text-xl [&_h2]:font-bold",
1516
+ "[&_h3]:my-2 [&_h3]:text-lg [&_h3]:font-semibold",
1517
+ "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:ps-6",
1518
+ "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:ps-6",
1519
+ "[&_blockquote]:border-border [&_blockquote]:text-muted-foreground [&_blockquote]:my-2 [&_blockquote]:border-s-4 [&_blockquote]:ps-3",
1520
+ blockMenu && "ps-10",
1228
1521
  hasMacros && macrosOpen && "md:min-h-[400px]",
1229
1522
  disabled && "bg-muted/20 cursor-not-allowed text-neutral-700 dark:text-neutral-300"
1230
1523
  ),
@@ -1262,5 +1555,5 @@ exports.MacroLibrary = MacroLibrary;
1262
1555
  exports.RichTextEditor = RichTextEditor;
1263
1556
  exports.convertAngleBracketsToMustache = convertAngleBracketsToMustache;
1264
1557
  exports.processDictation = processDictation;
1265
- //# sourceMappingURL=chunk-VEPARPUE.cjs.map
1266
- //# sourceMappingURL=chunk-VEPARPUE.cjs.map
1558
+ //# sourceMappingURL=chunk-PIE6M2MT.cjs.map
1559
+ //# sourceMappingURL=chunk-PIE6M2MT.cjs.map