one-design-next 0.0.120 → 0.0.122

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.
@@ -142,11 +142,25 @@ export interface ChipData {
142
142
  *
143
143
  * **不是正文内容**:既不进 `value`(无 marker),也不进剪贴板;只在
144
144
  * 「结构谓词成立」时作为派生的只读视觉渲染在 chip 之后——即该 chip 是
145
- * 最后一个内容、且其后**一个字符都没有**(空格 / 换行都算内容;详见
146
- * `resolveHintTarget`)。
145
+ * 最后一个内容、其后**一个字符都没有**(空格 / 换行都算内容),且它的
146
+ * hint 还没关闭(见 `hintClosed`;详见 `resolveHintTarget`)。
147
147
  * 发送时由 `rawValueToSegments` 按同一谓词追加为 `ComposerHintSegment`。
148
148
  */
149
149
  hint?: string;
150
+ /**
151
+ * 该 chip 的 hint 是否已经关闭(一次性,只关不开)。
152
+ *
153
+ * hint 的机会只有一次:只要这颗 chip **后面出现过东西**——插入时后面就有
154
+ * 文字、用户后面敲了字符 / 空格 / 换行,或者又插了一块 skill——就置为
155
+ * `true`。此后即使把后面的内容删回空串,灰字也不会再出现,发送时也不再补
156
+ * `hint` 段。
157
+ *
158
+ * 由编辑器在内容变化时写入(`resolveHintClosedChipIds`),业务不需要读写;
159
+ * 它随 `chips` 快照透传,业务若把 chips 一起存进草稿,恢复后 hint 也保持
160
+ * 关闭。skill 本身没有 `hint` 时不写这个字段——那是「没有这个能力」,不是
161
+ * 状态。
162
+ */
163
+ hintClosed?: boolean;
150
164
  }
151
165
  export interface SkillItem {
152
166
  id: string;
@@ -159,6 +173,8 @@ export interface SkillItem {
159
173
  * 选中插入后,只要该 skill chip 是输入框里最后一个内容且其后没有其它内容
160
174
  * (空格 / 换行都算内容),就会以只读灰字形式渲染在 chip 后面;用户在其后
161
175
  * 输入任何字符或换行即自动隐藏。与光标位置无关。
176
+ * 灰字是一次性的:一旦收起(或者插入时后面本来就有内容),再把后面的内容
177
+ * 删掉也不会重新出现。
162
178
  */
163
179
  hint?: string;
164
180
  /**
@@ -224,7 +240,8 @@ export interface ComposerMentionSegment {
224
240
  }
225
241
  /**
226
242
  * 派生的 skill 提示段:**不是用户输入的内容**,而是「最后一个 skill chip + 其后
227
- * 无内容」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板文案。
243
+ * 无内容 + hint 未关闭」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板
244
+ * 文案。
228
245
  *
229
246
  * 只在 `onSend` 的 segments 里出现;不出现在 `value` / `chips` / 剪贴板里。
230
247
  * 需要忽略它的业务可以直接 filter,或用 `isHintSegment` 收窄。
@@ -56,7 +56,8 @@ export var ComposerSegmentType = {
56
56
 
57
57
  /**
58
58
  * 派生的 skill 提示段:**不是用户输入的内容**,而是「最后一个 skill chip + 其后
59
- * 无内容」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板文案。
59
+ * 无内容 + hint 未关闭」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板
60
+ * 文案。
60
61
  *
61
62
  * 只在 `onSend` 的 segments 里出现;不出现在 `value` / `chips` / 剪贴板里。
62
63
  * 需要忽略它的业务可以直接 filter,或用 `isHintSegment` 收窄。
@@ -24,6 +24,10 @@ export interface ChatGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ti
24
24
  * 左侧文件夹 icon(用户分组缩进标识)。默认 `false`。
25
25
  */
26
26
  folderIcon?: boolean;
27
+ /** 替换默认文件夹图标。 */
28
+ folder?: ReactNode;
29
+ /** 替换默认展开箭头。 */
30
+ chevron?: ReactNode;
27
31
  /**
28
32
  * 分区头样式(如「未分组」)。与 ChatList「分组」同为 12px / black-9,
29
33
  * 不是用户文件夹组。
@@ -36,6 +40,11 @@ export interface ChatGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ti
36
40
  extra?: ReactNode;
37
41
  /** 强制显示 extra(如菜单打开时) */
38
42
  extraVisible?: boolean;
43
+ /**
44
+ * 标题右侧条数。平时占宽;组头悬停、键盘焦点或 `extraVisible` 时退出,
45
+ * 同一位置换成 `extra`。不传则不展示。不从 children 计数。
46
+ */
47
+ count?: ReactNode;
39
48
  /**
40
49
  * 组内每页条数。默认 5;`false` 关闭分页(搜索命中等)。
41
50
  * 首次展开与每次「展开更多」都只增加一页,收起再打开回到第一页。
@@ -48,7 +57,7 @@ export interface ChatGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ti
48
57
  overflowHint?: boolean;
49
58
  children?: ReactNode;
50
59
  }
51
- export declare function ChatGroup({ title, collapsible, collapsed: collapsedProp, defaultCollapsed, onCollapsedChange, renaming: renamingProp, defaultRenaming, onRenamingChange, onRename, folderIcon, section, extra, extraVisible, previewLimit, overflowHint, children, className, ...rest }: ChatGroupProps): React.JSX.Element;
60
+ export declare function ChatGroup({ title, collapsible, collapsed: collapsedProp, defaultCollapsed, onCollapsedChange, renaming: renamingProp, defaultRenaming, onRenamingChange, onRename, folderIcon, folder, chevron, section, extra, extraVisible, count, previewLimit, overflowHint, children, className, ...rest }: ChatGroupProps): React.JSX.Element;
52
61
  export declare namespace ChatGroup {
53
62
  var displayName: string;
54
63
  }
@@ -1,7 +1,7 @@
1
1
  import _extends from "@babel/runtime/helpers/esm/extends";
2
2
  import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
3
3
  import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
4
- var _excluded = ["title", "collapsible", "collapsed", "defaultCollapsed", "onCollapsedChange", "renaming", "defaultRenaming", "onRenamingChange", "onRename", "folderIcon", "section", "extra", "extraVisible", "previewLimit", "overflowHint", "children", "className"];
4
+ var _excluded = ["title", "collapsible", "collapsed", "defaultCollapsed", "onCollapsedChange", "renaming", "defaultRenaming", "onRenamingChange", "onRename", "folderIcon", "folder", "chevron", "section", "extra", "extraVisible", "count", "previewLimit", "overflowHint", "children", "className"];
5
5
  import clsx from 'clsx';
6
6
  import React, { Children, useEffect, useLayoutEffect, useRef, useState } from 'react';
7
7
  import HoverFill from "../hover-fill";
@@ -66,10 +66,13 @@ export function ChatGroup(_ref2) {
66
66
  onRename = _ref2.onRename,
67
67
  _ref2$folderIcon = _ref2.folderIcon,
68
68
  folderIcon = _ref2$folderIcon === void 0 ? false : _ref2$folderIcon,
69
+ folder = _ref2.folder,
70
+ chevron = _ref2.chevron,
69
71
  _ref2$section = _ref2.section,
70
72
  section = _ref2$section === void 0 ? false : _ref2$section,
71
73
  extra = _ref2.extra,
72
74
  extraVisible = _ref2.extraVisible,
75
+ count = _ref2.count,
73
76
  previewLimit = _ref2.previewLimit,
74
77
  _ref2$overflowHint = _ref2.overflowHint,
75
78
  overflowHint = _ref2$overflowHint === void 0 ? true : _ref2$overflowHint,
@@ -172,15 +175,31 @@ export function ChatGroup(_ref2) {
172
175
  }, /*#__PURE__*/React.createElement("div", {
173
176
  "data-odn-chat-group-slot-inner": true
174
177
  }, expandHint)) : expandHint : null);
