one-design-next 0.0.85 → 0.0.87

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.
@@ -16,10 +16,11 @@ import { Message } from "../message";
16
16
  import Popover from "../popover";
17
17
  import SkillSlot from "../skill-slot";
18
18
  import Tooltip from "../tooltip";
19
- import { ComposerEditor } from "./editor";
19
+ import { COMPOSER_EDITOR_MAX_ROWS_PX, ComposerEditor } from "./editor";
20
20
  import { chipHasEditableParams, isMentionChip } from "./inline-ref";
21
21
  import { rawValueToSegments, segmentsHasContent } from "./segments";
22
22
  import "./style";
23
+ import { filterSkillsByQuery, resolveComposerSkillMenuWidth, shouldOpenSlashTriggerMenu } from "./utils";
23
24
  function createInvocationChipData(skill) {
24
25
  return {
25
26
  id: "skill-".concat(skill.id, "-").concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 6)),
@@ -96,7 +97,7 @@ var DEFAULT_TOOL_CONFIGS = {
96
97
  */
97
98
 
98
99
  export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
99
- var _skillSlotProps$menuW, _paramEdit$chipId;
100
+ var _filteredSkillGroups$, _filteredSkills$lengt, _paramEdit$chipId;
100
101
  var valueProp = _ref.value,
101
102
  _ref$defaultValue = _ref.defaultValue,
102
103
  defaultValue = _ref$defaultValue === void 0 ? '' : _ref$defaultValue,
@@ -150,7 +151,6 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
150
151
  var onManageSkills = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.onManageSkills;
151
152
  var skillManageFooter = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.skillManageFooter;
152
153
  var resolvedMenuMaxHeight = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.menuMaxHeight;
153
- var resolvedMenuWidth = (_skillSlotProps$menuW = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.menuWidth) !== null && _skillSlotProps$menuW !== void 0 ? _skillSlotProps$menuW : 260;
154
154
  var showGroupTitles = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.showGroupTitles;
155
155
  var _useControlledState = useControlledState(defaultValue, valueProp),
156
156
  _useControlledState2 = _slicedToArray(_useControlledState, 2),
@@ -187,38 +187,58 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
187
187
  setTriggerActiveIndex = _useState8[1];
188
188
  /** trigger menu 的命令式句柄:导航语义在 SkillSlot 内,composer 只转发 ↑↓。 */
189
189
  var triggerSkillSlotRef = useRef(null);
190
- /** 同步标记:handleTriggerKeyDown 已在本轮事件中取消了 trigger,
191
- * onPressEnter 检测此标记以跳过 stale triggerMenuVisible 拦截。 */
192
- var triggerDismissedRef = useRef(false);
193
- /** 当 trigger 被 dismiss(无匹配 skill 按 Enter)时跳过 Popover 关闭动画 */
190
+ /** lite 模式下由内容高度驱动的单行 / 多行布局状态。 */
194
191
  var _useState9 = useState(false),
195
192
  _useState10 = _slicedToArray(_useState9, 2),
196
- skipTriggerTransition = _useState10[0],
197
- setSkipTriggerTransition = _useState10[1];
198
- /** lite 模式下由内容高度驱动的单行 / 多行布局状态。 */
193
+ expanded = _useState10[0],
194
+ setExpanded = _useState10[1];
195
+ /** 右上角按钮:拉到当前生效上限(10 行盒高,矮屏被整框 50vh 截住)。 */
199
196
  var _useState11 = useState(false),
200
197
  _useState12 = _slicedToArray(_useState11, 2),
201
- expanded = _useState12[0],
202
- setExpanded = _useState12[1];
203
- /** full 模式下由右上角按钮控制的 50vh 展开态。 */
204
- var _useState13 = useState(false),
205
- _useState14 = _slicedToArray(_useState13, 2),
206
- fullExpanded = _useState14[0],
207
- setFullExpanded = _useState14[1];
198
+ fullExpanded = _useState12[0],
199
+ setFullExpanded = _useState12[1];
208
200
  /** ComposerEditor 反馈的 editor 自然高度(offsetHeight,未 clamp),用于 lite 升格判断 */
209
- var _useState15 = useState(0),
201
+ var _useState13 = useState(0),
202
+ _useState14 = _slicedToArray(_useState13, 2),
203
+ editorHeight = _useState14[0],
204
+ setEditorHeight = _useState14[1];
205
+ /** 内容已超过 10 行,或整框 50vh 把编辑器挤到放不下当前内容 */
206
+ var _useState15 = useState(false),
210
207
  _useState16 = _slicedToArray(_useState15, 2),
211
- editorHeight = _useState16[0],
212
- setEditorHeight = _useState16[1];
213
- /** editor 内容是否真正达到 maxRows 触顶——配合 SCSS 控制 viewport overflow,
214
- * 阻止升格 transition / 换行未触顶时的 scrollbar 闪现 */
215
- var _useState17 = useState(false),
216
- _useState18 = _slicedToArray(_useState17, 2),
217
- atMax = _useState18[0],
218
- setAtMax = _useState18[1];
208
+ atMax = _useState16[0],
209
+ setAtMax = _useState16[1];
219
210
  var fileInputRef = useRef(null);
220
211
  var editorRef = useRef(null);
221
212
  var boxRef = useRef(null);
213
+ var skillTriggerRef = useRef(null);
214
+ var triggerAnchorRef = useRef(null);
215
+ var _useState17 = useState(0),
216
+ _useState18 = _slicedToArray(_useState17, 2),
217
+ skillMenuCrossAxis = _useState18[0],
218
+ setSkillMenuCrossAxis = _useState18[1];
219
+ /** composer box 实宽;技能菜单默认比它略窄。显式 menuWidth 覆盖。 */
220
+ var _useState19 = useState(0),
221
+ _useState20 = _slicedToArray(_useState19, 2),
222
+ boxWidth = _useState20[0],
223
+ setBoxWidth = _useState20[1];
224
+ useLayoutEffect(function () {
225
+ var el = boxRef.current;
226
+ if (!el) return undefined;
227
+ var sync = function sync() {
228
+ var next = el.getBoundingClientRect().width;
229
+ setBoxWidth(function (prev) {
230
+ return prev === next ? prev : next;
231
+ });
232
+ };
233
+ sync();
234
+ if (typeof ResizeObserver === 'undefined') return undefined;
235
+ var ro = new ResizeObserver(sync);
236
+ ro.observe(el);
237
+ return function () {
238
+ return ro.disconnect();
239
+ };
240
+ }, []);
241
+ var resolvedMenuWidth = resolveComposerSkillMenuWidth(skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.menuWidth, boxWidth);
222
242
  var handleComposerMouseDown = useCallback(function (event) {
223
243
  var _editorRef$current;
224
244
  if (disabled || event.button !== 0) return;
@@ -234,10 +254,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
234
254
  preventScroll: true
235
255
  });
236
256
  }, [disabled]);
