one-design-next 0.0.90 → 0.0.92

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.
@@ -1,6 +1,9 @@
1
1
  import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
2
2
  import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
3
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
4
+ import { scheduleLayoutBatch } from "../_util/layoutBatch";
5
+ import { observeResize } from "../_util/sharedResizeObserver";
6
+ import { fitToWidth, measureFit, resolveFont } from "../_util/textFit";
4
7
  import Dialog from "../dialog";
5
8
  import Dropdown from "../dropdown";
6
9
  import HoverFill from "../hover-fill";
@@ -15,6 +18,12 @@ var STATUS_ARIA_LABEL = {
15
18
  };
16
19
  var ELLIPSIS = '…';
17
20
 
21
+ /**
22
+ * 标记「JS 已把标题裁到可用宽度内」,由 useFittedTitle 在写入时同步,
23
+ * 不由 React 渲染:它表达的是某一帧的测量结果,而非「是否启用拟合」。
24
+ */
25
+ var TITLE_FITTED_ATTR = 'data-odn-chat-item-title-fitted';
26
+
18
27
  /** 可用宽度变化小于该值时跳过重算(与二分容差同量级) */
19
28
  var FIT_WIDTH_EPS = 0.5;
20
29
  /**
@@ -24,65 +33,85 @@ var FIT_WIDTH_EPS = 0.5;
24
33
  var FIT_MIN_RELIABLE_WIDTH = 24;
25
34
 
26
35
  /**
27
- * 有 trailing(客服 icon / status / 时间)时,用 DOM 实测把标题裁到可用宽度。
36
+ * 有 trailing(客服 icon / status / 时间)时,把标题裁到可用宽度。
28
37
  * 解决 CSS text-overflow 按字符边界截断后,省略号右侧留白把 trailing 顶开的问题。
29
38
  *
30
- * 性能:RO 监听行 / label / trailing(active 时时间让位不改变行宽,必须听内部);
31
- * 回调经 rAF 合并;缓存 maxWidth / trailingWidth / 裁切结果。
39
+ * 测量走 textFit 的 Canvas 预算:挂载时一次算完所有截断位置的宽度,
40
+ * 之后每次宽度变化只做二分,不再「填字 - 排版 - 量宽」地反复试探。
41
+ *
42
+ * 裁切结果直接写进 titleEl,不走 state:长列表拖宽时零 re-render。
43
+ * 标题本身由 React 渲染,title 变化时 React 会把完整标题写回 DOM,
44
+ * 随后本 effect 重跑重新裁切,顺序天然正确。
45
+ *
46
+ * 性能:RO 只监听整行;回调经 rAF 合并;缓存 maxWidth / trailingWidth / 裁切结果。
47
+ * trailing 宽度变化(时间文案 / 状态 / 让位)由调用方经 layoutKey 触发重测。
32
48
  */