178
+ var showCount = !editingTitle && count !== null && count !== undefined && count !== false && count !== '';
179
+ var showExtra = Boolean(extra) && !editingTitle;
180
+ // 条数和 extra 叠在同一格里,格子宽度取两者较大值。
181
+ // 让位时只切换可见性,长标题旁的箭头不会跟着挪。
182
+ var showTrailing = showCount && showExtra ? /*#__PURE__*/React.createElement("span", {
183
+ "data-odn-chat-group-aside": true
184
+ }, /*#__PURE__*/React.createElement("span", {
185
+ "data-odn-chat-group-count": true
186
+ }, count), /*#__PURE__*/React.createElement("div", {
187
+ "data-odn-chat-group-extra": true
188
+ }, extra)) : /*#__PURE__*/React.createElement(React.Fragment, null, showCount ? /*#__PURE__*/React.createElement("span", {
189
+ "data-odn-chat-group-count": true
190
+ }, count) : null, showExtra ? /*#__PURE__*/React.createElement("div", {
191
+ "data-odn-chat-group-extra": true
192
+ }, extra) : null);
175
193
  var header = /*#__PURE__*/React.createElement("div", {
176
194
  "data-odn-chat-group-header": true
177
195
  }, editingTitle ? /*#__PURE__*/React.createElement("div", {
178
196
  "data-odn-chat-group-rename": true
179
- }, folderIcon ? /*#__PURE__*/React.createElement(Icon, {
180
- name: "folder",
181
- size: 14,
197
+ }, folderIcon ? /*#__PURE__*/React.createElement("span", {
182
198
  "data-odn-chat-group-folder-icon": true
183
- }) : null, /*#__PURE__*/React.createElement("input", {
199
+ }, folder !== null && folder !== void 0 ? folder : /*#__PURE__*/React.createElement(Icon, {
200
+ name: "folder",
201
+ size: 14
202
+ })) : null, /*#__PURE__*/React.createElement("input", {
184
203
  ref: inputRef,
185
204
  value: draft,
186
205
  "aria-label": "\u91CD\u547D\u540D\u5206\u7EC4",
@@ -208,27 +227,28 @@ export function ChatGroup(_ref2) {
208
227
  "data-odn-chat-group-trigger": true,
209
228
  onClick: toggle,
210
229
  "aria-expanded": !collapsed
211
- }, folderIcon ? /*#__PURE__*/React.createElement(Icon, {
212
- name: "folder",
213
- size: 14,
230
+ }, folderIcon ? /*#__PURE__*/React.createElement("span", {
214
231
  "data-odn-chat-group-folder-icon": true
215
- }) : null, /*#__PURE__*/React.createElement("span", {
232
+ }, folder !== null && folder !== void 0 ? folder : /*#__PURE__*/React.createElement(Icon, {
233
+ name: "folder",
234
+ size: 14
235
+ })) : null, /*#__PURE__*/React.createElement("span", {
216
236
  "data-odn-chat-group-title": true
217
- }, title), /*#__PURE__*/React.createElement(Icon, {
218
- name: "right",
219
- size: 12,
237
+ }, title), /*#__PURE__*/React.createElement("span", {
220
238
  "data-odn-chat-group-chevron": true
221
- })) : /*#__PURE__*/React.createElement("div", {
239
+ }, chevron !== null && chevron !== void 0 ? chevron : /*#__PURE__*/React.createElement(Icon, {
240
+ name: "right",
241
+ size: 12
242
+ }))) : /*#__PURE__*/React.createElement("div", {
222
243
  "data-odn-chat-group-title-row": true
223
- }, folderIcon ? /*#__PURE__*/React.createElement(Icon, {
224
- name: "folder",
225
- size: 14,
244
+ }, folderIcon ? /*#__PURE__*/React.createElement("span", {
226
245
  "data-odn-chat-group-folder-icon": true
227
- }) : null, /*#__PURE__*/React.createElement("span", {
246
+ }, folder !== null && folder !== void 0 ? folder : /*#__PURE__*/React.createElement(Icon, {
247
+ name: "folder",
248
+ size: 14
249
+ })) : null, /*#__PURE__*/React.createElement("span", {
228
250
  "data-odn-chat-group-title": true
229
- }, title)), extra && !editingTitle ? /*#__PURE__*/React.createElement("div", {
230
- "data-odn-chat-group-extra": true
231
- }, extra) : null);
251
+ }, title)), showTrailing);
232
252
  return /*#__PURE__*/React.createElement("div", _extends({
233
253
  "data-odn-chat-group": true,
234
254
  "data-odn-chat-group-collapsed": collapsed || undefined,
@@ -26,6 +26,9 @@
26
26
  [data-odn-chat-group-header]:hover, [data-odn-chat-group-header]:has(:focus-visible) {
27
27
  gap: 4px;
28
28
  }
29
+ [data-odn-chat-group-header]:hover:has([data-odn-chat-group-aside]), [data-odn-chat-group-header]:has(:focus-visible):has([data-odn-chat-group-aside]) {
30
+ gap: 0;
31
+ }
29
32
  [data-odn-chat-group-header]:hover [data-odn-chat-group-extra], [data-odn-chat-group-header]:has(:focus-visible) [data-odn-chat-group-extra] {
30
33
  flex-basis: auto;
31
34
  width: auto;
@@ -39,6 +42,10 @@
39
42
  gap: 4px;
40
43
  }
41
44
 
45
+ [data-odn-chat-group-extra-visible] [data-odn-chat-group-header]:has([data-odn-chat-group-aside]) {
46
+ gap: 0;
47
+ }
48
+
42
49
  [data-odn-chat-group-extra-visible] [data-odn-chat-group-extra] {
43
50
  flex-basis: auto;
44
51
  width: auto;
@@ -115,6 +122,51 @@
115
122
  white-space: nowrap;
116
123
  }
117
124
 
125
+ [data-odn-chat-group-count] {
126
+ flex: none;
127
+ color: var(--odn-color-black-9);
128
+ font-size: 14px;
129
+ font-weight: 400;
130
+ line-height: 22px;
131
+ }
132
+
133
+ [data-odn-chat-group-aside] {
134
+ display: grid;
135
+ flex: none;
136
+ align-items: center;
137
+ justify-items: end;
138
+ }
139
+
140
+ [data-odn-chat-group-aside] > [data-odn-chat-group-count],
141
+ [data-odn-chat-group-aside] > [data-odn-chat-group-extra] {
142
+ grid-area: 1/1;
143
+ }
144
+
145
+ [data-odn-chat-group-aside] > [data-odn-chat-group-extra] {
146
+ width: auto;
147
+ max-width: none;
148
+ min-width: 0;
149
+ flex-basis: auto;
150
+ overflow: visible;
151
+ opacity: 0;
152
+ visibility: hidden;
153
+ pointer-events: none;
154
+ }
155
+
156
+ [data-odn-chat-group-header]:hover [data-odn-chat-group-aside] > [data-odn-chat-group-count],
157
+ [data-odn-chat-group-header]:has(:focus-visible) [data-odn-chat-group-aside] > [data-odn-chat-group-count],
158
+ [data-odn-chat-group-extra-visible] [data-odn-chat-group-aside] > [data-odn-chat-group-count] {
159
+ visibility: hidden;
160
+ }
161
+
162
+ [data-odn-chat-group-header]:hover [data-odn-chat-group-aside] > [data-odn-chat-group-extra],
163
+ [data-odn-chat-group-header]:has(:focus-visible) [data-odn-chat-group-aside] > [data-odn-chat-group-extra],
164
+ [data-odn-chat-group-extra-visible] [data-odn-chat-group-aside] > [data-odn-chat-group-extra] {
165
+ opacity: 1;
166
+ visibility: visible;
167
+ pointer-events: auto;
168
+ }
169
+
118
170
  [data-odn-chat-group-extra] {
119
171
  flex: 0 0 0;
120
172
  display: flex;
@@ -29,7 +29,7 @@ import { buildClipboardPayload, COMPOSER_CLIPBOARD_MIME, parseClipboardPayload,
29
29
  import { useChipManager } from "./hooks/useChipManager";
30
30
  import { useChipSelectionMarker } from "./hooks/useChipSelectionMarker";
31
31
  import { chipHasEditableParams, ComposerInlineRef } from "./inline-ref";
32
- import { resolveHintTarget } from "./segments";
32
+ import { resolveHintClosedChipIds, resolveHintTarget } from "./segments";
33
33
  import { COMPOSER_HINT_ACCEPT_ATTR, COMPOSER_HINT_ACCEPT_SELECTOR, COMPOSER_HINT_ATTR, createLineRoot, createLineRootWithText, ensureCaretAnchorAfter, ensureLineRoots, getPlainText, isEditorEmpty, isLineRoot, LINE_ATTR_NAME, normalizeEditorDom, probeTrigger, stripInvisibleChars, ZWSP } from "./utils";
34
34
 
35
35
  /** Composer editor 触发态对外信号——告诉父组件该弹浮层选 skill 了。 */
@@ -490,6 +490,9 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
490
490
  var getChipsRef = useRef(function () {
491
491
  return [];
492
492
  });
493
+ /* hint 关闭器:与 getChipsRef 同一套「ref 打破声明顺序」做法——fireChange 声明
494
+ * 在 useChipManager 之前,而关闭方法由后者提供。 */
495
+ var closeChipHintsRef = useRef(function () {});
493
496
 
494
497
  /* ---- 派生 hint:imperative DOM 同步 ----
495
498
  * hint 必须是 chip host 的**兄弟节点**,不能塞在 host 内部:host 是
@@ -615,11 +618,16 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
615
618
  normalizeEditorDom(el);
616
619
  var chips = getChipsRef.current();
617
620
  var raw = getPlainText(el);
618
- /* 派生 hint 与 value 同源同刻重算:结构谓词只依赖 raw + chips(换行也算
621
+ /* hint 是一次性的:先把「后面出现过东西」的 chip 永久关闭,再算谓词。
622
+ * 顺序不能反——反了的话,删到空的那一帧会先渲染出灰字再被移除(闪一下)。
623
+ * 关闭会改写 chips 快照,所以谓词必须用关闭后的快照重新取。
624
+ *
625
+ * 派生 hint 与 value 同源同刻重算:结构谓词只依赖 raw + chips(换行也算
619
626
  * 内容,所以按 Enter 后 hint 自动消失)。必须先落 DOM 再算 empty / 高度——
620
627
  * 否则「chip 被删」时残留的 hint 文本会让 placeholder 判定与 lite 升格
621
628
  * 判定晚一拍。 */
622
- syncHintNode(resolveHintTarget(raw, chips));
629
+ closeChipHintsRef.current(resolveHintClosedChipIds(raw, chips));
630
+ syncHintNode(resolveHintTarget(raw, getChipsRef.current()));
623
631
  var showPlaceholder = shouldShowPlaceholder(el);
624
632
  if (forceNonEmptyOnceRef.current && showPlaceholder) {
625
633
  setEmpty(false);
@@ -648,10 +656,12 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
648
656
  syncChipsAfterDomMutation = _useChipManager.syncChipsAfterDomMutation,
649
657
  removeChip = _useChipManager.removeChip,
650
658
  updateChipData = _useChipManager.updateChipData,
659
+ closeChipHints = _useChipManager.closeChipHints,
651
660
  getChipHost = _useChipManager.getChipHost,
652
661
  getChips = _useChipManager.getChips,
653
662
  resetChips = _useChipManager.resetChips;
654
663
  getChipsRef.current = getChips;
664
+ closeChipHintsRef.current = closeChipHints;
655
665
  useChipSelectionMarker(editorRef, chips);
656
666
  var insertSerializedAtRangeRef = useRef(insertSerializedAtRange);
657
667
  insertSerializedAtRangeRef.current = insertSerializedAtRange;
@@ -49,6 +49,14 @@ export interface UseChipManagerReturn {
49
49
  removeChip: (id: string) => void;
50
50
  /** 更新 chip 数据(参数面板关闭保存等)。 */
51
51
  updateChipData: (id: string, patch: Partial<ChipData>) => void;
52
+ /**
53
+ * 把这些 chip 的 hint 标记为已关闭(`ChipData.hintClosed`)。
54
+ *
55
+ * 只改 chips 快照里的数据,**不抛 onChange / 不走 onAfterMutate**:调用点在
56
+ * fireChange 内部,走 onAfterMutate 会立刻再进一次 fireChange(递归)。
57
+ * 幂等:已经关闭的 / 不存在的 id 直接忽略。
58
+ */
59
+ closeChipHints: (ids: readonly string[]) => void;
52
60
  /** 按 id 取 chip host(参数面板 anchor)。 */
53
61
  getChipHost: (id: string) => HTMLSpanElement | null;
54
62
  /** 当前 chip 快照(读 ref,插入后同步 onChange 时可用)。 */
@@ -397,6 +397,27 @@ onAfterMutate) {
397
397
  setChips(next);
398
398
  onAfterMutate === null || onAfterMutate === void 0 || onAfterMutate();
399
399
  }, [onAfterMutate]);
400
+ var closeChipHints = useCallback(function (ids) {
401
+ if (ids.length === 0) return;
402
+ var targets = new Set(ids);
403
+ var mutated = false;
404
+ /* 不就地写 `data`:insertChip / insertSerializedAtRange 拿到的对象可能属于
405
+ * 调用方(defaultChips / insertContent 的 chips),原地改会把内部状态写到
406
+ * 业务数据上。 */
407
+ var next = chipsRef.current.map(function (chip) {
408
+ if (!targets.has(chip.data.id) || chip.data.hintClosed) return chip;
409
+ mutated = true;
410
+ return _objectSpread(_objectSpread({}, chip), {}, {
411
+ data: _objectSpread(_objectSpread({}, chip.data), {}, {
412
+ hintClosed: true
413
+ })
414
+ });
415
+ });
416
+ if (!mutated) return;
417
+ chipsRef.current = next;
418
+ // 不调 onAfterMutate:调用方就在 fireChange 里,否则会递归
419
+ setChips(next);
420
+ }, []);
400
421
  var getChipHost = useCallback(function (id) {
401
422
  var _chipsRef$current$fin, _chipsRef$current$fin2;
402
423
  return (_chipsRef$current$fin = (_chipsRef$current$fin2 = chipsRef.current.find(function (c) {
@@ -422,6 +443,7 @@ onAfterMutate) {
422
443
  syncChipsAfterDomMutation: syncChipsAfterDomMutation,
423
444
  removeChip: removeChip,
424
445
  updateChipData: updateChipData,
446
+ closeChipHints: closeChipHints,
425
447
  getChipHost: getChipHost,
426
448
  getChips: getChips,
427
449
  resetChips: resetChips
@@ -111,11 +111,11 @@ export interface ComposerProps {
111
111
  * `segments`:有序内容 IR,业务按数组顺序自定义拼后端。
112
112
  * `meta`:附件 / 联网搜索等非正文数据;为空时不传。
113
113
  *
114
- * **派生 `hint` 段**:当最后一个 chip 是带 `SkillItem.hint` 的 skill、且其后
115
- * 一个字符都没有(空格 / 换行都算内容)时,该提示文案会作为
116
- * `{ type: 'hint', data: { text } }` 出现在 segments 末尾——与输入框里那段
117
- * 灰字同源同谓词,不会漂移。不需要它的业务直接 filter / `isHintSegment`
118
- * 判一下即可。
114
+ * **派生 `hint` 段**:当最后一个 chip 是带 `SkillItem.hint` 的 skill、其后
115
+ * 一个字符都没有(空格 / 换行都算内容)、且这颗 chip 的 hint 还没关闭时,
116
+ * 该提示文案会作为 `{ type: 'hint', data: { text } }` 出现在 segments 末尾
117
+ * ——与输入框里那段灰字同源同谓词,不会漂移。不需要它的业务直接 filter /
118
+ * `isHintSegment` 判一下即可。
119
119
  *
120
120
  * 兼容旧链路(需要单串文本):业务可调 `segmentsToReadableText(segments)`。
121
121
  *
@@ -514,9 +514,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
514
514
  * - segments:有序 IR(text / invocation / mention 交替)
515
515
  * - meta:attachments / webSearch 等非正文数据;都为空时不传
516
516
  *
517
- * 派生 hint:最后一个 chip 是 skill 且其后没有内容时,`rawValueToSegments`
518
- * 会在末尾追加一段 `hint`(与该 chip 后面渲染的灰字同源、同谓词)。
519
- * 它不属于用户输入,也不是受控 value 的一部分。
517
+ * 派生 hint:最后一个 chip 是 skill、其后没有内容、且它的 hint 还没关闭时,
518
+ * `rawValueToSegments` 会在末尾追加一段 `hint`(与该 chip 后面渲染的灰字
519
+ * 同源、同谓词)。它不属于用户输入,也不是受控 value 的一部分;hint 一旦
520
+ * 关闭(`ChipData.hintClosed`)就不再追加——没显示给用户的模板不会被发出去。
520
521
  *
521
522
  * 需要单串文本的业务方在 onSend 内自行调 segmentsToReadableText。
522
523
  * onSend 返回 false 或 Promise<false> 时视为发送失败,不清空;否则正常清空。
@@ -6,7 +6,10 @@
6
6
  * - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
7
7
  * 还原为 `ComposerSegment[]`。
8
8
  * - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
9
- * (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
9
+ * (空格 / 换行都算内容)+ 该 chip 带 hint + hint 未关闭」,渲染与发送
10
+ * 都靠它。
11
+ * - `resolveHintClosedChipIds`:找出「一次性机会已作废」的 chip(自己的尾串
12
+ * 出现过内容),由编辑器写回 `ChipData.hintClosed`。
10
13
  * - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
11
14
  * 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
12
15
  * - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
@@ -31,14 +34,30 @@ export interface ComposerHintTarget {
31
34
  * 1. `value` 里存在 marker(即至少有一个 chip)
32
35
  * 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
33
36
  * 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
37
+ * 4. 该 chip 的 hint 还没关闭(`hintClosed`,一次性)
34
38
  */
35
39
  export declare function resolveHintTarget(raw: string, chips: ChipData[]): ComposerHintTarget | null;
40
+ /**
41
+ * 找出「hint 的一次性机会已经作废」的 chip id(编辑器据此写回 `hintClosed`)。
42
+ *
43
+ * 判据只看每颗 chip **后面出现过什么**:本 marker 与下个 marker 之间有字符
44
+ * (文字 / 空格 / 换行),或者后面还有别的 marker(又插了一块),hint 的
45
+ * 机会就作废。因此它同时覆盖两类情况:
46
+ * - 插入时就落在正文中间(或后面紧跟着另一块):hint 从未出现,之后把后面
47
+ * 删空也不该再冒出来
48
+ * - 展示过又被内容顶掉:删回空串也不该回来
49
+ *
50
+ * 纯函数,幂等(已关闭的 chip 直接跳过,重复调用结果一致);没有 hint 文本的
51
+ * chip 不参与——没能力 ≠ 状态。
52
+ */
53
+ export declare function resolveHintClosedChipIds(raw: string, chips: ChipData[]): string[];
36
54
  /**
37
55
  * 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
38
56
  *
39
57
  * 这是「编辑器状态 → 发送内容」的**唯一映射**:
40
58
  * 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
41
- * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
59
+ * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后、其后一个字符都没有、
60
+ * 且 hint 还没关闭时)
42
61
  *
43
62
  * 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
44
63
  * 业务可以放心用它做预览 / 调试。
@@ -6,7 +6,10 @@
6
6
  * - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
7
7
  * 还原为 `ComposerSegment[]`。
8
8
  * - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
9
- * (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
9
+ * (空格 / 换行都算内容)+ 该 chip 带 hint + hint 未关闭」,渲染与发送
10
+ * 都靠它。
11
+ * - `resolveHintClosedChipIds`:找出「一次性机会已作废」的 chip(自己的尾串
12
+ * 出现过内容),由编辑器写回 `ChipData.hintClosed`。
10
13
  * - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
11
14
  * 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
12
15
  * - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
@@ -36,17 +39,22 @@ export var isHintSegment = function isHintSegment(s) {
36
39
  };
37
40
 
38
41
  /* ────────────────────────────────────────────────────────────────────
39
- * Hint 结构谓词 · 「最后一个 chip + 其后一个字符都没有」
42
+ * Hint 结构谓词 · 「最后一个 chip + 其后一个字符都没有 + 没关闭」
40
43
  *
41
44
  * hint 不是内容,而是内容结构的投影:只要最后一个 chip 之后的 value 是
42
- * **空串**,且该 chip 带 hint,就命中。
45
+ * **空串**,且该 chip 带 hint、且它的 hint 还没关闭,就命中。
43
46
  *
44
47
  * 空格、换行、文字**都算内容**:用户在 chip 后面敲下第一个字符 / 按下
45
48
  * Enter(含 Shift+Enter)都说明 skill 块之后已经有东西了,hint 必须让位。
46
- * 反过来,把这些字符(包括换行)删干净、chip 后面重新变成空串,hint 才回来。
47
49
  * 插入 chip 本身不会写空格(见 useChipManager.insertChip):行尾插入后尾串
48
50
  * 就是空串,hint 立刻命中,不需要为「自动补的空格」做特殊判断。
49
51
  *
52
+ * hint 是**一次性**的:只要这颗 chip 后面出现过东西——文字 / 空格 / 换行,
53
+ * 或者又插了一块——hint 就永久关闭(`ChipData.hintClosed`),之后把内容删回
54
+ * 空串也不会再出现,发送时也不再补 `hint` 段。因此「插入时后面本来就有内容」
55
+ * 的 chip 从一开始就是关闭态,不会因为后面被删空而凭空冒出灰字。关闭由
56
+ * `resolveHintClosedChipIds` 判定、编辑器在同一次内容变化里写回。
57
+ *
50
58
  * 谓词与光标位置无关——用户在前面打字不影响。**渲染(Editor)与发送
51
59
  * (Composer)都只依赖这一个谓词**,每次内容变化重算一次,两处永远一致。
52
60
  * ──────────────────────────────────────────────────────────────────── */
@@ -56,7 +64,7 @@ export var isHintSegment = function isHintSegment(s) {
56
64
  /**
57
65
  * 最后一个 marker 的 chip id 与它之后的尾串;没有 chip 时返回 null。
58
66
  *
59
- * 「尾串」是 hint 判定的唯一输入:谓词要求它是**空串**。
67
+ * 「尾串」是结构判定的一部分:谓词要求它是**空串**(另外还要求 hint 未关闭)。
60
68
  */
61
69
  function getLastMarkerTail(raw) {
62
70
  var markers = parseMarkers(raw);
@@ -73,6 +81,7 @@ function getLastMarkerTail(raw) {
73
81
  * 1. `value` 里存在 marker(即至少有一个 chip)
74
82
  * 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
75
83
  * 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
84
+ * 4. 该 chip 的 hint 还没关闭(`hintClosed`,一次性)
76
85
  */
77
86
  export function resolveHintTarget(raw, chips) {
78
87
  var _chip$hint;
@@ -83,19 +92,64 @@ export function resolveHintTarget(raw, chips) {
83
92
  });
84
93
  if (!chip || chip.kind === 'mention') return null;
85
94
  var text = (_chip$hint = chip.hint) === null || _chip$hint === void 0 ? void 0 : _chip$hint.trim();
86
- if (!text) return null;
95
+ if (!text || chip.hintClosed) return null;
87
96
  return {
88
97
  chipId: chip.id,
89
98
  text: text
90
99
  };
91
100
  }
92
101
 
102
+ /**
103
+ * 找出「hint 的一次性机会已经作废」的 chip id(编辑器据此写回 `hintClosed`)。
104
+ *
105
+ * 判据只看每颗 chip **后面出现过什么**:本 marker 与下个 marker 之间有字符
106
+ * (文字 / 空格 / 换行),或者后面还有别的 marker(又插了一块),hint 的
107
+ * 机会就作废。因此它同时覆盖两类情况:
108
+ * - 插入时就落在正文中间(或后面紧跟着另一块):hint 从未出现,之后把后面
109
+ * 删空也不该再冒出来
110
+ * - 展示过又被内容顶掉:删回空串也不该回来
111
+ *
112
+ * 纯函数,幂等(已关闭的 chip 直接跳过,重复调用结果一致);没有 hint 文本的
113
+ * chip 不参与——没能力 ≠ 状态。
114
+ */
115
+ export function resolveHintClosedChipIds(raw, chips) {
116
+ /* 先看有没有「还开着 hint 的 chip」:没有就不必解析 marker(无 hint 的
117
+ * composer 每次输入都会走到这里)。 */
118
+ var hasCandidate = chips.some(function (chip) {
119
+ var _chip$hint2;
120
+ return chip.kind !== 'mention' && !chip.hintClosed && !!((_chip$hint2 = chip.hint) !== null && _chip$hint2 !== void 0 && _chip$hint2.trim());
121
+ });
122
+ if (!hasCandidate || !raw) return [];
123
+ var markers = parseMarkers(raw);
124
+ if (markers.length === 0) return [];
125
+ var chipById = new Map(chips.map(function (chip) {
126
+ return [chip.id, chip];
127
+ }));
128
+ var closed = [];
129
+ var seen = new Set();
130
+ markers.forEach(function (marker, index) {
131
+ var _chip$hint3, _next$rawStart;
132
+ var chip = chipById.get(marker.id);
133
+ if (!chip || chip.kind === 'mention' || chip.hintClosed) return;
134
+ if (!((_chip$hint3 = chip.hint) !== null && _chip$hint3 !== void 0 && _chip$hint3.trim()) || seen.has(chip.id)) return;
135
+ var next = markers[index + 1];
136
+ /* 尾串 = 本 marker 结束 → 下个 marker 开始 */
137
+ var hasTextAfter = raw.slice(marker.rawEnd, (_next$rawStart = next === null || next === void 0 ? void 0 : next.rawStart) !== null && _next$rawStart !== void 0 ? _next$rawStart : raw.length).length > 0;
138
+ if (next || hasTextAfter) {
139
+ seen.add(chip.id);
140
+ closed.push(chip.id);
141
+ }
142
+ });
143
+ return closed;
144
+ }
145
+
93
146
  /**
94
147
  * 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
95
148
  *
96
149
  * 这是「编辑器状态 → 发送内容」的**唯一映射**:
97
150
  * 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
98
- * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
151
+ * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后、其后一个字符都没有、
152
+ * 且 hint 还没关闭时)
99
153
  *
100
154
  * 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
101
155
  * 业务可以放心用它做预览 / 调试。
@@ -82,9 +82,9 @@ export var skillCardSwitchStyle = {
82
82
  '--odn-switch-track-bg': 'rgba(74, 97, 143, 0.1)',
83
83
  '--odn-switch-track-bg-hover': 'rgba(74, 97, 143, 0.14)',
84
84
  '--odn-switch-track-bg-active': 'rgba(74, 97, 143, 0.18)',
85
- '--odn-switch-track-bg-checked': '#2694FD',
86
- '--odn-switch-track-bg-checked-hover': '#2694FD',
87
- '--odn-switch-track-bg-checked-active': '#2694FD'
85
+ '--odn-switch-track-bg-checked': 'var(--odn-color-brand-6)',
86
+ '--odn-switch-track-bg-checked-hover': 'var(--odn-color-brand-6)',
87
+ '--odn-switch-track-bg-checked-active': 'var(--odn-color-brand-6)'
88
88
  };
89
89
  var TAG_GAP = 6;
90
90
  var MIN_TRUNCATABLE_TAG_WIDTH = 36;
@@ -5,12 +5,16 @@ html {
5
5
  --odn-skill-card-action-gap: 8px;
6
6
  --odn-skill-card-hover-duration: 0.3s;
7
7
  --odn-skill-card-hover-ease: cubic-bezier(0.4, 0, 0.2, 1);
8
- --odn-skill-card-text: rgba(0, 0, 0, 0.95);
9
- --odn-skill-card-secondary: rgba(51, 55, 61, 0.58);
10
- --odn-skill-card-source: #296bef;
11
- --odn-skill-card-source-bg: rgba(41, 107, 239, 0.1);
12
- --odn-skill-card-soft-bg: rgba(74, 97, 143, 0.08);
13
- --odn-skill-card-border: rgba(74, 97, 143, 0.08);
8
+ --odn-skill-card-text: var(--odn-color-black-12);
9
+ --odn-skill-card-secondary: var(--odn-color-black-9);
10
+ --odn-skill-card-source: var(--odn-color-brand-6);
11
+ --odn-skill-card-source-bg: color-mix(
12
+ in srgb,
13
+ var(--odn-color-brand-6) 10%,
14
+ transparent
15
+ );
16
+ --odn-skill-card-soft-bg: var(--odn-color-black-3);
17
+ --odn-skill-card-border: var(--odn-color-black-3);
14
18
  }
15
19
 
16
20
  [data-odn-skill-card] {
@@ -73,7 +77,7 @@ html {
73
77
  width: 230px;
74
78
  height: 130px;
75
79
  transform: rotate(-8.4deg);
76
- background-image: radial-gradient(ellipse at center, rgb(38, 148, 253), transparent 60%);
80
+ background-image: radial-gradient(ellipse at center, var(--odn-color-brand-6), transparent 60%);
77
81
  }
78
82
 
79
83
  [data-odn-skill-card][data-odn-skill-card-hoverable=true]:hover [data-odn-skill-card-glow] {
@@ -107,7 +111,7 @@ html {
107
111
  overflow: hidden;
108
112
  border: 1px solid var(--odn-color-black-1);
109
113
  border-radius: 8px;
110
- color: rgb(38, 148, 253);
114
+ color: var(--odn-color-brand-6);
111
115
  object-fit: cover;
112
116
  }
113
117
 
@@ -132,7 +136,7 @@ html {
132
136
  [data-odn-skill-card-copy] {
133
137
  position: absolute;
134
138
  top: var(--odn-skill-card-padding);
135
- right: 70px;
139
+ right: var(--odn-skill-card-padding);
136
140
  left: 80px;
137
141
  z-index: 20;
138
142
  display: flex;
@@ -144,6 +148,10 @@ html {
144
148
  transition: left var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease), right var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease), transform var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease);
145
149
  }
146
150
 
151
+ [data-odn-skill-card][data-odn-skill-card-control=toggle] [data-odn-skill-card-copy] {
152
+ right: 70px;
153
+ }
154
+
147
155
  [data-odn-skill-card][data-odn-skill-card-control=add] [data-odn-skill-card-copy] {
148
156
  right: 62px;
149
157
  }
@@ -185,8 +193,8 @@ html {
185
193
 
186
194
  [data-odn-skill-card-name] {
187
195
  min-width: 0;
188
- max-width: calc(100% - 8px);
189
- flex: 0 1 auto;
196
+ max-width: 100%;
197
+ flex: 0 0 auto;
190
198
  overflow: hidden;
191
199
  color: var(--odn-skill-card-text);
192
200
  font-size: 14px;
@@ -198,7 +206,7 @@ html {
198
206
 
199
207
  [data-odn-skill-card-alias] {
200
208
  min-width: 0;
201
- flex: 1 1 0;
209
+ flex: 1 1 auto;
202
210
  overflow: hidden;
203
211
  color: var(--odn-skill-card-secondary);
204
212
  font-size: 14px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.120",
3
+ "version": "0.0.122",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -79,6 +79,7 @@
79
79
  "echarts": "^6.1.0",
80
80
  "echarts-for-react": "^3.0.6",
81
81
  "ldrs": "^1.1.9",
82
+ "modern-screenshot": "4.7.0",
82
83
  "motion": "^12.40.0",
83
84
  "rc-cascader": "^3.34.0",
84
85
  "rc-select": "^14.16.8",
@@ -90,7 +91,9 @@
90
91
  "rc-virtual-list": "^3.19.2",
91
92
  "react-d3-cloud": "^1.0.6",
92
93
  "react-markdown": "^10.1.0",
93
- "remark-gfm": "^4.0.1"
94
+ "remark-gfm": "^4.0.1",
95
+ "tdesign-icons-react": "0.6.11",
96
+ "tdesign-react": "1.18.2"
94
97
  },
95
98
  "devDependencies": {
96
99
  "@commitlint/cli": "^17.1.2",