237
- var _useState19 = useState(null),
238
- _useState20 = _slicedToArray(_useState19, 2),
239
- paramEdit = _useState20[0],
240
- setParamEdit = _useState20[1];
257
+ var _useState21 = useState(null),
258
+ _useState22 = _slicedToArray(_useState21, 2),
259
+ paramEdit = _useState22[0],
260
+ setParamEdit = _useState22[1];
241
261
  var openParamPanel = useCallback(function (chipId) {
242
262
  if (!renderParamPanel) return;
243
263
  requestAnimationFrame(function () {
@@ -377,13 +397,19 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
377
397
  }
378
398
  }, [text, attachments.length, expanded]);
379
399
 
380
- // lite 回落到单行后同步退出手动 50vh 展开态。
400
+ // lite 回落到单行后同步退出手动展开态。
381
401
  useEffect(function () {
382
402
  if (variant === 'lite' && !expanded) {
383
403
  setFullExpanded(false);
384
404
  }
385
405
  }, [variant, expanded]);
386
406
 
407
+ // 内容已顶满(满 10 行或整框 50vh 溢出)时展开态没有可切换的高度。
408
+ var atHeightCap = atMax || editorHeight >= COMPOSER_EDITOR_MAX_ROWS_PX;
409
+ useEffect(function () {
410
+ if (atHeightCap) setFullExpanded(false);
411
+ }, [atHeightCap]);
412
+
387
413
  /* ---- Send ----
388
414
  * 内部 text 是 raw value(chip 序列化为零宽 marker)。
389
415
  * 对外只有一份契约:`onSend(segments, meta?)`
@@ -572,10 +598,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
572
598
  * 不夺回焦点时 Enter 会再次激活按钮而不是发送 */
573
599
  (_editorRef$current15 = editorRef.current) === null || _editorRef$current15 === void 0 || _editorRef$current15.focus();
574
600
  };
575
- var _useState21 = useState(false),
576
- _useState22 = _slicedToArray(_useState21, 2),
577
- fileDragOver = _useState22[0],
578
- setFileDragOver = _useState22[1];
601
+ var _useState23 = useState(false),
602
+ _useState24 = _slicedToArray(_useState23, 2),
603
+ fileDragOver = _useState24[0],
604
+ setFileDragOver = _useState24[1];
579
605
  var composerBoxFileDragAttrs = fileDragOver ? {
580
606
  'data-odn-composer-file-drag': ''
581
607
  } : {};
@@ -625,39 +651,37 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
625
651
  }, [openParamPanel]);
626
652
 
627
653
  /* trigger query 实时筛选 skills(substring match,case-insensitive)。
628
- * 没有 query / 没有 trigger → 返回原列表。 */
654
+ * trigger 已结束时返回空列表,避免 Popover 关闭动画那一帧把子节点切回全量目录。
655
+ * 依赖 query / trigger 而不是整个 triggerInfo:rect 变化不应重建列表。 */
656
+ var slashTriggerActive = (triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.trigger) === '/';
657
+ var slashTriggerQuery = slashTriggerActive ? triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query : undefined;
629
658
  var filteredSkills = useMemo(function () {
630
- var _triggerInfo$query;
631
659
  if (!skills) return undefined;
632
- var q = triggerInfo === null || triggerInfo === void 0 || (_triggerInfo$query = triggerInfo.query) === null || _triggerInfo$query === void 0 ? void 0 : _triggerInfo$query.trim().toLowerCase();
633
- if (!q) return skills;
634
- return skills.filter(function (s) {
635
- return s.label.toLowerCase().includes(q);
636
- });
637
- }, [skills, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query]);
660
+ if (!slashTriggerActive) return [];
661
+ return filterSkillsByQuery(skills, slashTriggerQuery);
662
+ }, [skills, slashTriggerActive, slashTriggerQuery]);
638
663
 
