one-design-next 0.0.107 → 0.0.109

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.
@@ -29,7 +29,10 @@ export interface ChatGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ti
29
29
  * 不是用户文件夹组。
30
30
  */
31
31
  section?: boolean;
32
- /** 标题右侧扩展区(组级 … / + 等);默认仅 hover / 菜单打开时显示 */
32
+ /**
33
+ * 标题右侧扩展区(组级 … / + 等);默认仅 hover / 菜单打开时显示。
34
+ * 正在修改分组名称时不渲染。
35
+ */
33
36
  extra?: ReactNode;
34
37
  /** 强制显示 extra(如菜单打开时) */
35
38
  extraVisible?: boolean;
@@ -91,6 +91,7 @@ export function ChatGroup(_ref2) {
91
91
  setDraft = _useState4[1];
92
92
  var inputRef = useRef(null);
93
93
  var canRename = Boolean(onRename) && typeof title === 'string';
94
+ var editingTitle = renaming && canRename;
94
95
  useEffect(function () {
95
96
  if (!renaming) setDraft(titleText);
96
97
  }, [titleText, renaming]);
@@ -173,7 +174,7 @@ export function ChatGroup(_ref2) {
173
174
  }, expandHint)) : expandHint : null);
174
175
  var header = /*#__PURE__*/React.createElement("div", {
175
176
  "data-odn-chat-group-header": true
176
- }, renaming && canRename ? /*#__PURE__*/React.createElement("div", {
177
+ }, editingTitle ? /*#__PURE__*/React.createElement("div", {
177
178
  "data-odn-chat-group-rename": true
178
179
  }, folderIcon ? /*#__PURE__*/React.createElement(Icon, {
179
180
  name: "folder",
@@ -225,7 +226,7 @@ export function ChatGroup(_ref2) {
225
226
  "data-odn-chat-group-folder-icon": true
226
227
  }) : null, /*#__PURE__*/React.createElement("span", {
227
228
  "data-odn-chat-group-title": true
228
- }, title)), extra ? /*#__PURE__*/React.createElement("div", {
229
+ }, title)), extra && !editingTitle ? /*#__PURE__*/React.createElement("div", {
229
230
  "data-odn-chat-group-extra": true
230
231
  }, extra) : null);
231
232
  return /*#__PURE__*/React.createElement("div", _extends({
@@ -234,7 +235,7 @@ export function ChatGroup(_ref2) {
234
235
  "data-odn-chat-group-renaming": renaming || undefined,
235
236
  "data-odn-chat-group-folder": folderIcon || undefined,
236
237
  "data-odn-chat-group-section": section || undefined,
237
- "data-odn-chat-group-extra-visible": extraVisible || undefined,
238
+ "data-odn-chat-group-extra-visible": extraVisible && !editingTitle || undefined,
238
239
  className: clsx(className)
239
240
  }, rest), section ? header : /*#__PURE__*/React.createElement(HoverFill, {
240
241
  "data-odn-chat-group-header-fill": true,
@@ -9,6 +9,7 @@
9
9
  [data-odn-chat-group-header-fill] {
10
10
  min-width: 0;
11
11
  width: 100%;
12
+ cursor: pointer;
12
13
  }
13
14
 
14
15
  .odn-chat-group-header-bg {
@@ -355,7 +356,7 @@
355
356
  }
356
357
 
357
358
  [data-odn-chat-group-hint-kind=empty] {
358
- pointer-events: none;
359
+ cursor: default;
359
360
  user-select: none;
360
361
  }
361
362
 
@@ -20,7 +20,7 @@ export interface ChatItemProps {
20
20
  pinned?: boolean;
21
21
  /**
22
22
  * 对话时间。展示为相对时间(刚刚 / N分钟前 / …),在行右侧;
23
- * 与「…」同槽:hover 隐身但占位;选中常显「…」/ 菜单打开时才让出布局。
23
+ * 与「…」同槽:hover / 选中 / 菜单打开时时间退出布局,标题补到「…」前。
24
24
  */
25
25
  time?: number | string | Date;
26
26
  /** 当前所属分组。从「移动到」子菜单中排除,不作为可选项。 */
@@ -3,7 +3,6 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
3
3
  import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
4
4
  import { scheduleLayoutBatch } from "../_util/layoutBatch";
5
5
  import { observeResize } from "../_util/sharedResizeObserver";
6
- import { fitToWidth, measureFit, resolveFont } from "../_util/textFit";
7
6
  import Dialog from "../dialog";
8
7
  import Dropdown from "../dropdown";
9
8
  import HoverFill from "../hover-fill";
@@ -16,154 +15,30 @@ var STATUS_ARIA_LABEL = {
16
15
  unread: '未读更新',
17
16
  error: '生成出错'
18
17
  };
19
- var ELLIPSIS = '…';
20
18
 
21
19
  /**
22
- * 标记「JS 已把标题裁到可用宽度内」,由 useFittedTitle 在写入时同步,
23
- * 不由 React 渲染:它表达的是某一帧的测量结果,而非「是否启用拟合」。
20
+ * 标题保持原文,由 CSS 在贴住 trailing 的位置省略。
21
+ * 溢出时补原生 title。不把标题改写成「前缀…」。
24
22
  */
25
- var TITLE_FITTED_ATTR = 'data-odn-chat-item-title-fitted';
26
-
27
- /** 可用宽度变化小于该值时跳过重算(与二分容差同量级) */
28
- var FIT_WIDTH_EPS = 0.5;
29
- /**
30
- * 可用宽度低于该阈值时视为测量不可靠(布局未完成 / 被瞬时压扁)。
31
- * 此时保留上一帧可读结果或原文,避免把标题裁成单独的「…」。
32
- */
33
- var FIT_MIN_RELIABLE_WIDTH = 24;
34
-
35
- /**
36
- * 有 trailing(客服 icon / status / 时间)时,把标题裁到可用宽度。
37
- * 解决 CSS text-overflow 按字符边界截断后,省略号右侧留白把 trailing 顶开的问题。
38
- *
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 触发重测。
48
- */
49
- function useFittedTitle(title, enabled, labelRef, titleRef, /** 右对齐区(状态 / 时间),量宽预留 */
50
- trailingRef, /** 紧跟标题的类型 icon,量宽预留 */
51
- typeIconRef, /** trailing 宽度 / 字重变化,需强制重测 */
52
- layoutKey, /** 搜索高亮时标题由 React 子节点渲染,禁止 textContent 覆写 */
53
- skipDomWrite) {
23
+ function useChatItemTitleLayout(title, labelRef, titleRef, /** 选中 / 菜单 / 时间变化会改可用宽度,需重测溢出 */
24
+ layoutKey, /** 搜索高亮时标题含 mark,不用原生 title 盖住高亮 */
25
+ skipTooltip) {
54
26
  useLayoutEffect(function () {
55
- var _measure, _document$fonts, _label$closest;
27
+ var _measure, _label$closest;
56
28
  var titleEl = titleRef.current;
57
- if (!titleEl) return;
58
-
59
- // 高亮态保留 mark 子节点,截断交给 CSS
60
- if (skipDomWrite) {
61
- titleEl.removeAttribute(TITLE_FITTED_ATTR);
62
- titleEl.removeAttribute('title');
63
- return;
64
- }
65
-
66
- // React 渲染的是完整标题;未启用拟合时保持原样,交给 CSS text-overflow
67
- if (!enabled) {
68
- titleEl.textContent = title;
69
- titleEl.removeAttribute(TITLE_FITTED_ATTR);
70
- titleEl.removeAttribute('title');
71
- return;
72
- }
73
29
  var label = labelRef.current;
74
- if (!label) return;
75
-
76
- // 先回到原文,避免沿用上一会话裁成的「…」,也保证按完整标题取字体
77
- titleEl.textContent = title;
78
- var titleCs = getComputedStyle(titleEl);
79
- var metrics = measureFit(title, ELLIPSIS, {
80
- font: resolveFont(titleCs),
81
- letterSpacing: Number.parseFloat(titleCs.letterSpacing) || 0
82
- });
30
+ if (!titleEl || !label) return;
83
31
  var cancelled = false;
84
32
  var scheduled = false;
85
- var lastMaxWidth = Number.NaN;
86
- var lastTrailingWidth = Number.NaN;
87
- var lastFitted = title;
88
- var lastFitAccurate = false;
89
-
90
- /**
91
- * accurate 表示「当前文本确实在可用宽度内」,此时才挂 fitted 标记让
92
- * CSS 停止截断(否则省略号右侧会留出字符边界的空白,把 trailing 顶开)。
93
- * 放弃裁切时必须摘掉标记,把截断交回 CSS,否则标题直接溢出行外。
94
- */
95
- var applyFit = function applyFit(text, accurate) {
96
- titleEl.textContent = text;
97
- if (accurate) titleEl.setAttribute(TITLE_FITTED_ATTR, '');else titleEl.removeAttribute(TITLE_FITTED_ATTR);
98
- if (accurate && text !== title) titleEl.setAttribute('title', title);else titleEl.removeAttribute('title');
99
- };
100
-
101
- /**
102
- * gap 与 trailing / typeIcon 的宽度在同一个 effect 周期内是常量:
103
- * 所有会改变它们的条件(选中、菜单开合、时间文案、状态、类型 icon)
104
- * 都在 layoutKey 里,变化时 effect 会整体重跑。
105
- * 拖拽时因此只需读行内标题区的宽度,省掉每帧每项的 getComputedStyle。
106
- */
107
- var gap = Number.NaN;
108
- var trailingWidth = 0;
109
- var typeWidth = 0;
110
- var readStatics = function readStatics() {
111
- var _trailing$getBounding, _typeIconRef$current$, _typeIconRef$current;
112
- var cs = getComputedStyle(label);
113
- gap = Number.parseFloat(cs.columnGap || cs.gap) || 0;
114
- var trailing = trailingRef.current;
115
- trailingWidth = (_trailing$getBounding = trailing === null || trailing === void 0 ? void 0 : trailing.getBoundingClientRect().width) !== null && _trailing$getBounding !== void 0 ? _trailing$getBounding : 0;
116
- 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;
117
- var timeEl = trailing === null || trailing === void 0 ? void 0 : trailing.querySelector('[data-odn-chat-item-time]');
118
- if (trailing instanceof HTMLElement && timeEl instanceof HTMLElement) {
119
- var timeW = timeEl.getBoundingClientRect().width;
120
- // 选中让位时时间为 0,保留上次正值,避免 hover 位移用错
121
- if (timeW > 0) {
122
- trailing.style.setProperty('--odn-chat-item-time-w', "".concat(timeW, "px"));
123
- }
124
- }
125
- };
126
-
127
- /**
128
- * 只读布局,返回写入回调交由批处理统一执行。
129
- * 这里不能直接改 DOM:同帧内还有其他实例要读,写入会让它们的读重新排版。
130
- */
131
33
  var measure = function measure() {
132
34
  if (cancelled) return;
133
- if (!Number.isFinite(gap)) readStatics();
134
- // 标题两侧可能各有一段 gap:title↔type、type/title↔trailing
135
- var gapCount = (typeWidth > 0 ? 1 : 0) + (trailingWidth > 0 ? 1 : 0);
136
- var maxWidth = Math.max(0, label.getBoundingClientRect().width - trailingWidth - typeWidth - gap * gapCount);
137
- if (Number.isFinite(lastMaxWidth) && Math.abs(maxWidth - lastMaxWidth) < FIT_WIDTH_EPS && Math.abs(trailingWidth - lastTrailingWidth) < FIT_WIDTH_EPS) {
138
- return;
139
- }
140
-
141
- // 布局未就绪时不要写成单独的「…」,否则行宽不变时 RO 不触发会永久卡住
142
- if (maxWidth < FIT_MIN_RELIABLE_WIDTH) {
143
- lastMaxWidth = Number.NaN;
144
- lastTrailingWidth = Number.NaN;
145
- // 此刻 trailing / typeIcon 多半也没量准,下次重读
146
- gap = Number.NaN;
147
- if (lastFitted === title && !lastFitAccurate) return;
148
- lastFitted = title;
149
- lastFitAccurate = false;
150
- return function () {
151
- return applyFit(title, false);
152
- };
153
- }
154
- lastMaxWidth = maxWidth;
155
- lastTrailingWidth = trailingWidth;
156
- var _fitToWidth = fitToWidth(metrics, maxWidth + FIT_WIDTH_EPS, title, ELLIPSIS),
157
- next = _fitToWidth.text,
158
- accurate = _fitToWidth.accurate;
159
- if (next === lastFitted && accurate === lastFitAccurate) return;
160
- lastFitted = next;
161
- lastFitAccurate = accurate;
35
+ var overflow = titleEl.scrollWidth - titleEl.clientWidth > 1;
162
36
  return function () {
163
- return applyFit(next, accurate);
37
+ if (cancelled) return;
38
+ if (skipTooltip || !overflow) titleEl.removeAttribute('title');else titleEl.setAttribute('title', title);
164
39
  };
165
40
  };
166
- var scheduleFit = function scheduleFit() {
41
+ var schedule = function schedule() {
167
42
  if (cancelled || scheduled) return;
168
43
  scheduled = true;
169
44
  scheduleLayoutBatch(function () {
@@ -171,29 +46,14 @@ skipDomWrite) {
171
46
  return measure();
172
47
  });
173
48
  };
174
-
175
- // 首屏同步裁切,避免首帧闪全标题;后续宽度变化走批处理合并
176
49
  (_measure = measure()) === null || _measure === void 0 || _measure();
177
- void ((_document$fonts = document.fonts) === null || _document$fonts === void 0 ? void 0 : _document$fonts.ready.then(function () {
178
- if (cancelled) return;
179
- // 字体就绪后字形宽度可能变,需重新预算并绕过 maxWidth 缓存
180
- var readyCs = getComputedStyle(titleEl);
181
- metrics = measureFit(title, ELLIPSIS, {
182
- font: resolveFont(readyCs),
183
- letterSpacing: Number.parseFloat(readyCs.letterSpacing) || 0
184
- });
185
- lastMaxWidth = Number.NaN;
186
- gap = Number.NaN;
187
- scheduleFit();
188
- }));
189
50
  var row = (_label$closest = label.closest('[data-odn-chat-item-btn]')) !== null && _label$closest !== void 0 ? _label$closest : label;
190
- var stopObserving = observeResize(row, scheduleFit);
51
+ var stopObserving = observeResize(row, schedule);
191
52
  return function () {
192
- // 批处理队列里可能仍排着本实例的读回调,靠 cancelled 短路即可
193
53
  cancelled = true;
194
54
  stopObserving();
195
55
  };
196
- }, [title, enabled, layoutKey, skipDomWrite, labelRef, titleRef, trailingRef, typeIconRef]);
56
+ }, [title, layoutKey, skipTooltip, labelRef, titleRef]);
197
57
  }
198
58
  function ChatItemStatus(_ref) {
199
59
  var status = _ref.status;
@@ -340,7 +200,6 @@ function ChatItemInner(_ref2) {
340
200
  var labelRef = useRef(null);
341
201
  var titleRef = useRef(null);
342
202
  var typeIconRef = useRef(null);
343
- var trailingRef = useRef(null);
344
203
  // active 时隐藏 unread/error;generating / waiting 仍展示(当前会话仍在进行)
345
204
  var showStatus = Boolean(status && (!active || status === 'generating' || status === 'waiting'));
346
205
  var showTypeIcon = type === 'human';
@@ -349,10 +208,7 @@ function ChatItemInner(_ref2) {
349
208
  }, [time, now]);
350
209
  var showTime = Boolean(timeLabel);
351
210
  var showTrailing = showStatus || showTime;
352
- var fitTitle = showTypeIcon || showTrailing;
353
- useFittedTitle(title, fitTitle, labelRef, titleRef, trailingRef, typeIconRef, // trailing / 字重的全部变化来源。这些都会改标题可用宽度而行宽不变,
354
- // 提前声明即可,无需为此额外监听 label / trailing / typeIcon 的尺寸。
355
- "".concat(active ? 1 : 0, ":").concat(menuOpen ? 1 : 0, ":").concat(timeLabel, ":").concat(status !== null && status !== void 0 ? status : '', ":").concat(showTypeIcon ? 1 : 0), Boolean(highlightQuery === null || highlightQuery === void 0 ? void 0 : highlightQuery.trim()));
211
+ useChatItemTitleLayout(title, labelRef, titleRef, "".concat(active ? 1 : 0, ":").concat(menuOpen ? 1 : 0, ":").concat(timeLabel, ":").concat(status !== null && status !== void 0 ? status : '', ":").concat(showTypeIcon ? 1 : 0), Boolean(highlightQuery === null || highlightQuery === void 0 ? void 0 : highlightQuery.trim()));
356
212
  useEffect(function () {
357
213
  if (!renaming) setDraft(title);
358
214
  }, [title, renaming]);
@@ -473,8 +329,7 @@ function ChatItemInner(_ref2) {
473
329
  name: "support-bold",
474
330
  size: 12
475
331
  })) : null, showTrailing ? /*#__PURE__*/React.createElement("span", {
476
- "data-odn-chat-item-trailing": true,
477
- ref: trailingRef
332
+ "data-odn-chat-item-trailing": true
478
333
  }, showStatus && status ? /*#__PURE__*/React.createElement(ChatItemStatus, {
479
334
  status: status
480
335
  }) : null, showTime ? /*#__PURE__*/React.createElement("time", {
@@ -9,31 +9,37 @@
9
9
  display: block;
10
10
  width: 100%;
11
11
  min-width: 0;
12
+ /* 根节点与内层按钮光标必须一致。根是 auto 时,时间收宽会让命中点在手指和箭头之间来回跳 */
13
+ cursor: pointer;
12
14
  }
13
15
  [data-odn-chat-item]:hover [data-odn-chat-item-menu-trigger], [data-odn-chat-item]:has(:focus-visible) [data-odn-chat-item-menu-trigger] {
14
16
  opacity: 1;
15
17
  pointer-events: auto;
16
18
  }
17
19
  [data-odn-chat-item] {
18
- /* 无时间才加 36;有时间则占位已在,避免标题变宽、离开时时间从行外闪回 */
20
+ /* hover / 聚焦腾出 36 给 …,标题补进让出的时间位 */
19
21
  }
20
- [data-odn-chat-item]:hover [data-odn-chat-item-btn]:not(:has([data-odn-chat-item-time])), [data-odn-chat-item]:has(:focus-visible) [data-odn-chat-item-btn]:not(:has([data-odn-chat-item-time])), [data-odn-chat-item]:has([data-odn-chat-item-menu-trigger-open]) [data-odn-chat-item-btn] {
22
+ [data-odn-chat-item]:hover [data-odn-chat-item-btn], [data-odn-chat-item]:has(:focus-visible) [data-odn-chat-item-btn], [data-odn-chat-item]:has([data-odn-chat-item-menu-trigger-open]) [data-odn-chat-item-btn] {
21
23
  padding-right: 36px;
22
24
  }
23
25
  [data-odn-chat-item] {
24
- /* hover:只隐身,宽度留下。收宽度会让移出时时间从行外闪回 */
26
+ /*
27
+ * 同槽让位:时间瞬时退出布局,标题补到 … 前。
28
+ * 无 status 时收掉 trailing 左间距,避免标题与 … 之间空档。
29
+ */
25
30
  }
26
31
  [data-odn-chat-item]:hover [data-odn-chat-item-time], [data-odn-chat-item]:has(:focus-visible) [data-odn-chat-item-time] {
27
32
  opacity: 0;
33
+ width: 0;
34
+ min-width: 0;
35
+ max-width: 0;
36
+ flex: 0 0 0;
37
+ margin: 0;
38
+ overflow: hidden;
28
39
  pointer-events: none;
29
40
  }
30
- [data-odn-chat-item] {
31
- /* hover + 状态 + 时间:同一颗状态 relative 右移,占位不收,移出不飞 */
32
- }
33
- [data-odn-chat-item]:hover:has([data-odn-chat-item-time]):not([data-odn-chat-item-time-yield]) [data-odn-chat-item-status], [data-odn-chat-item]:has(:focus-visible):has([data-odn-chat-item-time]):not([data-odn-chat-item-time-yield]) [data-odn-chat-item-status] {
34
- position: relative;
35
- z-index: 1;
36
- left: max(0px, 6px + var(--odn-chat-item-time-w, 0px) - 24px);
41
+ [data-odn-chat-item]:hover [data-odn-chat-item-trailing]:not(:has([data-odn-chat-item-status])), [data-odn-chat-item]:has(:focus-visible) [data-odn-chat-item-trailing]:not(:has([data-odn-chat-item-status])) {
42
+ padding-left: 0;
37
43
  }
38
44
  [data-odn-chat-item] {
39
45
  /* 选中 / 菜单开:时间退出布局,右侧 36 给 …。
@@ -79,6 +85,7 @@
79
85
 
80
86
  /* 无痕输入框:完全继承父按钮的排版,保证尺寸/位置 1:1 对齐 title 文本 */
81
87
  [data-odn-chat-item-rename-input] {
88
+ cursor: text;
82
89
  flex: 1 1 auto;
83
90
  min-width: 0;
84
91
  width: 100%;
@@ -137,7 +144,7 @@
137
144
  }
138
145
 
139
146
  [data-odn-chat-item-title] {
140
- /* 0 1 auto:短标题不撑开,类型 icon 紧跟;长标题可收缩省略 */
147
+ /* 0 1 auto:短标题不撑开,类型 icon 紧跟;长标题收缩到 trailing 前再省略 */
141
148
  flex: 0 1 auto;
142
149
  min-width: 0;
143
150
  font-size: 14px;
@@ -153,13 +160,6 @@
153
160
  font: inherit;
154
161
  }
155
162
 
156
- /* JS 已裁到可用宽度:按内容收缩,省略号后无字符边界留白;量准时整组仍铺满行 */
157
- [data-odn-chat-item-title-fitted] {
158
- flex: 0 0 auto;
159
- overflow: visible;
160
- text-overflow: clip;
161
- }
162
-
163
163
  [data-odn-chat-item-title-bold] {
164
164
  font-weight: 500;
165
165
  }
@@ -137,6 +137,8 @@
137
137
  min-width: 0;
138
138
  min-height: 0;
139
139
  overflow-anchor: none;
140
+ /* 行与右侧滚动槽、行间缝都用手指。槽是 default 时,悬停收时间会让光标在缝上来回跳 */
141
+ cursor: pointer;
140
142
  }
141
143
 
142
144
  [data-odn-chat-list-fade] {
@@ -190,7 +192,7 @@
190
192
  font-weight: 400;
191
193
  line-height: 18px;
192
194
  user-select: none;
193
- pointer-events: none;
195
+ cursor: default;
194
196
  }
195
197
 
196
198
  [data-odn-chat-list-pinned-body],
@@ -381,6 +383,7 @@
381
383
  align-items: center;
382
384
  gap: 4px;
383
385
  min-width: 0;
386
+ cursor: default;
384
387
  /* 左缘与标题栏 / ChatItem / 置顶分区头同为 12px */
385
388
  padding: 4px 4px 2px 12px;
386
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.107",
3
+ "version": "0.0.109",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "license": "MIT",
6
6
  "type": "module",