@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
@@ -6,7 +6,7 @@ import { X, Search, History, BookOpen, ChevronRight, ChevronDown, Plus } from '.
6
6
  import { Button } from './chunk-WFBUBT54.js';
7
7
  import { cn } from './chunk-TQONQSJO.js';
8
8
  import * as React2 from 'react';
9
- import { Bold, Italic, Underline, List, ListOrdered, AlignLeft, AlignCenter, AlignRight, Braces, Replace, MicOff, Mic, BookOpen as BookOpen$1 } from 'lucide-react';
9
+ import { Bold, Italic, Underline, List, ListOrdered, AlignLeft, AlignCenter, AlignRight, Braces, Replace, MicOff, Mic, BookOpen as BookOpen$1, Plus as Plus$1 } from 'lucide-react';
10
10
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
11
11
 
12
12
  var DEFAULT_LABELS = {
@@ -520,6 +520,17 @@ var MARKDOWN_BLOCK_RULES = {
520
520
  "1.": { command: "insertOrderedList" },
521
521
  ">": { command: "formatBlock", value: "blockquote" }
522
522
  };
523
+ function nodeRect(node) {
524
+ if (node.nodeType === Node.ELEMENT_NODE) {
525
+ return node.getBoundingClientRect();
526
+ }
527
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").length > 0) {
528
+ const range = document.createRange();
529
+ range.selectNodeContents(node);
530
+ return range.getBoundingClientRect();
531
+ }
532
+ return null;
533
+ }
523
534
  function lastTextWithin(node) {
524
535
  if (node.nodeType === Node.TEXT_NODE) {
525
536
  return (node.textContent ?? "").length > 0 ? node : null;
@@ -566,8 +577,13 @@ var RichTextEditor = React2.forwardRef(
566
577
  defaultMacrosOpen = false,
567
578
  macroLabels,
568
579
  onCreateMacro,
569
- toolbar = "visible",
580
+ toolbar,
581
+ defaultToolbar = "visible",
582
+ onToolbarChange,
583
+ blockMenu = false,
584
+ blockMenuLabels,
570
585
  markdownShortcuts = false,
586
+ chromeless = false,
571
587
  "aria-label": ariaLabel
572
588
  }, ref) => {
573
589
  const editorRef = React2.useRef(null);
@@ -582,8 +598,46 @@ var RichTextEditor = React2.forwardRef(
582
598
  const hasVariables = Boolean(variableGroups && variableGroups.length > 0);
583
599
  const hasMacros = Boolean(macros);
584
600
  const isEmpty = isHtmlEmpty(value);
585
- const toolbarVisible = toolbar !== "hidden";
601
+ const [uncontrolledToolbar, setUncontrolledToolbar] = React2.useState(defaultToolbar);
602
+ const toolbarVisible = (toolbar ?? uncontrolledToolbar) !== "hidden";
603
+ const setToolbarVisibility = (next) => {
604
+ if (toolbar === void 0) setUncontrolledToolbar(next);
605
+ onToolbarChange?.(next);
606
+ };
586
607
  const toolbarVisibleRef = React2.useRef(toolbarVisible);
608
+ const [blockMenuOpen, setBlockMenuOpen] = React2.useState(false);
609
+ const [gutterTop, setGutterTop] = React2.useState(chromeless ? 7 : 16);
610
+ const gutterWrapperRef = React2.useRef(null);
611
+ const measureRestGutterTop = React2.useCallback(() => {
612
+ const fallback = chromeless ? 7 : 16;
613
+ if (!editorRef.current) return fallback;
614
+ const padding = parseFloat(
615
+ getComputedStyle(editorRef.current).paddingTop
616
+ );
617
+ return Number.isNaN(padding) ? fallback : Math.round(padding);
618
+ }, [chromeless]);
619
+ React2.useLayoutEffect(() => {
620
+ if (!blockMenu || !editorRef.current) return;
621
+ setGutterTop((current) => {
622
+ const next = measureRestGutterTop();
623
+ return current === next ? current : next;
624
+ });
625
+ }, [blockMenu, measureRestGutterTop]);
626
+ const hoverBlockNodeRef = React2.useRef(null);
627
+ const resetGutter = React2.useCallback(() => {
628
+ hoverBlockNodeRef.current = null;
629
+ const next = measureRestGutterTop();
630
+ setGutterTop((current) => current === next ? current : next);
631
+ }, [measureRestGutterTop]);
632
+ const reconcileGutterWithContent = React2.useCallback(() => {
633
+ if (!blockMenu || !editorRef.current) return;
634
+ const hovered = hoverBlockNodeRef.current;
635
+ if (hovered && !editorRef.current.contains(hovered) || !editorRef.current.hasChildNodes()) {
636
+ resetGutter();
637
+ }
638
+ }, [blockMenu, resetGutter]);
639
+ const blockTargetRangeRef = React2.useRef(null);
640
+ const firstBlockItemRef = React2.useRef(null);
587
641
  React2.useImperativeHandle(ref, () => editorRef.current);
588
642
  React2.useEffect(() => {
589
643
  setSpeechSupported(Boolean(getSpeechRecognition()));
@@ -610,10 +664,12 @@ var RichTextEditor = React2.forwardRef(
610
664
  React2.useEffect(() => {
611
665
  if (editorRef.current && editorRef.current.innerHTML !== value) {
612
666
  editorRef.current.innerHTML = value;
667
+ reconcileGutterWithContent();
613
668
  }
614
- }, [value]);
669
+ }, [value, reconcileGutterWithContent]);
615
670
  const updateContent = () => {
616
671
  if (editorRef.current) onChange(editorRef.current.innerHTML);
672
+ reconcileGutterWithContent();
617
673
  };
618
674
  const insertSanitized = (html) => {
619
675
  if (!sanitizeHtml) return false;
@@ -829,13 +885,108 @@ var RichTextEditor = React2.forwardRef(
829
885
  return true;
830
886
  };
831
887
  const handleEditorKeyDown = (event) => {
832
- if (disabled || event.key !== " " || // Space during IME composition selects/converts a candidate; never
888
+ if (disabled) return;
889
+ if (blockMenu && event.key === "/" && (event.ctrlKey || event.metaKey) && !event.altKey) {
890
+ event.preventDefault();
891
+ openBlockMenuAtCaret();
892
+ return;
893
+ }
894
+ if (event.key !== " " || // Space during IME composition selects/converts a candidate; never
833
895
  // hijack it for shortcut expansion.
834
896
  event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey)
835
897
  return;
836
898
  if (markdownShortcuts && applyMarkdownShortcut(event)) return;
837
899
  expandTypedShortcut(event);
838
900
  };
901
+ const handleGutterMouseMove = (event) => {
902
+ if (blockMenuOpen || !editorRef.current || !gutterWrapperRef.current)
903
+ return;
904
+ let target = null;
905
+ let top = null;
906
+ for (const child of Array.from(editorRef.current.childNodes)) {
907
+ const rect = nodeRect(child);
908
+ if (!rect) continue;
909
+ if (rect.width === 0 && rect.height === 0) continue;
910
+ if (target === null || rect.top <= event.clientY) {
911
+ target = child;
912
+ top = rect.top;
913
+ }
914
+ }
915
+ if (!target || top === null) {
916
+ resetGutter();
917
+ return;
918
+ }
919
+ hoverBlockNodeRef.current = target;
920
+ const wrapperTop = gutterWrapperRef.current.getBoundingClientRect().top;
921
+ const next = Math.max(Math.round(top - wrapperTop), 4);
922
+ setGutterTop((current) => current === next ? current : next);
923
+ };
924
+ const rangeAtStartOf = (node) => {
925
+ const range = document.createRange();
926
+ range.setStart(node, 0);
927
+ range.collapse(true);
928
+ return range;
929
+ };
930
+ const handleBlockMenuOpenChange = (open) => {
931
+ if (open) {
932
+ const block = hoverBlockNodeRef.current ?? editorRef.current?.firstChild ?? null;
933
+ blockTargetRangeRef.current = block ? rangeAtStartOf(block) : null;
934
+ } else if (editorRef.current && document.activeElement?.closest('[role="menu"]')) {
935
+ const editor = editorRef.current;
936
+ editor.focus();
937
+ const target = blockTargetRangeRef.current;
938
+ const selection = window.getSelection();
939
+ if (target && selection && editor.contains(target.startContainer)) {
940
+ selection.removeAllRanges();
941
+ selection.addRange(target);
942
+ saveSelection();
943
+ }
944
+ }
945
+ setBlockMenuOpen(open);
946
+ };
947
+ const openBlockMenuAtCaret = () => {
948
+ const selection = window.getSelection();
949
+ if (selection && selection.rangeCount > 0 && editorRef.current?.contains(selection.getRangeAt(0).startContainer)) {
950
+ const caret = selection.getRangeAt(0);
951
+ blockTargetRangeRef.current = caret.cloneRange();
952
+ let node = caret.startContainer;
953
+ while (node.parentNode && node.parentNode !== editorRef.current) {
954
+ node = node.parentNode;
955
+ }
956
+ const rect = nodeRect(node);
957
+ const wrapperRect = gutterWrapperRef.current?.getBoundingClientRect();
958
+ if (rect && wrapperRect) {
959
+ setGutterTop(Math.max(Math.round(rect.top - wrapperRect.top), 4));
960
+ }
961
+ } else {
962
+ blockTargetRangeRef.current = null;
963
+ }
964
+ setBlockMenuOpen(true);
965
+ };
966
+ const applyBlockFormat = (command, value2) => {
967
+ if (disabled || !editorRef.current) return;
968
+ setBlockMenuOpen(false);
969
+ editorRef.current.focus();
970
+ const target = blockTargetRangeRef.current;
971
+ const selection = window.getSelection();
972
+ if (target && selection && editorRef.current.contains(target.startContainer)) {
973
+ selection.removeAllRanges();
974
+ selection.addRange(target);
975
+ }
976
+ try {
977
+ document.execCommand(command, false, value2);
978
+ } catch {
979
+ }
980
+ saveSelection();
981
+ updateContent();
982
+ };
983
+ React2.useEffect(() => {
984
+ if (!blockMenuOpen) return;
985
+ const frame = requestAnimationFrame(
986
+ () => firstBlockItemRef.current?.focus()
987
+ );
988
+ return () => cancelAnimationFrame(frame);
989
+ }, [blockMenuOpen]);
839
990
  const toggleDictation = async () => {
840
991
  const SpeechRecognition = getSpeechRecognition();
841
992
  if (!SpeechRecognition) return;
@@ -934,12 +1085,51 @@ var RichTextEditor = React2.forwardRef(
934
1085
  }
935
1086
  };
936
1087
  const iconButtonClass = "h-8 w-8 p-0";
1088
+ const blockFormatItems = [
1089
+ { key: "text", label: "Text", command: "formatBlock", value: "p" },
1090
+ {
1091
+ key: "heading1",
1092
+ label: "Heading 1",
1093
+ command: "formatBlock",
1094
+ value: "h1"
1095
+ },
1096
+ {
1097
+ key: "heading2",
1098
+ label: "Heading 2",
1099
+ command: "formatBlock",
1100
+ value: "h2"
1101
+ },
1102
+ {
1103
+ key: "heading3",
1104
+ label: "Heading 3",
1105
+ command: "formatBlock",
1106
+ value: "h3"
1107
+ },
1108
+ {
1109
+ key: "bulletList",
1110
+ label: "Bullet list",
1111
+ command: "insertUnorderedList"
1112
+ },
1113
+ {
1114
+ key: "numberedList",
1115
+ label: "Numbered list",
1116
+ command: "insertOrderedList"
1117
+ },
1118
+ {
1119
+ key: "quote",
1120
+ label: "Quote",
1121
+ command: "formatBlock",
1122
+ value: "blockquote"
1123
+ }
1124
+ ];
937
1125
  return /* @__PURE__ */ jsxs(
938
1126
  "div",
939
1127
  {
940
1128
  "data-slot": "rich-text-editor",
1129
+ "data-chromeless": chromeless || void 0,
941
1130
  className: cn(
942
- "border-border bg-background flex flex-col overflow-hidden rounded-lg border",
1131
+ "bg-background flex flex-col",
1132
+ !chromeless && "border-border overflow-hidden rounded-lg border",
943
1133
  className
944
1134
  ),
945
1135
  children: [
@@ -1171,16 +1361,103 @@ var RichTextEditor = React2.forwardRef(
1171
1361
  /* @__PURE__ */ jsxs(
1172
1362
  "div",
1173
1363
  {
1364
+ ref: gutterWrapperRef,
1174
1365
  className: cn(
1175
1366
  "relative flex-1",
1367
+ blockMenu && "group/rte-body",
1176
1368
  hasMacros && macrosOpen && "md:me-72"
1177
1369
  ),
1370
+ onMouseMove: blockMenu && !disabled ? handleGutterMouseMove : void 0,
1178
1371
  children: [
1372
+ blockMenu && !disabled && /* @__PURE__ */ jsx(
1373
+ "div",
1374
+ {
1375
+ "data-slot": "rich-text-editor-block-gutter",
1376
+ className: "absolute start-1 z-10",
1377
+ style: { top: gutterTop },
1378
+ children: /* @__PURE__ */ jsxs(
1379
+ Dropdown,
1380
+ {
1381
+ open: blockMenuOpen,
1382
+ onOpenChange: handleBlockMenuOpenChange,
1383
+ placement: "bottom-start",
1384
+ trigger: /* @__PURE__ */ jsx(
1385
+ Button,
1386
+ {
1387
+ type: "button",
1388
+ variant: "ghost",
1389
+ size: "sm",
1390
+ className: cn(
1391
+ "h-6 w-6 p-0",
1392
+ // Revealed on hover/focus; always visible on touch,
1393
+ // which has no hover to reveal it with.
1394
+ "opacity-0 transition-opacity",
1395
+ "group-hover/rte-body:opacity-100",
1396
+ "focus-visible:opacity-100",
1397
+ "[@media(hover:none)]:opacity-100",
1398
+ blockMenuOpen && "opacity-100"
1399
+ ),
1400
+ "aria-label": blockMenuLabels?.blockMenu ?? "Block menu",
1401
+ title: blockMenuLabels?.blockMenu ?? "Block menu",
1402
+ children: /* @__PURE__ */ jsx(Plus$1, { className: "h-4 w-4" })
1403
+ }
1404
+ ),
1405
+ children: [
1406
+ blockFormatItems.map((item, index) => /* @__PURE__ */ jsx(
1407
+ DropdownItem,
1408
+ {
1409
+ ref: index === 0 ? firstBlockItemRef : void 0,
1410
+ onClick: () => applyBlockFormat(item.command, item.value),
1411
+ children: blockMenuLabels?.[item.key] ?? item.label
1412
+ },
1413
+ item.key
1414
+ )),
1415
+ /* @__PURE__ */ jsx(DropdownSeparator, {}),
1416
+ hasMacros && /* @__PURE__ */ jsx(
1417
+ DropdownItem,
1418
+ {
1419
+ onClick: () => {
1420
+ setBlockMenuOpen(false);
1421
+ if (blockTargetRangeRef.current) {
1422
+ savedRangeRef.current = blockTargetRangeRef.current;
1423
+ }
1424
+ setMacrosOpen(true);
1425
+ focusRestoringSelection();
1426
+ },
1427
+ children: blockMenuLabels?.insertMacro ?? "Insert macro\u2026"
1428
+ }
1429
+ ),
1430
+ /* @__PURE__ */ jsx(
1431
+ DropdownItem,
1432
+ {
1433
+ onClick: () => {
1434
+ setBlockMenuOpen(false);
1435
+ setToolbarVisibility(
1436
+ toolbarVisible ? "hidden" : "visible"
1437
+ );
1438
+ editorRef.current?.focus();
1439
+ },
1440
+ children: toolbarVisible ? blockMenuLabels?.hideToolbar ?? "Hide toolbar" : blockMenuLabels?.showToolbar ?? "Show toolbar"
1441
+ }
1442
+ )
1443
+ ]
1444
+ }
1445
+ )
1446
+ }
1447
+ ),
1179
1448
  isEmpty && placeholder && /* @__PURE__ */ jsx(
1180
1449
  "div",
1181
1450
  {
1182
1451
  "aria-hidden": "true",
1183
- className: "text-muted-foreground pointer-events-none absolute start-0 top-0 p-4",
1452
+ className: cn(
1453
+ "text-muted-foreground pointer-events-none absolute start-0 top-0",
1454
+ // Chromeless hosts drop the wrapper's clipping, so a long
1455
+ // placeholder must stay a single ellipsized line (as a
1456
+ // native textarea clips its own) instead of wrapping out
1457
+ // of the one-row field.
1458
+ chromeless ? "end-0 truncate px-3 py-[7px] text-base" : "p-4",
1459
+ blockMenu && "ps-10"
1460
+ ),
1184
1461
  children: placeholder
1185
1462
  }
1186
1463
  ),
@@ -1192,17 +1469,33 @@ var RichTextEditor = React2.forwardRef(
1192
1469
  "aria-multiline": "true",
1193
1470
  "aria-label": ariaLabel ?? placeholder,
1194
1471
  contentEditable: !disabled,
1472
+ "data-macros-open": hasMacros && macrosOpen ? "" : void 0,
1195
1473
  tabIndex: disabled ? -1 : 0,
1196
1474
  onInput: updateContent,
1197
1475
  onPaste: handlePaste,
1198
1476
  onDrop: handleDrop,
1199
1477
  onBlur: hasMacros ? saveSelection : void 0,
1200
- onKeyDown: hasMacros || markdownShortcuts ? handleEditorKeyDown : void 0,
1478
+ onKeyDown: hasMacros || markdownShortcuts || blockMenu ? handleEditorKeyDown : void 0,
1201
1479
  onKeyUp: hasMacros ? saveSelection : void 0,
1202
1480
  onMouseUp: hasMacros ? saveSelection : void 0,
1203
1481
  className: cn(
1204
- "min-h-[250px] overflow-y-auto p-4 focus:outline-none",
1482
+ "overflow-y-auto focus:outline-none",
1483
+ // Chromeless surfaces start one Input row tall (Textarea md
1484
+ // metrics: 24px line + 7px padding + the host's 1px borders
1485
+ // = h-10) and auto-grow with content.
1486
+ chromeless ? "min-h-[38px] px-3 py-[7px] text-base" : "min-h-[250px] p-4",
1205
1487
  "prose prose-sm max-w-none",
1488
+ // Baseline block styles: Tailwind preflight strips heading,
1489
+ // list and quote styling, and the `prose` classes above only
1490
+ // work when the host installs @tailwindcss/typography —
1491
+ // without these, block formats would apply invisibly.
1492
+ "[&_h1]:my-3 [&_h1]:text-2xl [&_h1]:font-bold",
1493
+ "[&_h2]:my-3 [&_h2]:text-xl [&_h2]:font-bold",
1494
+ "[&_h3]:my-2 [&_h3]:text-lg [&_h3]:font-semibold",
1495
+ "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:ps-6",
1496
+ "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:ps-6",
1497
+ "[&_blockquote]:border-border [&_blockquote]:text-muted-foreground [&_blockquote]:my-2 [&_blockquote]:border-s-4 [&_blockquote]:ps-3",
1498
+ blockMenu && "ps-10",
1206
1499
  hasMacros && macrosOpen && "md:min-h-[400px]",
1207
1500
  disabled && "bg-muted/20 cursor-not-allowed text-neutral-700 dark:text-neutral-300"
1208
1501
  ),
@@ -1237,5 +1530,5 @@ var RichTextEditor = React2.forwardRef(
1237
1530
  RichTextEditor.displayName = "RichTextEditor";
1238
1531
 
1239
1532
  export { MacroLibrary, RichTextEditor, convertAngleBracketsToMustache, processDictation };
1240
- //# sourceMappingURL=chunk-IYY7NKA3.js.map
1241
- //# sourceMappingURL=chunk-IYY7NKA3.js.map
1533
+ //# sourceMappingURL=chunk-OEJ4TCXY.js.map
1534
+ //# sourceMappingURL=chunk-OEJ4TCXY.js.map