639
664
  /* grouped 版本的 trigger query 筛选:按组分别过滤,保留非空组。 */
640
665
  var filteredSkillGroups = useMemo(function () {
641
- var _triggerInfo$query2;
642
666
  if (!skillGroups) return undefined;
643
- var q = triggerInfo === null || triggerInfo === void 0 || (_triggerInfo$query2 = triggerInfo.query) === null || _triggerInfo$query2 === void 0 ? void 0 : _triggerInfo$query2.trim().toLowerCase();
644
- if (!q) return skillGroups;
667
+ if (!slashTriggerActive) return [];
645
668
  return skillGroups.map(function (g) {
646
669
  return {
647
670
  groupName: g.groupName,
648
- skills: g.skills.filter(function (s) {
649
- return s.label.toLowerCase().includes(q);
650
- })
671
+ skills: filterSkillsByQuery(g.skills, slashTriggerQuery)
651
672
  };
652
673
  }).filter(function (g) {
653
674
  return g.skills.length > 0;
654
675
  });
655
- }, [skillGroups, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query]);
676
+ }, [skillGroups, slashTriggerActive, slashTriggerQuery]);
656
677
 
657
- /* trigger 浮层渲染条件:editor 处于 `/` 触发态 + 业务方传了 skills 或 skillGroups。
658
- * `@` 触发本轮不接(mention 数据源待业务接入),onTriggerChange 拿到也不弹。 */
678
+ /* trigger 浮层:`/` 触发态 + 目录非空 + 过滤后仍有命中。
679
+ * 无匹配时菜单关闭,Enter 不再被当成「选 skill」。`@` 本轮不接。 */
659
680
  var hasSkills = !!(skills && skills.length > 0) || !!(skillGroups && skillGroups.length > 0);
660
- var triggerMenuVisible = !!(triggerInfo && triggerInfo.trigger === '/' && hasSkills);
681
+ var triggerHitCount = skillGroups ? (_filteredSkillGroups$ = filteredSkillGroups === null || filteredSkillGroups === void 0 ? void 0 : filteredSkillGroups.reduce(function (n, g) {
682
+ return n + g.skills.length;
683
+ }, 0)) !== null && _filteredSkillGroups$ !== void 0 ? _filteredSkillGroups$ : 0 : (_filteredSkills$lengt = filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills.length) !== null && _filteredSkills$lengt !== void 0 ? _filteredSkills$lengt : 0;
684
+ var triggerMenuVisible = shouldOpenSlashTriggerMenu(triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.trigger, hasSkills ? triggerHitCount : 0);
661
685
 
662
686
  /* query 变化或浮层关闭 → 高亮项重置到「最上方的可用项」(跳过 disabled)。
663
687
  * 用 query 而不是 triggerInfo 整体当依赖:rect 变化(光标移位)不应该 reset。 */
@@ -694,55 +718,25 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
694
718
  return true;
695
719
  }
696
720
  if (e.key === 'Enter' && !e.shiftKey || e.key === 'Tab') {
697
- var _ref6, _triggerSkillSlotRef$3, _triggerSkillSlotRef$4, _editorRef$current19;
721
+ var _ref6, _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
722
+ e.preventDefault();
698
723
  var _skill = (_ref6 = (_triggerSkillSlotRef$3 = (_triggerSkillSlotRef$4 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$4 === void 0 ? void 0 : _triggerSkillSlotRef$4.getActiveSkill()) !== null && _triggerSkillSlotRef$3 !== void 0 ? _triggerSkillSlotRef$3 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex]) !== null && _ref6 !== void 0 ? _ref6 : filteredSkillGroups === null || filteredSkillGroups === void 0 ? void 0 : filteredSkillGroups.flatMap(function (g) {
699
724
  return g.skills;
700
725
  })[triggerActiveIndex];
701
- if (_skill) {
702
- e.preventDefault();
703
- handleSelectSkill(_skill, 'trigger');
704
- return true;
705
- }
706
- /* 过滤后列表为空(无匹配 skill):取消 trigger,让 Enter 继续走发送流程 */
707
- (_editorRef$current19 = editorRef.current) === null || _editorRef$current19 === void 0 || _editorRef$current19.cancelTrigger();
708
- setTriggerInfo(null);
709
- setSkipTriggerTransition(true);
710
- triggerDismissedRef.current = true;
711
- return false;
726
+ if (!_skill) return false;
727
+ handleSelectSkill(_skill, 'trigger');
728
+ return true;
712
729
  }
713
730
  if (e.key === 'Escape') {
714
- var _editorRef$current20;
731
+ var _editorRef$current19;
715
732
  e.preventDefault();
716
- (_editorRef$current20 = editorRef.current) === null || _editorRef$current20 === void 0 || _editorRef$current20.cancelTrigger();
733
+ (_editorRef$current19 = editorRef.current) === null || _editorRef$current19 === void 0 || _editorRef$current19.cancelTrigger();
717
734
  setTriggerInfo(null);
718
735
  return true;
719
736
  }
720
737
  return false;