33
49
  function useFittedTitle(title, enabled, labelRef, titleRef, /** 右对齐区(状态 / 时间),量宽预留 */
34
50
  trailingRef, /** 紧跟标题的类型 icon,量宽预留 */
35
- typeIconRef, /** active 时时间让位 / 字重变化,需强制重测 */
51
+ typeIconRef, /** trailing 宽度 / 字重变化,需强制重测 */
36
52
  layoutKey) {
37
- var _useState = useState(title),
38
- _useState2 = _slicedToArray(_useState, 2),
39
- fitted = _useState2[0],
40
- setFitted = _useState2[1];
41
53
  useLayoutEffect(function () {
42
- var _document$fonts, _label$closest;
54
+ var _measure, _document$fonts, _label$closest;
55
+ var titleEl = titleRef.current;
56
+ if (!titleEl) return;
57
+
58
+ // React 渲染的是完整标题;未启用拟合时保持原样,交给 CSS text-overflow
43
59
  if (!enabled) {
44
- setFitted(title);
60
+ titleEl.textContent = title;
61
+ titleEl.removeAttribute(TITLE_FITTED_ATTR);
45
62
  return;
46
63
  }
47
64
  var label = labelRef.current;
48
- var titleEl = titleRef.current;
49
- if (!label || !titleEl) return;
65
+ if (!label) return;
50
66
 
51
- // 标题变更时先回到原文,避免沿用上一会话裁成的「…」
52
- setFitted(title);
67
+ // 先回到原文,避免沿用上一会话裁成的「…」,也保证按完整标题取字体
68
+ titleEl.textContent = title;
69
+ var titleCs = getComputedStyle(titleEl);
70
+ var metrics = measureFit(title, ELLIPSIS, {
71
+ font: resolveFont(titleCs),
72
+ letterSpacing: Number.parseFloat(titleCs.letterSpacing) || 0
73
+ });
53
74
  var cancelled = false;
54
- var rafId = 0;
75
+ var scheduled = false;
55
76
  var lastMaxWidth = Number.NaN;
56
77
  var lastTrailingWidth = Number.NaN;
57
78
  var lastFitted = title;
58
- var probe = document.createElement('span');
59
- probe.setAttribute('aria-hidden', 'true');
60
- probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;white-space:nowrap;left:-9999px;top:0;';
61
- var syncProbeFont = function syncProbeFont() {
62
- var cs = getComputedStyle(titleEl);
63
- probe.style.fontFamily = cs.fontFamily;
64
- probe.style.fontSize = cs.fontSize;
65
- probe.style.fontWeight = cs.fontWeight;
66
- probe.style.fontStyle = cs.fontStyle;
67
- probe.style.letterSpacing = cs.letterSpacing;
68
- probe.style.fontFeatureSettings = cs.fontFeatureSettings;
69
- probe.style.fontVariationSettings = cs.fontVariationSettings;
79
+ var lastFitAccurate = false;
80
+
81
+ /**
82
+ * accurate 表示「当前文本确实在可用宽度内」,此时才挂 fitted 标记让
83
+ * CSS 停止截断(否则省略号右侧会留出字符边界的空白,把 trailing 顶开)。
84
+ * 放弃裁切时必须摘掉标记,把截断交回 CSS,否则标题直接溢出行外。
85
+ */
86
+ var applyFit = function applyFit(text, accurate) {
87
+ titleEl.textContent = text;
88
+ if (accurate) titleEl.setAttribute(TITLE_FITTED_ATTR, '');else titleEl.removeAttribute(TITLE_FITTED_ATTR);
70
89
  };
71
- var measure = function measure(text) {
72
- probe.textContent = text;
73
- return probe.getBoundingClientRect().width;
90
+
91
+ /**
92
+ * gap 与 trailing / typeIcon 的宽度在同一个 effect 周期内是常量:
93
+ * 所有会改变它们的条件(选中、菜单开合、时间文案、状态、类型 icon)
94
+ * 都在 layoutKey 里,变化时 effect 会整体重跑。
95
+ * 拖拽时因此只需读行内标题区的宽度,省掉每帧每项的 getComputedStyle。
96
+ */
97
+ var gap = Number.NaN;
98
+ var trailingWidth = 0;
99
+ var typeWidth = 0;
100
+ var readStatics = function readStatics() {
101
+ var _trailingRef$current$, _trailingRef$current, _typeIconRef$current$, _typeIconRef$current;
102
+ var cs = getComputedStyle(label);
103
+ gap = Number.parseFloat(cs.columnGap || cs.gap) || 0;
104
+ trailingWidth = (_trailingRef$current$ = (_trailingRef$current = trailingRef.current) === null || _trailingRef$current === void 0 ? void 0 : _trailingRef$current.getBoundingClientRect().width) !== null && _trailingRef$current$ !== void 0 ? _trailingRef$current$ : 0;
105
+ typeWidth = (_typeIconRef$current$ = (_typeIconRef$current = typeIconRef.current) === null || _typeIconRef$current === void 0 ? void 0 : _typeIconRef$current.getBoundingClientRect().width) !== null && _typeIconRef$current$ !== void 0 ? _typeIconRef$current$ : 0;
74
106
  };
75
- var fit = function fit() {
76
- var _trailing$getBounding, _typeIcon$getBounding;
107
+
108
+ /**
109
+ * 只读布局,返回写入回调交由批处理统一执行。
110
+ * 这里不能直接改 DOM:同帧内还有其他实例要读,写入会让它们的读重新排版。
111
+ */
112
+ var measure = function measure() {
77
113
  if (cancelled) return;
78
- syncProbeFont();
79
- if (!probe.isConnected) document.body.appendChild(probe);
80
- var cs = getComputedStyle(label);
81
- var gap = Number.parseFloat(cs.columnGap || cs.gap) || 0;
82
- var trailing = trailingRef.current;
83
- var typeIcon = typeIconRef.current;
84
- var trailingWidth = (_trailing$getBounding = trailing === null || trailing === void 0 ? void 0 : trailing.getBoundingClientRect().width) !== null && _trailing$getBounding !== void 0 ? _trailing$getBounding : 0;
85
- var typeWidth = (_typeIcon$getBounding = typeIcon === null || typeIcon === void 0 ? void 0 : typeIcon.getBoundingClientRect().width) !== null && _typeIcon$getBounding !== void 0 ? _typeIcon$getBounding : 0;
114
+ if (!Number.isFinite(gap)) readStatics();
86
115
  // 标题两侧可能各有一段 gap:title↔type、type/title↔trailing
87
116
  var gapCount = (typeWidth > 0 ? 1 : 0) + (trailingWidth > 0 ? 1 : 0);
88
117
  var maxWidth = Math.max(0, label.getBoundingClientRect().width - trailingWidth - typeWidth - gap * gapCount);
@@ -94,71 +123,58 @@ layoutKey) {
94
123
  if (maxWidth < FIT_MIN_RELIABLE_WIDTH) {
95
124
  lastMaxWidth = Number.NaN;
96
125
  lastTrailingWidth = Number.NaN;
97
- if (lastFitted !== title) {
98
- lastFitted = title;
99
- setFitted(title);
100
- }
101
- return;
126
+ // 此刻 trailing / typeIcon 多半也没量准,下次重读
127
+ gap = Number.NaN;
128
+ if (lastFitted === title && !lastFitAccurate) return;
129
+ lastFitted = title;
130
+ lastFitAccurate = false;
131
+ return function () {
132
+ return applyFit(title, false);
133
+ };
102
134
  }
103
135
  lastMaxWidth = maxWidth;
104
136
  lastTrailingWidth = trailingWidth;
105
- var next = title;
106
- if (measure(title) > maxWidth + FIT_WIDTH_EPS) {
107
- var lo = 0;
108
- var hi = title.length;
109
- next = title;
110
- while (lo <= hi) {
111
- var mid = lo + hi >> 1;
112
- var candidate = "".concat(title.slice(0, mid)).concat(ELLIPSIS);
113
- if (measure(candidate) <= maxWidth + FIT_WIDTH_EPS) {
114
- next = candidate;
115
- lo = mid + 1;
116
- } else {
117
- hi = mid - 1;
118
- }
119
- }
120
- // 极端窄宽仍放不下「字+…」时交给 CSS ellipsis,保留原文
121
- if (next === ELLIPSIS || next === title && measure(title) > maxWidth + FIT_WIDTH_EPS) {
122
- var withEllipsis = "".concat(title.slice(0, 1)).concat(ELLIPSIS);
123
- next = measure(withEllipsis) <= maxWidth + FIT_WIDTH_EPS ? withEllipsis : title;
124
- }
125
- }
126
- if (next === lastFitted) return;
137
+ var _fitToWidth = fitToWidth(metrics, maxWidth + FIT_WIDTH_EPS, title, ELLIPSIS),
138
+ next = _fitToWidth.text,
139
+ accurate = _fitToWidth.accurate;
140
+ if (next === lastFitted && accurate === lastFitAccurate) return;
127
141
  lastFitted = next;
128
- setFitted(next);
142
+ lastFitAccurate = accurate;
143
+ return function () {
144
+ return applyFit(next, accurate);
145
+ };
129
146
  };
130
147
  var scheduleFit = function scheduleFit() {
131
- if (cancelled || rafId) return;
132
- rafId = window.requestAnimationFrame(function () {
133
- rafId = 0;
134
- fit();
148
+ if (cancelled || scheduled) return;
149
+ scheduled = true;
150
+ scheduleLayoutBatch(function () {
151
+ scheduled = false;
152
+ return measure();
135
153
  });
136
154
  };
137
155
 
138
- // 首屏同步裁切,避免首帧闪全标题;后续宽度变化走 rAF 合并
139
- fit();
156
+ // 首屏同步裁切,避免首帧闪全标题;后续宽度变化走批处理合并
157
+ (_measure = measure()) === null || _measure === void 0 || _measure();
140
158
  void ((_document$fonts = document.fonts) === null || _document$fonts === void 0 ? void 0 : _document$fonts.ready.then(function () {
141
159
  if (cancelled) return;
142
- // 字体就绪后字形宽度可能变,需绕过 maxWidth 缓存强制重测
160
+ // 字体就绪后字形宽度可能变,需重新预算并绕过 maxWidth 缓存
161
+ var readyCs = getComputedStyle(titleEl);
162
+ metrics = measureFit(title, ELLIPSIS, {
163
+ font: resolveFont(readyCs),
164
+ letterSpacing: Number.parseFloat(readyCs.letterSpacing) || 0
165
+ });
143
166
  lastMaxWidth = Number.NaN;
167
+ gap = Number.NaN;
144
168
  scheduleFit();
145
169
  }));
146
170
  var row = (_label$closest = label.closest('[data-odn-chat-item-btn]')) !== null && _label$closest !== void 0 ? _label$closest : label;
147
- var ro = new ResizeObserver(scheduleFit);
148
- ro.observe(row);
149
- ro.observe(label);
150
- var trailing = trailingRef.current;
151
- if (trailing) ro.observe(trailing);
152
- var typeIcon = typeIconRef.current;
153
- if (typeIcon) ro.observe(typeIcon);
171
+ var stopObserving = observeResize(row, scheduleFit);
154
172
  return function () {
173
+ // 批处理队列里可能仍排着本实例的读回调,靠 cancelled 短路即可
155
174
  cancelled = true;
156
- if (rafId) window.cancelAnimationFrame(rafId);
157
- ro.disconnect();
158
- probe.remove();
175
+ stopObserving();
159
176
  };
160
177
  }, [title, enabled, layoutKey, labelRef, titleRef, trailingRef, typeIconRef]);
161
- return enabled ? fitted : title;
162
178
  }
163
179
  function ChatItemStatus(_ref) {
164
180
  var status = _ref.status;
@@ -204,18 +220,28 @@ function ChatItemStatus(_ref) {
204
220
  }));
205
221
  }
206
222
  function useControllableState(controlled, defaultValue, onChange) {
207
- var _useState3 = useState(defaultValue),
208
- _useState4 = _slicedToArray(_useState3, 2),
209
- internal = _useState4[0],
210
- setInternal = _useState4[1];
223
+ var _useState = useState(defaultValue),
224
+ _useState2 = _slicedToArray(_useState, 2),
225
+ internal = _useState2[0],
226
+ setInternal = _useState2[1];
211
227
  var value = controlled !== null && controlled !== void 0 ? controlled : internal;
212
- var setValue = function setValue(next) {
213
- onChange === null || onChange === void 0 || onChange(next);
214
- if (controlled === undefined) setInternal(next);
228
+ // 每次渲染新建 setValue 会让下游 memo 组件(Dropdown / 菜单项)全部失效
229
+ var latest = useRef({
230
+ controlled: controlled,
231
+ onChange: onChange
232
+ });
233
+ latest.current = {
234
+ controlled: controlled,
235
+ onChange: onChange
215
236
  };
237
+ var setValue = useCallback(function (next) {
238
+ var _latest$current$onCha, _latest$current;
239
+ (_latest$current$onCha = (_latest$current = latest.current).onChange) === null || _latest$current$onCha === void 0 || _latest$current$onCha.call(_latest$current, next);
240
+ if (latest.current.controlled === undefined) setInternal(next);
241
+ }, []);
216
242
  return [value, setValue];
217
243
  }
218
- export function ChatItem(_ref2) {
244
+ function ChatItemInner(_ref2) {
219
245
  var id = _ref2.id,
220
246
  title = _ref2.title,
221
247
  active = _ref2.active,
@@ -246,10 +272,10 @@ export function ChatItem(_ref2) {
246
272
  _useControllableState2 = _slicedToArray(_useControllableState, 2),
247
273
  renaming = _useControllableState2[0],
248
274
  setRenaming = _useControllableState2[1];
249
- var _useState5 = useState(title),
250
- _useState6 = _slicedToArray(_useState5, 2),
251
- draft = _useState6[0],
252
- setDraft = _useState6[1];
275
+ var _useState3 = useState(title),
276
+ _useState4 = _slicedToArray(_useState3, 2),
277
+ draft = _useState4[0],
278
+ setDraft = _useState4[1];
253
279
  var _useControllableState3 = useControllableState(deleteOpenProp, defaultDeleteOpen, onDeleteOpenChange),
254
280
  _useControllableState4 = _slicedToArray(_useControllableState3, 2),
255
281
  deleteOpen = _useControllableState4[0],
@@ -258,12 +284,12 @@ export function ChatItem(_ref2) {
258
284
  _useControllableState6 = _slicedToArray(_useControllableState5, 2),
259
285
  menuOpen = _useControllableState6[0],
260
286
  setMenuOpen = _useControllableState6[1];
261
- var _useState7 = useState(function () {
287
+ var _useState5 = useState(function () {
262
288
  return Date.now();
263
289
  }),
264
- _useState8 = _slicedToArray(_useState7, 2),
265
- now = _useState8[0],
266
- setNow = _useState8[1];
290
+ _useState6 = _slicedToArray(_useState5, 2),
291
+ now = _useState6[0],
292
+ setNow = _useState6[1];
267
293
  var inputRef = useRef(null);
268
294
  var labelRef = useRef(null);
269
295
  var titleRef = useRef(null);
@@ -278,8 +304,9 @@ export function ChatItem(_ref2) {
278
304
  var showTime = Boolean(timeLabel);
279
305
  var showTrailing = showStatus || showTime;
280
306
  var fitTitle = showTypeIcon || showTrailing;
281
- var displayTitle = useFittedTitle(title, fitTitle, labelRef, titleRef, trailingRef, typeIconRef, // active / 菜单开合都会改时间让位与字重,行宽可能不变
282
- "".concat(active ? 1 : 0, ":").concat(menuOpen ? 1 : 0));
307
+ useFittedTitle(title, fitTitle, labelRef, titleRef, trailingRef, typeIconRef, // trailing / 字重的全部变化来源。这些都会改标题可用宽度而行宽不变,
308
+ // 提前声明即可,无需为此额外监听 label / trailing / typeIcon 的尺寸。
309
+ "".concat(active ? 1 : 0, ":").concat(menuOpen ? 1 : 0, ":").concat(timeLabel, ":").concat(status !== null && status !== void 0 ? status : '', ":").concat(showTypeIcon ? 1 : 0));
283
310
  useEffect(function () {
284
311
  if (!renaming) setDraft(title);
285
312
  }, [title, renaming]);
@@ -316,19 +343,21 @@ export function ChatItem(_ref2) {
316
343
  setDraft(title);
317
344
  setRenaming(false);
318
345
  };
319
- var actionMenu = [].concat(_toConsumableArray(onRename ? [{
320
- value: 'rename',
321
- label: '修改名称',
322
- icon: 'pencil-line'
323
- }] : []), _toConsumableArray(onPin ? [{
324
- value: 'pin',
325
- label: pinned ? '取消置顶' : '置顶',
326
- icon: 'arrow-up-to-line'
327
- }] : []), _toConsumableArray(onDelete ? [{
328
- value: 'delete',
329
- label: '删除',
330
- icon: 'trash-2'
331
- }] : []));
346
+ var actionMenu = useMemo(function () {
347
+ return [].concat(_toConsumableArray(onRename ? [{
348
+ value: 'rename',
349
+ label: '修改名称',
350
+ icon: 'pencil-line'
351
+ }] : []), _toConsumableArray(onPin ? [{
352
+ value: 'pin',
353
+ label: pinned ? '取消置顶' : '置顶',
354
+ icon: 'arrow-up-to-line'
355
+ }] : []), _toConsumableArray(onDelete ? [{
356
+ value: 'delete',
357
+ label: '删除',
358
+ icon: 'trash-2'
359
+ }] : []));
360
+ }, [onRename, onPin, onDelete, pinned]);
332
361
  return /*#__PURE__*/React.createElement(HoverFill, {
333
362
  "data-odn-chat-item": true,
334
363
  "data-odn-chat-item-id": id,
@@ -374,9 +403,8 @@ export function ChatItem(_ref2) {
374
403
  ref: titleRef,
375
404
  "data-odn-chat-item-title": true,
376
405
  "data-odn-chat-item-title-bold": active ? '' : undefined,
377
- "data-odn-chat-item-title-fitted": fitTitle ? '' : undefined,
378
406
  title: title
379
- }, displayTitle), showTypeIcon ? /*#__PURE__*/React.createElement("span", {
407
+ }, title), showTypeIcon ? /*#__PURE__*/React.createElement("span", {
380
408
  ref: typeIconRef,
381
409
  "data-odn-chat-item-type-icon": true,
382
410
  "aria-label": "\u4EBA\u5DE5\u5BA2\u670D\u4F1A\u8BDD",
@@ -446,6 +474,14 @@ export function ChatItem(_ref2) {
446
474
  }
447
475
  })));
448
476
  }
477
+
478
+ /**
479
+ * 会话列表项。
480
+ *
481
+ * memo:列表里单项 hover / 选中会触发父级重渲染,未 memo 时其余项会一并
482
+ * 重跑 render 与相对时间计算。父级需传入稳定的回调引用才能生效。
483
+ */
484
+ export var ChatItem = /*#__PURE__*/memo(ChatItemInner);
449
485
  ChatItem.displayName = 'ChatItem';
450
486
  export default ChatItem;
451
487
  export { formatRelativeTime } from "./relative-time";
@@ -28,10 +28,10 @@ export interface ComposerToolConfig {
28
28
  }
29
29
  /**
30
30
  * Composer 向内嵌 SkillSlot 透传的配置袋。
31
- * 技能目录(skills / skillGroups)应由「技能管理」模块维护后注入此处;
32
- * Composer 只负责触发入口与选中后插入 chip,不是目录 SSOT。
31
+ * 技能目录(skills)应由「技能管理」模块维护后注入此处;
32
+ * 菜单宽高由 Composer 按输入框与视口计算,不经本袋覆盖。
33
33
  */
34
- export type ComposerSkillSlotProps = Pick<SkillSlotProps, 'skills' | 'skillGroups' | 'showGroupTitles' | 'menuMaxHeight' | 'menuWidth' | 'onManageSkills' | 'skillManageFooter'>;
34
+ export type ComposerSkillSlotProps = Pick<SkillSlotProps, 'skills' | 'onManageSkills' | 'skillManageFooter'>;
35
35
  export interface ComposerRef {
36
36
  /** 聚焦输入框 */
37
37
  focus: (options?: FocusOptions) => void;
@@ -156,6 +156,7 @@ export interface ComposerProps {
156
156
  /**
157
157
  * 向内嵌 SkillSlot 透传的配置袋。
158
158
  * 技能目录由业务 / 技能管理模块维护后注入;Composer 只负责触发入口与插入 chip。
159
+ * 菜单宽高由 Composer 按输入框与视口计算,不经本袋覆盖。
159
160
  */
160
161
  skillSlotProps?: ComposerSkillSlotProps;
161
162
  /**
@@ -20,7 +20,7 @@ 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
+ import { COMPOSER_SKILL_MENU_DENSITY_CAP_PX, filterSkillsByQuery, resolveComposerSkillMenuMaxHeight, resolveComposerSkillMenuWidth, shouldOpenSlashTriggerMenu } from "./utils";
24
24
  function createInvocationChipData(skill) {
25
25
  return {
26
26
  id: "skill-".concat(skill.id, "-").concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 6)),
@@ -92,12 +92,12 @@ var DEFAULT_TOOL_CONFIGS = {
92
92
 
93
93
  /**
94
94
  * Composer 向内嵌 SkillSlot 透传的配置袋。
95
- * 技能目录(skills / skillGroups)应由「技能管理」模块维护后注入此处;
96
- * Composer 只负责触发入口与选中后插入 chip,不是目录 SSOT。
95
+ * 技能目录(skills)应由「技能管理」模块维护后注入此处;
96
+ * 菜单宽高由 Composer 按输入框与视口计算,不经本袋覆盖。
97
97
  */
98
98
 
99
99
  export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
100
- var _filteredSkillGroups$, _filteredSkills$lengt, _paramEdit$chipId;
100
+ var _filteredSkills$lengt, _paramEdit$chipId;
101
101
  var valueProp = _ref.value,
102
102
  _ref$defaultValue = _ref.defaultValue,
103
103
  defaultValue = _ref$defaultValue === void 0 ? '' : _ref$defaultValue,
@@ -147,11 +147,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
147
147
  className = _ref.className,
148
148
  style = _ref.style;
149
149
  var skills = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.skills;
150
- var skillGroups = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.skillGroups;
151
150
  var onManageSkills = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.onManageSkills;
152
151
  var skillManageFooter = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.skillManageFooter;
153
- var resolvedMenuMaxHeight = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.menuMaxHeight;
154
- var showGroupTitles = skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.showGroupTitles;
155
152
  var _useControlledState = useControlledState(defaultValue, valueProp),
156
153
  _useControlledState2 = _slicedToArray(_useControlledState, 2),
157
154
  text = _useControlledState2[0],
@@ -216,11 +213,15 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
216
213
  _useState18 = _slicedToArray(_useState17, 2),
217
214
  skillMenuCrossAxis = _useState18[0],
218
215
  setSkillMenuCrossAxis = _useState18[1];
219
- /** composer box 实宽;技能菜单默认比它略窄。显式 menuWidth 覆盖。 */
216
+ /** composer box 实宽;技能菜单比它略窄。 */
220
217
  var _useState19 = useState(0),
221
218
  _useState20 = _slicedToArray(_useState19, 2),
222
219
  boxWidth = _useState20[0],
223
220
  setBoxWidth = _useState20[1];
221
+ var _useState21 = useState(COMPOSER_SKILL_MENU_DENSITY_CAP_PX),
222
+ _useState22 = _slicedToArray(_useState21, 2),
223
+ skillMenuMaxHeight = _useState22[0],
224
+ setSkillMenuMaxHeight = _useState22[1];
224
225
  useLayoutEffect(function () {
225
226
  var el = boxRef.current;
226
227
  if (!el) return undefined;
@@ -238,7 +239,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
238
239
  return ro.disconnect();
239
240
  };
240
241
  }, []);
241
- var resolvedMenuWidth = resolveComposerSkillMenuWidth(skillSlotProps === null || skillSlotProps === void 0 ? void 0 : skillSlotProps.menuWidth, boxWidth);
242
+ var resolvedMenuWidth = resolveComposerSkillMenuWidth(boxWidth);
242
243
  var handleComposerMouseDown = useCallback(function (event) {
243
244
  var _editorRef$current;
244
245
  if (disabled || event.button !== 0) return;
@@ -254,10 +255,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
254
255
  preventScroll: true
255
256
  });
256
257
  }, [disabled]);
257
- var _useState21 = useState(null),
258
- _useState22 = _slicedToArray(_useState21, 2),
259
- paramEdit = _useState22[0],
260
- setParamEdit = _useState22[1];
258
+ var _useState23 = useState(null),
259
+ _useState24 = _slicedToArray(_useState23, 2),
260
+ paramEdit = _useState24[0],
261
+ setParamEdit = _useState24[1];
261
262
  var openParamPanel = useCallback(function (chipId) {
262
263
  if (!renderParamPanel) return;
263
264
  requestAnimationFrame(function () {
@@ -598,10 +599,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
598
599
  * 不夺回焦点时 Enter 会再次激活按钮而不是发送 */
599
600
  (_editorRef$current15 = editorRef.current) === null || _editorRef$current15 === void 0 || _editorRef$current15.focus();
600
601
  };
601
- var _useState23 = useState(false),
602
- _useState24 = _slicedToArray(_useState23, 2),
603
- fileDragOver = _useState24[0],
604
- setFileDragOver = _useState24[1];
602
+ var _useState25 = useState(false),
603
+ _useState26 = _slicedToArray(_useState25, 2),
604
+ fileDragOver = _useState26[0],
605
+ setFileDragOver = _useState26[1];
605
606
  var composerBoxFileDragAttrs = fileDragOver ? {
606
607
  'data-odn-composer-file-drag': ''
607
608
  } : {};
@@ -661,40 +662,22 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
661
662
  return filterSkillsByQuery(skills, slashTriggerQuery);
662
663
  }, [skills, slashTriggerActive, slashTriggerQuery]);
663
664
 
664
- /* grouped 版本的 trigger query 筛选:按组分别过滤,保留非空组。 */
665
- var filteredSkillGroups = useMemo(function () {
666
- if (!skillGroups) return undefined;
667
- if (!slashTriggerActive) return [];
668
- return skillGroups.map(function (g) {
669
- return {
670
- groupName: g.groupName,
671
- skills: filterSkillsByQuery(g.skills, slashTriggerQuery)
672
- };
673
- }).filter(function (g) {
674
- return g.skills.length > 0;
675
- });
676
- }, [skillGroups, slashTriggerActive, slashTriggerQuery]);
677
-
678
665
  /* trigger 浮层:`/` 触发态 + 目录非空 + 过滤后仍有命中。
679
666
  * 无匹配时菜单关闭,Enter 不再被当成「选 skill」。`@` 本轮不接。 */
680
- var hasSkills = !!(skills && skills.length > 0) || !!(skillGroups && skillGroups.length > 0);
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;
667
+ var hasSkills = !!(skills && skills.length > 0);
668
+ var triggerHitCount = (_filteredSkills$lengt = filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills.length) !== null && _filteredSkills$lengt !== void 0 ? _filteredSkills$lengt : 0;
684
669
  var triggerMenuVisible = shouldOpenSlashTriggerMenu(triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.trigger, hasSkills ? triggerHitCount : 0);
685
670
 
686
671
  /* query 变化或浮层关闭 → 高亮项重置到「最上方的可用项」(跳过 disabled)。
687
672
  * 用 query 而不是 triggerInfo 整体当依赖:rect 变化(光标移位)不应该 reset。 */
688
673
  useEffect(function () {
689
- var _ref3, _ref4, _ref5;
690
- var list = (_ref3 = (_ref4 = filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : skills) !== null && _ref4 !== void 0 ? _ref4 : (_ref5 = filteredSkillGroups !== null && filteredSkillGroups !== void 0 ? filteredSkillGroups : skillGroups) === null || _ref5 === void 0 ? void 0 : _ref5.flatMap(function (g) {
691
- return g.skills;
692
- })) !== null && _ref3 !== void 0 ? _ref3 : [];
674
+ var _ref3;
675
+ var list = (_ref3 = filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : skills) !== null && _ref3 !== void 0 ? _ref3 : [];
693
676
  var first = list.findIndex(function (s) {
694
677
  return !s.disabled;
695
678
  });
696
679
  setTriggerActiveIndex(first === -1 ? 0 : first);
697
- }, [filteredSkills, filteredSkillGroups, skills, skillGroups, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query, triggerInfo === null]);
680
+ }, [filteredSkills, skills, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query, triggerInfo === null]);
698
681
 
699
682
  /* editor 在 trigger 激活时把 ↑↓Enter Esc Tab 丢过来。返回 true = editor 别管了。
700
683
  * - ↑↓ 改高亮 index(夹在 [0, max] 内)
@@ -718,11 +701,9 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
718
701
  return true;
719
702
  }
720
703
  if (e.key === 'Enter' && !e.shiftKey || e.key === 'Tab') {
721
- var _ref6, _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
704
+ var _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
722
705
  e.preventDefault();
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) {
724
- return g.skills;
725
- })[triggerActiveIndex];
706
+ var _skill = (_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];
726
707
  if (!_skill) return false;
727
708
  handleSelectSkill(_skill, 'trigger');
728
709
  return true;
@@ -735,7 +716,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
735
716
  return true;
736
717
  }
737
718
  return false;
738
- }, [filteredSkills, filteredSkillGroups, handleSelectSkill, triggerActiveIndex, triggerMenuVisible]);
719
+ }, [filteredSkills, handleSelectSkill, triggerActiveIndex, triggerMenuVisible]);
739
720
 
740
721
  /* ---- Derived ---- */
741
722
  var hasUploadingAttachment = attachments.some(function (a) {
@@ -757,6 +738,30 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
757
738
  var boxRect = box.getBoundingClientRect();
758
739
  el.style.top = "".concat(triggerInfo.rect.top - boxRect.top, "px");
759
740
  }, [triggerInfo]);
741
+ useLayoutEffect(function () {
742
+ if (!toolMenuOpen && !triggerMenuVisible) return undefined;
743
+ var sync = function sync() {
744
+ var _skillTriggerRef$curr, _skillTriggerRef$curr2, _ref4, _triggerInfo$rect$top, _triggerAnchorRef$cur;
745
+ var triggerTop = toolMenuOpen ? (_skillTriggerRef$curr = (_skillTriggerRef$curr2 = skillTriggerRef.current) === null || _skillTriggerRef$curr2 === void 0 ? void 0 : _skillTriggerRef$curr2.getBoundingClientRect().top) !== null && _skillTriggerRef$curr !== void 0 ? _skillTriggerRef$curr : null : (_ref4 = (_triggerInfo$rect$top = triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.rect.top) !== null && _triggerInfo$rect$top !== void 0 ? _triggerInfo$rect$top : (_triggerAnchorRef$cur = triggerAnchorRef.current) === null || _triggerAnchorRef$cur === void 0 ? void 0 : _triggerAnchorRef$cur.getBoundingClientRect().top) !== null && _ref4 !== void 0 ? _ref4 : null;
746
+ if (triggerTop === null) return;
747
+ var next = resolveComposerSkillMenuMaxHeight({
748
+ triggerTop: triggerTop,
749
+ viewportHeight: window.innerHeight
750
+ });
751
+ setSkillMenuMaxHeight(function (prev) {
752
+ return prev === next ? prev : next;
753
+ });
754
+ };
755
+ sync();
756
+ window.addEventListener('resize', sync);
757
+ var box = boxRef.current;
758
+ var ro = box && typeof ResizeObserver !== 'undefined' ? new ResizeObserver(sync) : null;
759
+ if (box) ro === null || ro === void 0 || ro.observe(box);
760
+ return function () {
761
+ window.removeEventListener('resize', sync);
762
+ ro === null || ro === void 0 || ro.disconnect();
763
+ };
764
+ }, [toolMenuOpen, triggerMenuVisible, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.rect.top]);
760
765
  useLayoutEffect(function () {
761
766
  var box = boxRef.current;
762
767
  var trigger = skillTriggerRef.current;
@@ -907,9 +912,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
907
912
  }), showLabel && /*#__PURE__*/React.createElement("span", null, label))));
908
913
  };
909
914
  var renderSkillBtn = function renderSkillBtn() {
910
- var hasAnySkills = skills && skills.length > 0 || skillGroups && skillGroups.some(function (g) {
911
- return g.skills.length > 0;
912
- });
915
+ var hasAnySkills = skills && skills.length > 0;
913
916
  if (!hasAnySkills) return null;
914
917
  var _resolvedToolConfigs$3 = resolvedToolConfigs.skill,
915
918
  icon = _resolvedToolConfigs$3.icon,
@@ -946,10 +949,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
946
949
  key: toolMenuOpen ? 'open' : 'closed',
947
950
  variant: "menu",
948
951
  searchable: true,
949
- skills: skillGroups ? undefined : skills,
950
- skillGroups: skillGroups,
951
- showGroupTitles: showGroupTitles,
952
- menuMaxHeight: resolvedMenuMaxHeight,
952
+ skills: skills,
953
+ menuMaxHeight: skillMenuMaxHeight,
953
954
  menuWidth: resolvedMenuWidth,
954
955
  onSelect: function onSelect(s) {
955
956
  return handleSelectSkill(s, 'btn');
@@ -1164,12 +1165,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1164
1165
  popup: /*#__PURE__*/React.createElement(SkillSlot, {
1165
1166
  ref: triggerSkillSlotRef,
1166
1167
  variant: "menu",
1167
- skills: skillGroups ? undefined : filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : [],
1168
- skillGroups: filteredSkillGroups,
1169
- showGroupTitles: showGroupTitles,
1168
+ skills: filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : [],
1170
1169
  activeIndex: triggerActiveIndex,
1171
1170
  onActiveIndexChange: setTriggerActiveIndex,
1172
- menuMaxHeight: resolvedMenuMaxHeight,
1171
+ menuMaxHeight: skillMenuMaxHeight,
1173
1172
  menuWidth: resolvedMenuWidth,
1174
1173
  onSelect: function onSelect(s) {
1175
1174
  return handleSelectSkill(s, 'trigger');