@lobehub/editor 4.0.1 → 4.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.
@@ -27,6 +27,7 @@ import { IMarkdownShortCutService } from "../../markdown/service/shortcut";
27
27
  import { createDebugLogger } from "../../../utils/debug";
28
28
  import { registerMathCommand } from "../command";
29
29
  import { $createMathBlockNode, $createMathInlineNode, MathBlockNode, MathInlineNode } from "../node";
30
+ import { isLikelyMathContent } from "../utils";
30
31
 
31
32
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
32
33
 
@@ -103,10 +104,11 @@ export var MathPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
103
104
  var _this2 = this;
104
105
  var markdownService = this.kernel.requireService(IMarkdownShortCutService);
105
106
  markdownService === null || markdownService === void 0 || markdownService.registerMarkdownShortCut({
106
- regExp: /\$([^$]+)\$\s?$/,
107
+ regExp: /\$([^\s$](?:[^$]*[^\s$])?)\$\s?$/,
107
108
  replace: function replace(textNode, match) {
108
109
  var _match = _slicedToArray(match, 2),
109
110
  code = _match[1];
111
+ if (!isLikelyMathContent(code)) return;
110
112
  var mathNode = $createMathInlineNode(code);
111
113
  _this2.logger.debug('Math node inserted:', mathNode);
112
114
  // textNode.replace(mathNode);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Heuristic: does the content between `$...$` look like a math formula?
3
+ *
4
+ * Positive signals (any one → math):
5
+ * - LaTeX commands \alpha \frac …
6
+ * - Structural chars ^ _ { } ( )
7
+ * - Operators / relations + - * / = < > | ! ,
8
+ * - Digits 0-9
9
+ * - Single ASCII letter x y n (common math variable)
10
+ *
11
+ * Everything else (multi-char words, CJK, spaced plain text) → not math.
12
+ */
13
+ export declare function isLikelyMathContent(content: string): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Heuristic: does the content between `$...$` look like a math formula?
3
+ *
4
+ * Positive signals (any one → math):
5
+ * - LaTeX commands \alpha \frac …
6
+ * - Structural chars ^ _ { } ( )
7
+ * - Operators / relations + - * / = < > | ! ,
8
+ * - Digits 0-9
9
+ * - Single ASCII letter x y n (common math variable)
10
+ *
11
+ * Everything else (multi-char words, CJK, spaced plain text) → not math.
12
+ */
13
+ export function isLikelyMathContent(content) {
14
+ var trimmed = content.trim();
15
+ if (!trimmed) return false;
16
+
17
+ // LaTeX commands
18
+ if (/\\[A-Za-z]/.test(trimmed)) return true;
19
+
20
+ // Math structural chars, operators, or digits
21
+ if (/[\d!()*+,/<=>^_{|}-]/.test(trimmed)) return true;
22
+
23
+ // Single ASCII letter → likely a variable (x, y, n, …)
24
+ if (/^[A-Za-z]$/.test(trimmed)) return true;
25
+ return false;
26
+ }
@@ -8,7 +8,7 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
8
8
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
9
9
  import { mergeRegister } from '@lexical/utils';
10
10
  import { COMMAND_PRIORITY_CRITICAL, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND } from 'lexical';
11
- import { Children, useCallback, useLayoutEffect, useRef, useState } from 'react';
11
+ import { Children, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
12
12
  import { noop } from "../../../editor-kernel";
13
13
  import { useLexicalEditor } from "../../../editor-kernel/react";
14
14
  import { useLexicalComposerContext } from "../../../editor-kernel/react/react-context";
@@ -20,7 +20,9 @@ import { setCancelablePromise } from "./utils";
20
20
  import { jsx as _jsx } from "react/jsx-runtime";
21
21
  var ReactSlashPlugin = function ReactSlashPlugin(_ref) {
22
22
  var children = _ref.children,
23
- anchorClassName = _ref.anchorClassName;
23
+ anchorClassName = _ref.anchorClassName,
24
+ getPopupContainer = _ref.getPopupContainer,
25
+ placement = _ref.placement;
24
26
  var _useLexicalComposerCo = useLexicalComposerContext(),
25
27
  _useLexicalComposerCo2 = _slicedToArray(_useLexicalComposerCo, 1),
26
28
  editor = _useLexicalComposerCo2[0];
@@ -61,6 +63,11 @@ var ReactSlashPlugin = function ReactSlashPlugin(_ref) {
61
63
  setResolution(null);
62
64
  setActiveKey(null);
63
65
  }, []);
66
+
67
+ // Close menu on unmount to prevent orphaned portal at (0,0)
68
+ useEffect(function () {
69
+ return close;
70
+ }, [close]);
64
71
  var handleActiveKeyChange = useCallback(function (key) {
65
72
  setActiveKey(key);
66
73
  }, []);
@@ -113,6 +120,8 @@ var ReactSlashPlugin = function ReactSlashPlugin(_ref) {
113
120
  }
114
121
  var rect = ctx.getRect();
115
122
  setDropdownPosition({
123
+ getRect: ctx.getRect,
124
+ rect: rect,
116
125
  x: rect.left,
117
126
  y: rect.bottom
118
127
  });
@@ -234,12 +243,14 @@ var ReactSlashPlugin = function ReactSlashPlugin(_ref) {
234
243
  activeKey: activeKey,
235
244
  anchorClassName: anchorClassName,
236
245
  customRender: CustomRender,
246
+ getPopupContainer: getPopupContainer,
237
247
  loading: loading,
238
248
  onActiveKeyChange: handleActiveKeyChange,
239
249
  onClose: close,
240
250
  onSelect: handleMenuSelect,
241
251
  open: isOpen,
242
252
  options: options,
253
+ placement: placement,
243
254
  position: dropdownPosition
244
255
  });
245
256
  };
@@ -1,26 +1,107 @@
1
1
  'use client';
2
2
 
3
- var _templateObject;
3
+ var _templateObject, _templateObject2;
4
+ function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
5
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
6
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
7
+ function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
8
+ function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
9
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
4
10
  function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
5
- import { Dropdown } from '@lobehub/ui';
11
+ import { flip, offset, shift, useFloating } from '@floating-ui/react';
12
+ import { Menu } from '@lobehub/ui';
6
13
  import { createStaticStyles } from 'antd-style';
7
- import { useCallback } from 'react';
14
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
15
+ import { createPortal } from 'react-dom';
8
16
  import { jsx as _jsx } from "react/jsx-runtime";
17
+ var LOBE_THEME_APP_ID = 'lobe-ui-theme-app';
9
18
  var styles = createStaticStyles(function (_ref) {
10
- var css = _ref.css;
19
+ var css = _ref.css,
20
+ cssVar = _ref.cssVar;
11
21
  return {
12
- menu: css(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n position: fixed;\n z-index: 9999;\n "])))
22
+ menu: css(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n z-index: 9999;\n width: max-content;\n "]))),
23
+ popup: css(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n scrollbar-width: none;\n\n overflow-y: auto;\n\n min-width: 120px;\n max-height: min(50vh, 400px);\n padding: 4px;\n border-radius: ", ";\n\n background: ", ";\n outline: none;\n box-shadow:\n 0 0 15px 0 #00000008,\n 0 2px 30px 0 #00000014,\n 0 0 0 1px ", " inset;\n\n .ant-menu {\n min-width: 200px;\n padding: 0 !important;\n border-inline-end: none !important;\n\n color: ", " !important;\n\n background: transparent !important;\n background-color: transparent !important;\n }\n\n .ant-menu-item {\n overflow: hidden;\n display: flex !important;\n align-items: center;\n\n width: 100% !important;\n min-height: 36px;\n margin: 0 !important;\n padding-block: 8px !important;\n padding-inline: 12px !important;\n border-radius: ", " !important;\n\n font-size: 14px;\n line-height: 20px;\n color: ", " !important;\n\n background: transparent !important;\n\n transition: all 150ms ", ";\n\n &:hover,\n &.ant-menu-item-active {\n background: ", " !important;\n }\n\n &:active {\n background: ", " !important;\n }\n }\n\n .ant-menu-item-divider {\n height: 1px;\n margin-block: 4px !important;\n margin-inline: 0 !important;\n background: ", " !important;\n }\n\n .ant-menu-title-content,\n .ant-menu-title-content-with-extra {\n overflow: visible;\n display: inline-flex;\n gap: 24px;\n justify-content: space-between;\n\n width: 100%;\n\n text-overflow: unset;\n }\n "])), cssVar.borderRadius, cssVar.colorBgElevated, cssVar.colorBorder, cssVar.colorText, cssVar.borderRadiusSM, cssVar.colorText, cssVar.motionEaseOut, cssVar.colorFillTertiary, cssVar.colorFillSecondary, cssVar.colorBorder)
13
24
  };
14
25
  });
15
26
  var DefaultSlashMenu = function DefaultSlashMenu(_ref2) {
16
27
  var activeKey = _ref2.activeKey,
17
- anchorClassName = _ref2.anchorClassName,
28
+ getPopupContainer = _ref2.getPopupContainer,
18
29
  loading = _ref2.loading,
19
30
  onSelect = _ref2.onSelect,
20
31
  open = _ref2.open,
21
32
  options = _ref2.options,
22
- position = _ref2.position,
23
- _onClose = _ref2.onClose;
33
+ forcePlacement = _ref2.placement,
34
+ position = _ref2.position;
35
+ var resolvedPlacement = forcePlacement ? "".concat(forcePlacement, "-start") : 'top-start';
36
+ var middleware = useMemo(function () {
37
+ return [offset(8)].concat(_toConsumableArray(!forcePlacement ? [flip()] : []), [shift({
38
+ padding: 8
39
+ })]);
40
+ }, [forcePlacement]);
41
+
42
+ // Keep getRect in a ref so the virtual reference always calls the latest version
43
+ var getRectRef = useRef(position.getRect);
44
+ getRectRef.current = position.getRect;
45
+ var _useFloating = useFloating({
46
+ middleware: middleware,
47
+ open: open,
48
+ placement: resolvedPlacement,
49
+ strategy: 'fixed'
50
+ }),
51
+ refs = _useFloating.refs,
52
+ floatingStyles = _useFloating.floatingStyles,
53
+ update = _useFloating.update;
54
+ useLayoutEffect(function () {
55
+ if (!position.rect) return;
56
+ refs.setPositionReference({
57
+ getBoundingClientRect: function getBoundingClientRect() {
58
+ var _getRectRef$current, _getRectRef$current2;
59
+ return (_getRectRef$current = (_getRectRef$current2 = getRectRef.current) === null || _getRectRef$current2 === void 0 ? void 0 : _getRectRef$current2.call(getRectRef)) !== null && _getRectRef$current !== void 0 ? _getRectRef$current : position.rect;
60
+ }
61
+ });
62
+ }, [position.rect, refs]);
63
+
64
+ // Force position recalculation after reference is set.
65
+ // useFloating computes before useLayoutEffect sets the reference,
66
+ // so the first frame has wrong position. rAF ensures a correct update.
67
+ useEffect(function () {
68
+ if (!open || !position.rect) return;
69
+ var frame = requestAnimationFrame(function () {
70
+ return update();
71
+ });
72
+ return function () {
73
+ return cancelAnimationFrame(frame);
74
+ };
75
+ }, [open, position.rect, update]);
76
+
77
+ // Listen to scroll events to update floating position.
78
+ // capture phase on window catches scroll from any ancestor (scroll events don't bubble,
79
+ // but do propagate during capture). Also listen on getPopupContainer for edge cases
80
+ // like shadow DOM where capture on window may not reach.
81
+ useEffect(function () {
82
+ if (!open) return;
83
+ var onScroll = function onScroll() {
84
+ return update();
85
+ };
86
+ var container = getPopupContainer === null || getPopupContainer === void 0 ? void 0 : getPopupContainer();
87
+ window.addEventListener('scroll', onScroll, {
88
+ capture: true,
89
+ passive: true
90
+ });
91
+ if (container) {
92
+ container.addEventListener('scroll', onScroll, {
93
+ passive: true
94
+ });
95
+ }
96
+ return function () {
97
+ window.removeEventListener('scroll', onScroll, {
98
+ capture: true
99
+ });
100
+ if (container) {
101
+ container.removeEventListener('scroll', onScroll);
102
+ }
103
+ };
104
+ }, [open, getPopupContainer, update]);
24
105
  var handleMenuClick = useCallback(function (_ref3) {
25
106
  var key = _ref3.key;
26
107
  var option = options.find(function (item) {
@@ -28,29 +109,33 @@ var DefaultSlashMenu = function DefaultSlashMenu(_ref2) {
28
109
  });
29
110
  if (option) onSelect(option);
30
111
  }, [options, onSelect]);
31
- return /*#__PURE__*/_jsx("div", {
112
+ var hasVisibleItems = options === null || options === void 0 ? void 0 : options.some(function (item) {
113
+ return !('type' in item && item.type === 'divider');
114
+ });
115
+ if (!open || !hasVisibleItems) return null;
116
+ var portalContainer = (getPopupContainer === null || getPopupContainer === void 0 ? void 0 : getPopupContainer()) || document.getElementById(LOBE_THEME_APP_ID) || document.body;
117
+ var node = /*#__PURE__*/_jsx("div", {
32
118
  className: styles.menu,
33
- style: {
34
- left: position.x,
35
- top: position.y
36
- },
37
- children: /*#__PURE__*/_jsx(Dropdown, {
38
- menu: {
39
- // @ts-ignore
119
+ "data-resloved-placement": resolvedPlacement,
120
+ ref: refs.setFloating,
121
+ style: floatingStyles,
122
+ children: /*#__PURE__*/_jsx("div", {
123
+ className: styles.popup,
124
+ children: /*#__PURE__*/_jsx(Menu
125
+ // @ts-ignore - activeKey is a valid antd Menu prop passed via ...rest
126
+ , {
40
127
  activeKey: activeKey,
41
128
  items: loading ? [{
42
129
  disabled: true,
43
130
  key: 'loading',
44
131
  label: 'Loading...'
45
132
  }] : options,
46
- onClick: handleMenuClick
47
- },
48
- open: open,
49
- children: /*#__PURE__*/_jsx("span", {
50
- className: anchorClassName
133
+ onClick: handleMenuClick,
134
+ selectable: false
51
135
  })
52
136
  })
53
137
  });
138
+ return /*#__PURE__*/createPortal(node, portalContainer);
54
139
  };
55
140
  DefaultSlashMenu.displayName = 'DefaultSlashMenu';
56
141
  export default DefaultSlashMenu;
@@ -12,11 +12,13 @@ var SlashMenu = function SlashMenu(_ref) {
12
12
  var activeKey = _ref.activeKey,
13
13
  anchorClassName = _ref.anchorClassName,
14
14
  CustomRender = _ref.customRender,
15
+ getPopupContainer = _ref.getPopupContainer,
15
16
  loading = _ref.loading,
16
17
  onActiveKeyChange = _ref.onActiveKeyChange,
17
18
  onSelect = _ref.onSelect,
18
19
  open = _ref.open,
19
20
  options = _ref.options,
21
+ placement = _ref.placement,
20
22
  position = _ref.position,
21
23
  onClose = _ref.onClose;
22
24
  // Adapter for custom render component onSelect
@@ -40,11 +42,13 @@ var SlashMenu = function SlashMenu(_ref) {
40
42
  return /*#__PURE__*/_jsx(DefaultSlashMenu, {
41
43
  activeKey: activeKey,
42
44
  anchorClassName: anchorClassName,
45
+ getPopupContainer: getPopupContainer,
43
46
  loading: loading,
44
47
  onClose: onClose,
45
48
  onSelect: onSelect,
46
49
  open: open,
47
50
  options: options,
51
+ placement: placement,
48
52
  position: position
49
53
  });
50
54
  };
@@ -77,6 +77,8 @@ export interface SlashMenuProps {
77
77
  anchorClassName?: string;
78
78
  /** Custom render component if provided */
79
79
  customRender?: FC<MenuRenderProps>;
80
+ /** Custom popup container for portal rendering and scroll tracking */
81
+ getPopupContainer?: () => HTMLElement | null;
80
82
  /** Loading state */
81
83
  loading: boolean;
82
84
  /** Callback to set active key */
@@ -89,8 +91,12 @@ export interface SlashMenuProps {
89
91
  open: boolean;
90
92
  /** Available options to display */
91
93
  options: Array<ISlashOption>;
94
+ /** Force menu placement direction, skipping auto-flip detection */
95
+ placement?: 'bottom' | 'top';
92
96
  /** Menu position */
93
97
  position: {
98
+ getRect?: () => DOMRect;
99
+ rect?: DOMRect;
94
100
  x: number;
95
101
  y: number;
96
102
  };
@@ -98,4 +104,8 @@ export interface SlashMenuProps {
98
104
  export interface ReactSlashPluginProps {
99
105
  anchorClassName?: string;
100
106
  children?: (ReactElement<ReactSlashOptionProps> | undefined) | (ReactElement<ReactSlashOptionProps> | undefined)[];
107
+ /** Custom popup container for portal rendering and scroll tracking */
108
+ getPopupContainer?: () => HTMLElement | null;
109
+ /** Force menu placement direction, skipping auto-flip detection */
110
+ placement?: 'bottom' | 'top';
101
111
  }
@@ -42,6 +42,8 @@ var Editor = /*#__PURE__*/memo(function (_ref) {
42
42
  plugins = _ref$plugins === void 0 ? [] : _ref$plugins,
43
43
  _ref$slashOption = _ref.slashOption,
44
44
  slashOption = _ref$slashOption === void 0 ? {} : _ref$slashOption,
45
+ slashPlacement = _ref.slashPlacement,
46
+ getPopupContainer = _ref.getPopupContainer,
45
47
  _ref$mentionOption = _ref.mentionOption,
46
48
  mentionOption = _ref$mentionOption === void 0 ? {} : _ref$mentionOption,
47
49
  variant = _ref.variant,
@@ -100,6 +102,8 @@ var Editor = /*#__PURE__*/memo(function (_ref) {
100
102
  var memoSlash = useMemo(function () {
101
103
  if (!enableSlash && !enableMention) return null;
102
104
  return /*#__PURE__*/_jsxs(ReactSlashPlugin, {
105
+ getPopupContainer: getPopupContainer,
106
+ placement: slashPlacement,
103
107
  children: [enableSlash ? /*#__PURE__*/_jsx(ReactSlashOption, _objectSpread({
104
108
  maxLength: 8,
105
109
  trigger: "/"
@@ -108,7 +112,7 @@ var Editor = /*#__PURE__*/memo(function (_ref) {
108
112
  trigger: "@"
109
113
  }, restMentionOption)) : undefined]
110
114
  });
111
- }, [enableSlash, enableMention, slashOption, restMentionOption]);
115
+ }, [enableSlash, enableMention, slashOption, slashPlacement, getPopupContainer, restMentionOption]);
112
116
  return /*#__PURE__*/_jsxs(ReactEditor, {
113
117
  config: config,
114
118
  editor: editor,
@@ -23,6 +23,8 @@ export interface EditorProps extends Partial<ReactEditorContentProps>, Omit<Reac
23
23
  * @default true
24
24
  */
25
25
  enablePasteMarkdown?: boolean;
26
+ /** Custom popup container for slash menu portal rendering and scroll tracking */
27
+ getPopupContainer?: () => HTMLElement | null;
26
28
  markdownOption?: boolean | {
27
29
  bold?: boolean;
28
30
  code?: boolean;
@@ -42,6 +44,8 @@ export interface EditorProps extends Partial<ReactEditorContentProps>, Omit<Reac
42
44
  onTextChange?: (editor: IEditor) => void;
43
45
  plugins?: EditorPlugin[];
44
46
  slashOption?: Partial<ReactSlashOptionProps>;
47
+ /** Force slash menu placement direction, skipping auto-flip detection */
48
+ slashPlacement?: 'bottom' | 'top';
45
49
  style?: CSSProperties;
46
50
  }
47
51
  export {};
@@ -19,13 +19,16 @@ var FloatMenu = function FloatMenu(_ref) {
19
19
  _ref$maxHeight = _ref.maxHeight,
20
20
  maxHeight = _ref$maxHeight === void 0 ? 'min(50vh, 640px)' : _ref$maxHeight,
21
21
  open = _ref.open,
22
+ _ref$placement = _ref.placement,
23
+ placement = _ref$placement === void 0 ? 'top' : _ref$placement,
22
24
  customStyles = _ref.styles,
23
25
  classNames = _ref.classNames;
24
26
  var parent = getPopupContainer();
25
27
  if (!parent) return;
26
28
  if (!open) return;
29
+ var rootClassName = placement === 'bottom' ? styles.rootBottom : styles.rootTop;
27
30
  var node = /*#__PURE__*/_jsx(Flexbox, {
28
- className: cx(styles.root, classNames === null || classNames === void 0 ? void 0 : classNames.root),
31
+ className: cx(rootClassName, classNames === null || classNames === void 0 ? void 0 : classNames.root),
29
32
  paddingInline: 8,
30
33
  style: customStyles === null || customStyles === void 0 ? void 0 : customStyles.root,
31
34
  width: '100%',
@@ -1,5 +1,6 @@
1
1
  export declare const styles: {
2
2
  container: string;
3
3
  containerWithMaxHeight: string;
4
- root: string;
4
+ rootBottom: string;
5
+ rootTop: string;
5
6
  };
@@ -1,4 +1,4 @@
1
- var _templateObject, _templateObject2, _templateObject3;
1
+ var _templateObject, _templateObject2, _templateObject3, _templateObject4;
2
2
  function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
3
3
  import { createStaticStyles } from 'antd-style';
4
4
  export var styles = createStaticStyles(function (_ref) {
@@ -7,6 +7,7 @@ export var styles = createStaticStyles(function (_ref) {
7
7
  return {
8
8
  container: css(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n position: relative;\n overflow: hidden auto;\n background: ", ";\n "])), cssVar.colorBgElevated),
9
9
  containerWithMaxHeight: css(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n /* maxHeight is set via inline style as it's dynamic */\n "]))),
10
- root: css(_templateObject3 || (_templateObject3 = _taggedTemplateLiteral(["\n position: absolute;\n inset-block-start: -8px;\n inset-inline-start: 0;\n transform: translateY(-100%);\n "])))
10
+ rootBottom: css(_templateObject3 || (_templateObject3 = _taggedTemplateLiteral(["\n position: absolute;\n z-index: 9999;\n inset-block-start: 100%;\n inset-inline-start: 0;\n\n padding-block-start: 8px;\n "]))),
11
+ rootTop: css(_templateObject4 || (_templateObject4 = _taggedTemplateLiteral(["\n position: absolute;\n inset-block-start: -8px;\n inset-inline-start: 0;\n transform: translateY(-100%);\n "])))
11
12
  };
12
13
  });
@@ -11,6 +11,8 @@ export interface FloatMenuProps {
11
11
  getPopupContainer: () => HTMLDivElement | null;
12
12
  maxHeight?: string | number;
13
13
  open?: boolean;
14
+ /** Menu placement direction: 'top' (default) or 'bottom' */
15
+ placement?: 'bottom' | 'top';
14
16
  style?: CSSProperties;
15
17
  styles?: {
16
18
  container?: CSSProperties;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/editor",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.",
5
5
  "keywords": [
6
6
  "lobehub",