721
738
  }, [filteredSkills, filteredSkillGroups, handleSelectSkill, triggerActiveIndex, triggerMenuVisible]);
722
739
 
723
- /* trigger 浮 anchor:跟随 caret 的 0×0 隐形元素,给 Popover 当 children。
724
- * Popover 需要 DOM children 当 reference,没有"虚拟 anchor" API,只能这么搞。
725
- *
726
- * 使用 position: absolute 而非 fixed——Drawer 等容器的 transform 会创建
727
- * 新的 containing block,导致 fixed 定位失准。absolute 相对于最近 positioned
728
- * 祖先([data-odn-composer] 有 position:relative),通过减去 offsetParent
729
- * 的 viewport rect 得到正确的局部坐标。 */
730
- var triggerAnchorRef = useRef(null);
731
- useLayoutEffect(function () {
732
- var el = triggerAnchorRef.current;
733
- if (!el || !triggerInfo) return;
734
- var offsetParent = el.offsetParent;
735
- if (offsetParent) {
736
- var parentRect = offsetParent.getBoundingClientRect();
737
- el.style.left = "".concat(triggerInfo.rect.left - parentRect.left, "px");
738
- el.style.top = "".concat(triggerInfo.rect.top - parentRect.top, "px");
739
- } else {
740
- // fallback:没有 offsetParent(比如 display:none),用 viewport 坐标
741
- el.style.left = "".concat(triggerInfo.rect.left, "px");
742
- el.style.top = "".concat(triggerInfo.rect.top, "px");
743
- }
744
- }, [triggerInfo]);
745
-
746
740
  /* ---- Derived ---- */
747
741
  var hasUploadingAttachment = attachments.some(function (a) {
748
742
  var _a$status2;
@@ -756,6 +750,29 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
756
750
  var effectiveLabeled = isLite ? false : toolsLabeled;
757
751
  // lite 模式下仅允许一个工具按钮(数量在两态下保持恒定)
758
752
  var effectiveTools = isLite ? tools.slice(0, 1) : tools;
753
+ useLayoutEffect(function () {
754
+ var el = triggerAnchorRef.current;
755
+ var box = boxRef.current;
756
+ if (!el || !box || !triggerInfo) return;
757
+ var boxRect = box.getBoundingClientRect();
758
+ el.style.top = "".concat(triggerInfo.rect.top - boxRect.top, "px");
759
+ }, [triggerInfo]);
760
+ useLayoutEffect(function () {
761
+ var box = boxRef.current;
762
+ var trigger = skillTriggerRef.current;
763
+ if (!box || !trigger) {
764
+ setSkillMenuCrossAxis(function (prev) {
765
+ return prev === 0 ? prev : 0;
766
+ });
767
+ return;
768
+ }
769
+ var boxRect = box.getBoundingClientRect();
770
+ var triggerRect = trigger.getBoundingClientRect();
771
+ var next = Math.round(boxRect.left + boxRect.width / 2 - (triggerRect.left + triggerRect.width / 2));
772
+ setSkillMenuCrossAxis(function (prev) {
773
+ return prev === next ? prev : next;
774
+ });
775
+ }, [boxWidth, toolMenuOpen, effectiveLabeled, isLite, expanded, fullExpanded]);
759
776
 
760
777
  /* 统一解析 toolConfigs:
761
778
  * - key 未配置 → 整份默认
@@ -907,12 +924,17 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
907
924
  visible: toolMenuOpen ? false : undefined,
908
925
  transitionDuration: toolMenuOpen ? 0 : undefined
909
926
  }, /*#__PURE__*/React.createElement("div", {
927
+ ref: skillTriggerRef,
910
928
  "data-odn-composer-skill-trigger": true
911
929
  }, /*#__PURE__*/React.createElement(Popover, {
912
930
  trigger: "click",
913
931
  arrowed: false,
914
- placement: "topLeft",
915
- offset: 4,
932
+ placement: "top",
933
+ shift: false,
934
+ offset: {
935
+ mainAxis: 4,
936
+ crossAxis: skillMenuCrossAxis
937
+ },
916
938
  visible: !disabled && toolMenuOpen,
917
939
  onVisibleChange: function onVisibleChange(v) {
918
940
  if (disabled) return;
@@ -1052,9 +1074,9 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1052
1074
  "data-odn-composer-file-input": true,
1053
1075
  onChange: handleFileChange
1054
1076
  });
1055
- var paramPanelNode = function (_editorRef$current$ge4, _editorRef$current21, _boxRef$current) {
1077
+ var paramPanelNode = function (_editorRef$current$ge4, _editorRef$current20, _boxRef$current) {
1056
1078
  if (!paramEdit || !renderParamPanel) return null;
1057
- var chips = (_editorRef$current$ge4 = (_editorRef$current21 = editorRef.current) === null || _editorRef$current21 === void 0 ? void 0 : _editorRef$current21.getChips()) !== null && _editorRef$current$ge4 !== void 0 ? _editorRef$current$ge4 : [];
1079
+ var chips = (_editorRef$current$ge4 = (_editorRef$current20 = editorRef.current) === null || _editorRef$current20 === void 0 ? void 0 : _editorRef$current20.getChips()) !== null && _editorRef$current$ge4 !== void 0 ? _editorRef$current$ge4 : [];
1058
1080
  var chip = chips.find(function (c) {
1059
1081
  return c.id === paramEdit.chipId;
1060
1082
  });
@@ -1064,8 +1086,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1064
1086
  chip: chip,
1065
1087
  anchor: paramEdit.anchor,
1066
1088
  onApply: function onApply(patch) {
1067
- var _editorRef$current22;
1068
- (_editorRef$current22 = editorRef.current) === null || _editorRef$current22 === void 0 || _editorRef$current22.updateChipData(chip.id, patch);
1089
+ var _editorRef$current21;
1090
+ (_editorRef$current21 = editorRef.current) === null || _editorRef$current21 === void 0 || _editorRef$current21.updateChipData(chip.id, patch);
1069
1091
  },
1070
1092
  onClose: closeParamPanel
1071
1093
  };
@@ -1094,10 +1116,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1094
1116
  onInlineRefClick: renderParamPanel ? handleInlineRefClick : undefined
1095
1117
  /* 本轮只接 `/`,`@` 留给业务接 mention 搜索时再加 */,
1096
1118
  triggers: ['/'],
1097
- onTriggerChange: function onTriggerChange(info) {
1098
- setTriggerInfo(info);
1099
- if (info) setSkipTriggerTransition(false);
1100
- },
1119
+ onTriggerChange: setTriggerInfo,
1101
1120
  onTriggerKeyDown: handleTriggerKeyDown,
1102
1121
  onPasteFiles: addFilesFromList,
1103
1122
  onHeightChange: function onHeightChange(height, overflow) {
@@ -1108,15 +1127,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1108
1127
  },
1109
1128
  onPressEnter: function onPressEnter(e) {
1110
1129
  if (e.nativeEvent.isComposing || e.keyCode === 229) return;
1111
- // trigger 浮层激活时,普通 Enter 留给浮层做选择;Shift+Enter 继续换行
1112
- // 但若 handleTriggerKeyDown 已在本轮事件中取消了 trigger(列表为空),则放行
1130
+ // trigger 浮层有可选中项时,普通 Enter 留给浮层做选择;Shift+Enter 继续换行
1113
1131
  if (triggerMenuVisible && !e.shiftKey) {
1114
- if (triggerDismissedRef.current) {
1115
- triggerDismissedRef.current = false;
1116
- } else {
1117
- e.preventDefault();
1118
- return false;
1119
- }
1132
+ e.preventDefault();
1133
+ return false;
1120
1134
  }
1121
1135
  var isSubmitKey = submitType === 'enter' ? !e.shiftKey : e.shiftKey;
1122
1136
  if (!isSubmitKey) return; // 不阻止 → 让 editor 走默认 <br>
@@ -1130,22 +1144,20 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1130
1144
  }
1131
1145
  });
1132
1146
 
1133
- /* trigger 浮层:anchor 是 fixed 定位的 0×0 隐形 div,跟随 caret 处的 trigger rect。
1134
- * 共用 SkillSlot 渲染,过滤后的列表为空时仍显示 menu(empty 视觉由 SkillSlot 自己处理)。 */
1147
+ /* `/` 浮层:anchor 水平铺满 composer box、竖直跟 caret,菜单与输入框水平居中。
1148
+ * 共用 SkillSlot 渲染;过滤后无命中时 triggerMenuVisible=false,不挂菜单。 */
1135
1149
  var triggerMenu = hasSkills ? /*#__PURE__*/React.createElement(Popover, {
1136
1150
  trigger: "click",
1137
1151
  arrowed: false,
1138
- placement: "topLeft",
1152
+ placement: "top",
1153
+ shift: false,
1139
1154
  offset: 4,
1140
1155
  visible: triggerMenuVisible,
1141
- transitionDuration: skipTriggerTransition ? 0 : undefined,
1142
1156
  onVisibleChange: function onVisibleChange(v) {
1143
1157
  if (!v) {
1144
- var _editorRef$current23;
1145
- (_editorRef$current23 = editorRef.current) === null || _editorRef$current23 === void 0 || _editorRef$current23.cancelTrigger();
1158
+ var _editorRef$current22;
1159
+ (_editorRef$current22 = editorRef.current) === null || _editorRef$current22 === void 0 || _editorRef$current22.cancelTrigger();
1146
1160
  setTriggerInfo(null);
1147
- } else {
1148
- setSkipTriggerTransition(false);
1149
1161
  }
1150
1162
  },
1151
1163
  popupClassName: "odn-composer-skill-popup",
@@ -1170,7 +1182,9 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1170
1182
  "data-odn-composer-trigger-anchor": true,
1171
1183
  style: {
1172
1184
  position: 'absolute',
1173
- width: 0,
1185
+ left: 0,
1186
+ right: 0,
1187
+ top: 0,
1174
1188
  height: 0,
1175
1189
  pointerEvents: 'none'
1176
1190
  },
@@ -1179,6 +1193,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1179
1193
 
1180
1194
  // lite:有附件即强制升格(否则按钮会和附件/多行内容打架)
1181
1195
  var liteExpanded = isLite && !isInlineLite;
1196
+ var showExpandBtn = !isInlineLite && !atHeightCap;
1182
1197
  var fullExpandLabel = fullExpanded ? '收起输入框' : '展开输入框';
1183
1198
  return /*#__PURE__*/React.createElement("div", {
1184
1199
  "data-odn-composer": true,
@@ -1186,6 +1201,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1186
1201
  "data-odn-composer-expanded": liteExpanded ? '' : undefined,
1187
1202
  "data-odn-composer-full-expanded": fullExpanded ? '' : undefined,
1188
1203
  "data-odn-composer-atmax": atMax ? '' : undefined,
1204
+ "data-odn-composer-height-cap": atHeightCap ? '' : undefined,
1189
1205
  "data-odn-composer-disabled": disabled || undefined,
1190
1206
  "data-odn-composer-spacing": spacing === 'drawer' ? 'drawer' : undefined,
1191
1207
  "aria-disabled": disabled || undefined,
@@ -1194,7 +1210,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1194
1210
  onMouseDown: handleComposerMouseDown
1195
1211
  }, /*#__PURE__*/React.createElement("div", {
1196
1212
  "data-odn-composer-inner": true
1197
- }, hiddenFileInput, triggerMenu, paramPanelNode, /*#__PURE__*/React.createElement("div", _extends({
1213
+ }, hiddenFileInput, paramPanelNode, /*#__PURE__*/React.createElement("div", _extends({
1198
1214
  ref: boxRef,
1199
1215
  "data-odn-composer-box": true
1200
1216
  }, composerBoxFileDragAttrs, {
@@ -1202,7 +1218,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1202
1218
  onDragOver: handleComposerDragOver,
1203
1219
  onDragLeave: handleComposerDragLeave,
1204
1220
  onDrop: handleComposerDrop
1205
- }), !isInlineLite && /*#__PURE__*/React.createElement(Tooltip, {
1221
+ }), triggerMenu, showExpandBtn && /*#__PURE__*/React.createElement(Tooltip, {
1206
1222
  popup: fullExpandLabel,
1207
1223
  placement: "top"
1208
1224
  }, /*#__PURE__*/React.createElement("button", {
@@ -31,6 +31,11 @@
31
31
  --odn-composer-expand-control-offset: 38px;
32
32
  }
33
33
 
34
+ /* 满 10 行或溢出后缩放按钮隐藏,右上角不必再给按钮留位。 */
35
+ [data-odn-composer][data-odn-composer-height-cap] {
36
+ --odn-composer-expand-control-offset: 6px;
37
+ }
38
+
34
39
  [data-odn-composer-box] {
35
40
  position: relative;
36
41
  display: flex;
@@ -168,7 +173,7 @@
168
173
  padding-bottom: 42px;
169
174
  }
170
175
 
171
- /* ─────────── scrollbar 显隐控制(lite 三重保险) ───────────
176
+ /* ─────────── scrollbar 显隐控制 ───────────
172
177
  * base-ui 的 ScrollArea autohide 默认在 root hover / scrolling 时会把
173
178
  * [data-hovering] / [data-scrolling] 标到 Scrollbar 上,opacity 切到 1,
174
179
  * 那条灰色 thumb 就显出来。这是 ScrollArea 的预设行为,对 chat composer
@@ -180,13 +185,13 @@
180
185
  * 3. thumb 背景透明(即便前两条都被压,看到的也是空 div)
181
186
  *
182
187
  * 触顶(atmax)时三条全部回滚,由 ScrollArea autohide 接管。 */
183
- [data-odn-composer][data-odn-composer-variant=lite] [data-odn-scroll-area-scrollbar] {
188
+ [data-odn-composer]:not([data-odn-composer-atmax]) [data-odn-scroll-area-scrollbar] {
184
189
  display: none !important;
185
190
  opacity: 0 !important;
186
191
  pointer-events: none !important;
187
192
  }
188
193
 
189
- [data-odn-composer][data-odn-composer-variant=lite] [data-odn-scroll-area-thumb] {
194
+ [data-odn-composer]:not([data-odn-composer-atmax]) [data-odn-scroll-area-thumb] {
190
195
  background-color: transparent !important;
191
196
  }
192
197
 
@@ -194,20 +199,20 @@
194
199
  * 不能用 `revert` —— revert 跳过整个 author origin,连 ODN ScrollArea
195
200
  * 的 `opacity: 0` / thumb `background-color: var(--odn-scrollbar-...)`
196
201
  * 都会被擦掉,结果是 thumb 元素存在但没颜色(看不到 bar)。 */
197
- [data-odn-composer][data-odn-composer-variant=lite][data-odn-composer-atmax] [data-odn-scroll-area-scrollbar] {
202
+ [data-odn-composer][data-odn-composer-atmax] [data-odn-scroll-area-scrollbar] {
198
203
  display: flex !important;
199
204
  opacity: 0 !important;
200
205
  pointer-events: none !important;
201
206
  transition: opacity 0.18s ease;
202
207
  }
203
208
 
204
- [data-odn-composer][data-odn-composer-variant=lite][data-odn-composer-atmax] [data-odn-scroll-area-scrollbar][data-hovering],
205
- [data-odn-composer][data-odn-composer-variant=lite][data-odn-composer-atmax] [data-odn-scroll-area-scrollbar][data-scrolling] {
209
+ [data-odn-composer][data-odn-composer-atmax] [data-odn-scroll-area-scrollbar][data-hovering],
210
+ [data-odn-composer][data-odn-composer-atmax] [data-odn-scroll-area-scrollbar][data-scrolling] {
206
211
  opacity: 1 !important;
207
212
  pointer-events: auto !important;
208
213
  }
209
214
 
210
- [data-odn-composer][data-odn-composer-variant=lite][data-odn-composer-atmax] [data-odn-scroll-area-thumb] {
215
+ [data-odn-composer][data-odn-composer-atmax] [data-odn-scroll-area-thumb] {
211
216
  background-color: var(--odn-scrollbar-background-color) !important;
212
217
  }
213
218
 
@@ -225,6 +230,7 @@
225
230
  flex: 1 1 auto;
226
231
  min-height: 0;
227
232
  overflow: hidden;
233
+ max-height: 242px;
228
234
  }
229
235
 
230
236
  [data-odn-composer] [data-odn-composer-editor] {
@@ -509,5 +515,6 @@ p[data-odn-composer-hint] {
509
515
 
510
516
  [data-odn-composer][data-odn-composer-full-expanded] [data-odn-composer-box] {
511
517
  box-sizing: border-box;
512
- min-height: 50vh;
518
+ /* 拉到当前上限:10 行盒高 302px(242 编辑器 + 60 chrome)与 50vh 取小。 */
519
+ min-height: min(302px, 50vh);
513
520
  }
@@ -101,7 +101,7 @@ export declare function isEditorEmpty(editor: HTMLElement, chipIdAttr?: string):
101
101
  * 之后空 text / chip 前 leading br 的清理逻辑不变(TreeWalker / querySelectorAll
102
102
  * 在 line div 内部一样工作)。
103
103
  */
104
- export declare function normalizeEditorDom(editor: HTMLElement, _chipIdAttr?: string): void;
104
+ export declare function normalizeEditorDom(editor: HTMLElement): void;
105
105
  export interface TriggerProbe {
106
106
  /** 触发字符 */
107
107
  trigger: string;
@@ -117,3 +117,15 @@ export interface TriggerProbe {
117
117
  export declare function probeTrigger(range: Range, triggers: readonly string[],
118
118
  /** caret 与触发字符之间允许的最大 query 字符数;默认 50 */
119
119
  maxQueryLen?: number): TriggerProbe | null;
120
+ /** 按 trigger query 做 label 子串匹配(忽略大小写)。query 为空则返回原列表。 */
121
+ export declare function filterSkillsByQuery<T extends {
122
+ label: string;
123
+ }>(items: T[], query?: string | null): T[];
124
+ /** `/` 技能菜单只在「有可选中的过滤结果」时打开;空匹配不应拦截 Enter。 */
125
+ export declare function shouldOpenSlashTriggerMenu(trigger: string | null | undefined, hitCount: number): boolean;
126
+ /** 技能菜单比 composer box 左右各缩 12px。 */
127
+ export declare const COMPOSER_SKILL_MENU_INSET_PX = 24;
128
+ /** box 尚未量到宽度时的兜底(与旧默认对齐)。 */
129
+ export declare const COMPOSER_SKILL_MENU_FALLBACK_WIDTH = 260;
130
+ /** 未显式传 menuWidth 时,菜单比 composer box 略窄。 */
131
+ export declare function resolveComposerSkillMenuWidth(explicit: number | undefined, boxWidth: number): number;
@@ -294,6 +294,11 @@ export function getPlainText(editor) {
294
294
  }
295
295
  return out;
296
296
  }
297
+ var kebab = function kebab(s) {
298
+ return s.replace(/[A-Z]/g, function (m) {
299
+ return "-".concat(m.toLowerCase());
300
+ });
301
+ };
297
302
 
298
303
  /**
299
304
  * editor 是否为完全空(无 chip + 文本剔除 ZWSP / 换行后为空)。
@@ -310,16 +315,6 @@ export function isEditorEmpty(editor) {
310
315
  // 仅过滤换行符(contenteditable 默认 <br> 单独存在时 textContent 是 '\n',视觉仍为空)
311
316
  return text.replace(/\n/g, '') === '';
312
317
  }
313
- var kebab = function kebab(s) {
314
- return s.replace(/[A-Z]/g, function (m) {
315
- return "-".concat(m.toLowerCase());
316
- });
317
- };
318
- var makeIsHost = function makeIsHost(chipIdAttr) {
319
- return function (el) {
320
- return el instanceof HTMLElement && el.dataset[chipIdAttr] !== undefined;
321
- };
322
- };
323
318
 
324
319
  /* ────────────────────────────────────────────────────────────────────
325
320
  * DOM 不变量 · chip 后 ZWSP 锚点 + 行首占位清理
@@ -338,7 +333,6 @@ var makeIsHost = function makeIsHost(chipIdAttr) {
338
333
  * 在 line div 内部一样工作)。
339
334
  */
340
335
  export function normalizeEditorDom(editor) {
341
- var _chipIdAttr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'mentionId';
342
336
  ensureLineRoots(editor);
343
337
  var emptyTexts = [];
344
338
  var orphanAnchorTexts = [];
@@ -491,4 +485,32 @@ export function probeTrigger(range, triggers) {
491
485
  triggerOffset: triggerOffset,
492
486
  caretOffset: caret
493
487
  };
488
+ }
489
+
490
+ /** 按 trigger query 做 label 子串匹配(忽略大小写)。query 为空则返回原列表。 */
491
+ export function filterSkillsByQuery(items, query) {
492
+ var q = query === null || query === void 0 ? void 0 : query.trim().toLowerCase();
493
+ if (!q) return items;
494
+ return items.filter(function (s) {
495
+ return s.label.toLowerCase().includes(q);
496
+ });
497
+ }
498
+
499
+ /** `/` 技能菜单只在「有可选中的过滤结果」时打开;空匹配不应拦截 Enter。 */
500
+ export function shouldOpenSlashTriggerMenu(trigger, hitCount) {
501
+ return trigger === '/' && hitCount > 0;
502
+ }
503
+
504
+ /** 技能菜单比 composer box 左右各缩 12px。 */
505
+ export var COMPOSER_SKILL_MENU_INSET_PX = 24;
506
+ /** box 尚未量到宽度时的兜底(与旧默认对齐)。 */
507
+ export var COMPOSER_SKILL_MENU_FALLBACK_WIDTH = 260;
508
+
509
+ /** 未显式传 menuWidth 时,菜单比 composer box 略窄。 */
510
+ export function resolveComposerSkillMenuWidth(explicit, boxWidth) {
511
+ if (explicit !== undefined) return explicit;
512
+ if (boxWidth > COMPOSER_SKILL_MENU_INSET_PX) {
513
+ return Math.round(boxWidth - COMPOSER_SKILL_MENU_INSET_PX);
514
+ }
515
+ return COMPOSER_SKILL_MENU_FALLBACK_WIDTH;
494
516
  }
package/dist/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export { APPROVAL_DEFAULT_ACTIONS_HINT, default as Approval, type ApprovalDecisi
58
58
  export { ARTIFACT_DEDICATED_FILE_ICON_TYPES, ARTIFACT_FILE_GLOW_COLORS, ARTIFACT_FILE_ICON_TYPES, default as Artifact, ArtifactFileIcon, getArtifactAction, getArtifactActionFromKind, getArtifactGlowColor, type ArtifactAction, type ArtifactActionKind, type ArtifactFileIconProps, type ArtifactFileIconSize, type ArtifactFileIconType, type ArtifactProps, type ArtifactSize, } from './artifact';
59
59
  export { default as Attachments, type AttachmentsProps } from './attachments';
60
60
  export { default as ChatItem, formatRelativeTime, type ChatItemProps, } from './chat-item';
61
- export { default as ChatList, matchChatListFilterTime, matchChatListFilterType, type ChatListFilterTime, type ChatListFilterType, type ChatListItem, type ChatListProps, } from './chat-list';
61
+ export { default as ChatList, matchChatListFilterStatus, matchChatListFilterTime, matchChatListFilterType, type ChatListFilterStatus, type ChatListFilterTime, type ChatListFilterType, type ChatListItem, type ChatListProps, } from './chat-list';
62
62
  export { CLARIFY_DOCK_MAX_OPTIONS, CLARIFY_DOCK_SKIPPED_LABEL, CLARIFY_DOCK_SKIPPED_VALUE, ClarifyDockPanel, ClarifyDockTrigger, clarifyDockLayoutId, parseClarifyBoundDate, toISODate, useClarifyDockState, type ClarifyDockAnswerItem, type ClarifyDockHostMessage, type ClarifyDockMessageState, type ClarifyDockPanelProps, type ClarifyDockQuestion, type ClarifyDockResolvePayload, type ClarifyDockSpec, type ClarifyDockState, type ClarifyDockTriggerProps, type ClarifyInput, type ClarifyOption, } from './clarify-dock';
63
63
  export { default as CodeBlock, type CodeBlockProps } from './code-block';
64
64
  export { default as Composer, type ComposerProps, type ComposerRef, type ComposerSkillSlotProps, type ComposerTool, type ComposerToolConfig, } from './composer';
package/dist/index.js CHANGED
@@ -64,7 +64,7 @@ export { APPROVAL_DEFAULT_ACTIONS_HINT, default as Approval } from "./approval";
64
64
  export { ARTIFACT_DEDICATED_FILE_ICON_TYPES, ARTIFACT_FILE_GLOW_COLORS, ARTIFACT_FILE_ICON_TYPES, default as Artifact, ArtifactFileIcon, getArtifactAction, getArtifactActionFromKind, getArtifactGlowColor } from "./artifact";
65
65
  export { default as Attachments } from "./attachments";
66
66
  export { default as ChatItem, formatRelativeTime } from "./chat-item";
67
- export { default as ChatList, matchChatListFilterTime, matchChatListFilterType } from "./chat-list";
67
+ export { default as ChatList, matchChatListFilterStatus, matchChatListFilterTime, matchChatListFilterType } from "./chat-list";
68
68
  export { CLARIFY_DOCK_MAX_OPTIONS, CLARIFY_DOCK_SKIPPED_LABEL, CLARIFY_DOCK_SKIPPED_VALUE, ClarifyDockPanel, ClarifyDockTrigger, clarifyDockLayoutId, parseClarifyBoundDate, toISODate, useClarifyDockState } from "./clarify-dock";
69
69
  export { default as CodeBlock } from "./code-block";
70
70
  export { default as Composer } from "./composer";