one-design-next 0.0.114 → 0.0.116
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.
- package/dist/composer/editor.d.ts +13 -0
- package/dist/composer/editor.js +224 -17
- package/dist/composer/hooks/useChipManager.d.ts +4 -0
- package/dist/composer/hooks/useChipManager.js +3 -21
- package/dist/composer/index.d.ts +34 -0
- package/dist/composer/index.js +41 -17
- package/dist/composer/insert.d.ts +75 -0
- package/dist/composer/insert.js +130 -0
- package/dist/composer/style/index.css +68 -0
- package/dist/composer/utils.d.ts +18 -0
- package/dist/composer/utils.js +30 -2
- package/dist/index.d.ts +1 -0
- package/dist/skill-slot/index.js +30 -18
- package/dist/skill-slot/style/index.css +55 -15
- package/package.json +1 -1
|
@@ -122,6 +122,19 @@ export interface ComposerEditorRef {
|
|
|
122
122
|
* 配合浮层"用户选了某个 skill"的回调使用。
|
|
123
123
|
*/
|
|
124
124
|
replaceTriggerWithChip: (data: ChipData) => void;
|
|
125
|
+
/**
|
|
126
|
+
* 在内容末尾插入 raw value + chips(`ComposerRef.insertContent` 的编辑器实现)。
|
|
127
|
+
*
|
|
128
|
+
* 与粘贴 / 草稿还原共用 `insertSerializedAtRange`:一次 mutation 只抛一次
|
|
129
|
+
* onChange,派生 hint / 高度 / trigger 也只重算一次。
|
|
130
|
+
*
|
|
131
|
+
* - `focus: false`:不聚焦、不滚动,也不主动改写 selection(后台预填草稿)
|
|
132
|
+
*/
|
|
133
|
+
insertSerializedAtEnd: (value: string, chips: ChipData[], options?: {
|
|
134
|
+
focus?: boolean;
|
|
135
|
+
}) => void;
|
|
136
|
+
/** 当前 raw value(含 marker,DOM 实时值;不依赖 React state 是否已 flush) */
|
|
137
|
+
getValue: () => string;
|
|
125
138
|
/** 主动取消当前触发态(不修改 DOM,只清状态 + 抛 onTriggerChange(null))。 */
|
|
126
139
|
cancelTrigger: () => void;
|
|
127
140
|
/** 取最新的 trigger rect,便于浮层在滚动 / resize 时刷新位置。 */
|
package/dist/composer/editor.js
CHANGED
|
@@ -23,13 +23,13 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
|
|
|
23
23
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
|
|
24
24
|
import { createPortal } from 'react-dom';
|
|
25
25
|
import ScrollArea from "../scroll-area";
|
|
26
|
-
import { caretBeforeHintIn, caretRangeFromPoint, findChipHostBeforeCaret, getCurrentLine, isCaretBeforeHint, isCaretOnHintLine, isPointOnHint, isPositionAfterHint, mergeLineIntoPreviousAtCaretStart, normalizeCollapsedCaretAtChipBoundary, normalizeCollapsedCaretBeforeHint, resolveCaretMove, splitLineAt, wouldArrowCrossHint } from "./caret";
|
|
26
|
+
import { caretBeforeHintIn, caretRangeFromPoint, findChipHostBeforeCaret, getCaretBeforeHint, getCurrentLine, isCaretBeforeHint, isCaretOnHintLine, isPointOnHint, isPositionAfterHint, mergeLineIntoPreviousAtCaretStart, normalizeCollapsedCaretAtChipBoundary, normalizeCollapsedCaretBeforeHint, resolveCaretMove, splitLineAt, wouldArrowCrossHint } from "./caret";
|
|
27
27
|
import { buildClipboardPayload, COMPOSER_CLIPBOARD_MIME, parseClipboardPayload, plainTextFromClipboardPayload, remapClipboardPayload } from "./clipboard";
|
|
28
28
|
import { useChipManager } from "./hooks/useChipManager";
|
|
29
29
|
import { useChipSelectionMarker } from "./hooks/useChipSelectionMarker";
|
|
30
30
|
import { chipHasEditableParams, ComposerInlineRef } from "./inline-ref";
|
|
31
31
|
import { resolveHintTarget } from "./segments";
|
|
32
|
-
import { COMPOSER_HINT_ATTR, createLineRoot, createLineRootWithText, ensureLineRoots, getPlainText, isEditorEmpty, LINE_ATTR_NAME, normalizeEditorDom, probeTrigger, stripInvisibleChars, ZWSP } from "./utils";
|
|
32
|
+
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";
|
|
33
33
|
|
|
34
34
|
/** Composer editor 触发态对外信号——告诉父组件该弹浮层选 skill 了。 */
|
|
35
35
|
|
|
@@ -82,6 +82,137 @@ var scrollCaretIntoView = function scrollCaretIntoView(viewport) {
|
|
|
82
82
|
viewport.scrollTop -= viewportRect.top - rect.top + 4;
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
+
|
|
86
|
+
/** insertContent 的目标行:最后一个行容器;没有行模型时退回 editor 本身。 */
|
|
87
|
+
var resolveEndLine = function resolveEndLine(editor) {
|
|
88
|
+
var last = editor.lastElementChild;
|
|
89
|
+
return isLineRoot(last) ? last : editor;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** 行内直接子节点的派生 hint(不是正文,插入必须落在它之前)。 */
|
|
93
|
+
var findDirectHint = function findDirectHint(line) {
|
|
94
|
+
for (var _i = 0, _Array$from = Array.from(line.children); _i < _Array$from.length; _i++) {
|
|
95
|
+
var child = _Array$from[_i];
|
|
96
|
+
if (child.hasAttribute(COMPOSER_HINT_ATTR)) return child;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* hint 末尾的「TAB 采纳」小块:点它 = 按 Tab 采纳这段模板文案。
|
|
103
|
+
*
|
|
104
|
+
* 与 hint 文本是同级节点(见 `COMPOSER_HINT_ACCEPT_ATTR` 的说明),由
|
|
105
|
+
* `syncHintNode` 一起插入 / 移除,不进 value / 剪贴板 / segments。
|
|
106
|
+
* 视觉在 `style/index.scss`(`[data-odn-composer-hint-accept]`)。
|
|
107
|
+
*/
|
|
108
|
+
var createHintAcceptNode = function createHintAcceptNode() {
|
|
109
|
+
var accept = document.createElement('span');
|
|
110
|
+
accept.setAttribute(COMPOSER_HINT_ACCEPT_ATTR, '');
|
|
111
|
+
accept.contentEditable = 'false';
|
|
112
|
+
/* 刻意不加 role="button" / aria-label / title:
|
|
113
|
+
* - 它不可聚焦、也没有 Enter/Space 激活逻辑,挂 button 语义等于给读屏用户
|
|
114
|
+
* 一个按不动的控件(无障碍反而变成障碍)。真正的键盘路径就是它写着的
|
|
115
|
+
* Tab 键本身,可见文案已经说清动作。
|
|
116
|
+
* - aria-label 会盖掉可见文案,反而违反「可见标签即无障碍名」。
|
|
117
|
+
* - title 是浏览器原生 tooltip:延迟出现、样式不可控、会盖住灰字。 */
|
|
118
|
+
|
|
119
|
+
var key = document.createElement('span');
|
|
120
|
+
key.setAttribute('data-odn-composer-hint-accept-key', '');
|
|
121
|
+
key.textContent = 'TAB';
|
|
122
|
+
var label = document.createElement('span');
|
|
123
|
+
label.setAttribute('data-odn-composer-hint-accept-label', '');
|
|
124
|
+
label.textContent = '采纳';
|
|
125
|
+
accept.append(key, label);
|
|
126
|
+
return accept;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* insertContent 的插入点:最后一个行容器的内容末尾。
|
|
131
|
+
*
|
|
132
|
+
* 不能直接 `selectNodeContents(editor).collapse(false)`:
|
|
133
|
+
* - 行尾的派生 hint 不是内容:落在 hint 之前,避免内容插到灰字后面
|
|
134
|
+
* - 行尾的占位 `<br>` 不是换行:插到它前面。插到后面会让它从「占位」变成
|
|
135
|
+
* 真实换行,`getPlainText` 就会凭空多输出一个 '\n'
|
|
136
|
+
*/
|
|
137
|
+
var resolveEndRange = function resolveEndRange(editor) {
|
|
138
|
+
var line = resolveEndLine(editor);
|
|
139
|
+
var range = document.createRange();
|
|
140
|
+
var hint = findDirectHint(line);
|
|
141
|
+
if (hint) {
|
|
142
|
+
var before = getCaretBeforeHint(hint);
|
|
143
|
+
if (before) {
|
|
144
|
+
range.setStart(before.node, before.offset);
|
|
145
|
+
range.collapse(true);
|
|
146
|
+
return range;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
var offset = line.childNodes.length;
|
|
150
|
+
while (offset > 0) {
|
|
151
|
+
var node = line.childNodes[offset - 1];
|
|
152
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
|
|
153
|
+
offset -= 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
range.setStart(line, offset);
|
|
159
|
+
range.collapse(true);
|
|
160
|
+
return range;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* insertContent 之后把 caret 落到新内容末尾。
|
|
165
|
+
*
|
|
166
|
+
* 行尾是 chip 时补一颗不进 value 的 ZWSP 锚点(与 insertChip 一致):
|
|
167
|
+
* 只靠元素偏移落位时,部分浏览器会把 caret 带到结构块之后。
|
|
168
|
+
* 行尾有派生 hint 时仍落在 hint 之前(hint 之后不是合法插入点)。
|
|
169
|
+
*/
|
|
170
|
+
var placeCaretAtEnd = function placeCaretAtEnd(editor) {
|
|
171
|
+
var sel = window.getSelection();
|
|
172
|
+
if (!sel) return;
|
|
173
|
+
var line = resolveEndLine(editor);
|
|
174
|
+
var target;
|
|
175
|
+
var hint = findDirectHint(line);
|
|
176
|
+
var beforeHint = hint ? getCaretBeforeHint(hint) : null;
|
|
177
|
+
if (beforeHint) {
|
|
178
|
+
target = beforeHint;
|
|
179
|
+
} else {
|
|
180
|
+
var offset = line.childNodes.length;
|
|
181
|
+
while (offset > 0) {
|
|
182
|
+
var node = line.childNodes[offset - 1];
|
|
183
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
|
|
184
|
+
offset -= 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
var tail = offset > 0 ? line.childNodes[offset - 1] : null;
|
|
190
|
+
if ((tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE) {
|
|
191
|
+
var _tail$textContent;
|
|
192
|
+
target = {
|
|
193
|
+
node: tail,
|
|
194
|
+
offset: ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '').length
|
|
195
|
+
};
|
|
196
|
+
} else if (tail instanceof HTMLElement && tail.hasAttribute('data-odn-composer-chip-host')) {
|
|
197
|
+
var _anchor$textContent;
|
|
198
|
+
var _anchor = ensureCaretAnchorAfter(tail);
|
|
199
|
+
target = {
|
|
200
|
+
node: _anchor,
|
|
201
|
+
offset: ((_anchor$textContent = _anchor.textContent) !== null && _anchor$textContent !== void 0 ? _anchor$textContent : '').length
|
|
202
|
+
};
|
|
203
|
+
} else {
|
|
204
|
+
target = {
|
|
205
|
+
node: line,
|
|
206
|
+
offset: offset
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
var range = document.createRange();
|
|
211
|
+
range.setStart(target.node, target.offset);
|
|
212
|
+
range.collapse(true);
|
|
213
|
+
sel.removeAllRanges();
|
|
214
|
+
sel.addRange(range);
|
|
215
|
+
};
|
|
85
216
|
var LINE_HEIGHT = 24;
|
|
86
217
|
/** 与 SCSS `padding: 2px … 0` 对齐。10 行 = 10 * 24 + 2 = 242px。 */
|
|
87
218
|
var EDITOR_PADDING_BLOCK = 2;
|
|
@@ -256,12 +387,18 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
256
387
|
* 就不存在了,caret 只能落在整个 host 之后(= 提示文本末尾)。
|
|
257
388
|
*
|
|
258
389
|
* 它不归 React 管(editor 的 children 由 contenteditable 自治),由 fireChange
|
|
259
|
-
* 按结构谓词增删/挪位;`getPlainText` 会整棵子树跳过,所以不进 value / 剪贴板。
|
|
390
|
+
* 按结构谓词增删/挪位;`getPlainText` 会整棵子树跳过,所以不进 value / 剪贴板。
|
|
391
|
+
*
|
|
392
|
+
* 末尾的「TAB 采纳」小块是 hint 文本的**兄弟节点**(不是子节点):hint 的
|
|
393
|
+
* textContent 必须保持纯提示文案(采纳时直接落成正文,调试面板也按它读)。 */
|
|
260
394
|
var hintNodeRef = useRef(null);
|
|
395
|
+
var hintAcceptNodeRef = useRef(null);
|
|
261
396
|
var removeHintNode = useCallback(function () {
|
|
262
|
-
var _hintNodeRef$current;
|
|
397
|
+
var _hintNodeRef$current, _hintAcceptNodeRef$cu;
|
|
263
398
|
(_hintNodeRef$current = hintNodeRef.current) === null || _hintNodeRef$current === void 0 || _hintNodeRef$current.remove();
|
|
264
399
|
hintNodeRef.current = null;
|
|
400
|
+
(_hintAcceptNodeRef$cu = hintAcceptNodeRef.current) === null || _hintAcceptNodeRef$cu === void 0 || _hintAcceptNodeRef$cu.remove();
|
|
401
|
+
hintAcceptNodeRef.current = null;
|
|
265
402
|
}, []);
|
|
266
403
|
var syncHintNode = useCallback(function (target) {
|
|
267
404
|
var _Array$from$find, _blankTail;
|
|
@@ -281,6 +418,11 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
281
418
|
hintNodeRef.current = node;
|
|
282
419
|
}
|
|
283
420
|
if (node.textContent !== target.text) node.textContent = target.text;
|
|
421
|
+
var accept = hintAcceptNodeRef.current;
|
|
422
|
+
if (!accept || !accept.isConnected) {
|
|
423
|
+
accept = createHintAcceptNode();
|
|
424
|
+
hintAcceptNodeRef.current = accept;
|
|
425
|
+
}
|
|
284
426
|
|
|
285
427
|
/* 落位:命中 chip 之后、**紧随其后的不可见锚点文本(ZWSP)之后**。
|
|
286
428
|
*
|
|
@@ -295,8 +437,9 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
295
437
|
var cur = host.nextSibling;
|
|
296
438
|
while (cur) {
|
|
297
439
|
var _cur$textContent;
|
|
298
|
-
// 跳过 hint
|
|
299
|
-
|
|
440
|
+
// 跳过 hint 自身与它的「TAB 采纳」小块:上一轮若排到了锚点前面,
|
|
441
|
+
// 这里顺手挪回末尾
|
|
442
|
+
if (cur === node || cur === accept) {
|
|
300
443
|
cur = cur.nextSibling;
|
|
301
444
|
continue;
|
|
302
445
|
}
|
|
@@ -323,6 +466,12 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
323
466
|
if (node.parentNode !== parent || node.previousSibling !== anchor) {
|
|
324
467
|
parent.insertBefore(node, anchor.nextSibling);
|
|
325
468
|
}
|
|
469
|
+
/* 「TAB 采纳」小块紧跟 hint 文案之后(同一套行内流),顺序必须是
|
|
470
|
+
* hint → 小块:caret 的合法落点只到 hint 之前,块本身
|
|
471
|
+
* contentEditable=false,不会成为落点。 */
|
|
472
|
+
if (accept.parentNode !== parent || accept.previousSibling !== node) {
|
|
473
|
+
parent.insertBefore(accept, node.nextSibling);
|
|
474
|
+
}
|
|
326
475
|
}, [removeHintNode]);
|
|
327
476
|
|
|
328
477
|
/** 把 caret 直接落到 hint 之前(= chip 右侧);没有 hint 时不动。 */
|
|
@@ -427,26 +576,45 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
427
576
|
recomputeTriggerRef.current = recomputeTrigger;
|
|
428
577
|
|
|
429
578
|
/* ─────────────────────────────────────
|
|
430
|
-
* Tab
|
|
579
|
+
* 采纳 hint(Tab / 点击「TAB 采纳」小块)
|
|
431
580
|
*
|
|
432
581
|
* hint 只是结构投影(不进 value);caret 恰好停在 chip 与 hint 之间
|
|
433
582
|
* (= 内容末尾)时按 Tab,表示用户接受这段模板,于是把提示文案落成正文:
|
|
434
583
|
* chip 之后出现内容 → 结构谓词不再命中 → 灰字自动移除,onChange 抛出的
|
|
435
584
|
* value 里带上这段文案,与用户自己敲进去的效果一致。
|
|
585
|
+
*
|
|
586
|
+
* 鼠标点末尾的「TAB 采纳」小块走**同一条链路**,只是带 `force`:
|
|
587
|
+
* 不要求点击前 caret 就停在 hint 之前,先把 caret 归位到 chip 右侧再落。
|
|
436
588
|
* ───────────────────────────────────── */
|
|
437
|
-
var acceptHintAsText = useCallback(function () {
|
|
589
|
+
var acceptHintAsText = useCallback(function (options) {
|
|
438
590
|
var _hint$textContent;
|
|
439
591
|
var el = editorRef.current;
|
|
440
592
|
var hint = hintNodeRef.current;
|
|
441
593
|
var sel = window.getSelection();
|
|
442
|
-
if (!el || !hint || !hint.isConnected || !sel
|
|
443
|
-
|
|
594
|
+
if (!el || !hint || !hint.isConnected || !sel) return false;
|
|
595
|
+
var range;
|
|
596
|
+
if (options !== null && options !== void 0 && options.force) {
|
|
597
|
+
/* 点击路径:mousedown 已被 preventDefault,焦点 / caret 都还没进来,
|
|
598
|
+
* 先聚焦再把 caret 落到 hint 之前(= 内容末尾),避免原生落点乱跳。 */
|
|
599
|
+
el.focus({
|
|
600
|
+
preventScroll: true
|
|
601
|
+
});
|
|
602
|
+
var target = caretBeforeHintIn(el);
|
|
603
|
+
if (!target) return false;
|
|
604
|
+
range = document.createRange();
|
|
605
|
+
range.setStart(target.node, target.offset);
|
|
606
|
+
range.collapse(true);
|
|
607
|
+
sel.removeAllRanges();
|
|
608
|
+
sel.addRange(range);
|
|
609
|
+
lastEditorRangeRef.current = range.cloneRange();
|
|
610
|
+
} else {
|
|
611
|
+
if (sel.rangeCount === 0) return false;
|
|
612
|
+
range = sel.getRangeAt(0);
|
|
613
|
+
if (!range.collapsed || !el.contains(range.startContainer)) return false;
|
|
614
|
+
/* 只认「chip 之后、hint 之前」这一个落点;caret 在别处时 Tab 仍归浏览器,
|
|
615
|
+
* 避免在任意位置误吞焦点切换。 */
|
|
616
|
+
if (!isCaretBeforeHint(range, el)) return false;
|
|
444
617
|
}
|
|
445
|
-
var range = sel.getRangeAt(0);
|
|
446
|
-
if (!range.collapsed || !el.contains(range.startContainer)) return false;
|
|
447
|
-
/* 只认「chip 之后、hint 之前」这一个落点;caret 在别处时 Tab 仍归浏览器,
|
|
448
|
-
* 避免在任意位置误吞焦点切换。 */
|
|
449
|
-
if (!isCaretBeforeHint(range, el)) return false;
|
|
450
618
|
var text = (_hint$textContent = hint.textContent) !== null && _hint$textContent !== void 0 ? _hint$textContent : '';
|
|
451
619
|
if (!text) return false;
|
|
452
620
|
|
|
@@ -463,6 +631,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
463
631
|
caret.collapse(true);
|
|
464
632
|
sel.removeAllRanges();
|
|
465
633
|
sel.addRange(caret);
|
|
634
|
+
lastEditorRangeRef.current = caret.cloneRange();
|
|
466
635
|
}
|
|
467
636
|
fireChange();
|
|
468
637
|
recomputeTrigger();
|
|
@@ -536,6 +705,33 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
536
705
|
onTriggerChange === null || onTriggerChange === void 0 || onTriggerChange(null);
|
|
537
706
|
_insertChip(data, el);
|
|
538
707
|
},
|
|
708
|
+
insertSerializedAtEnd: function insertSerializedAtEnd(value, nextChips, options) {
|
|
709
|
+
var el = editorRef.current;
|
|
710
|
+
if (!el || !value) return;
|
|
711
|
+
ensureLineRoots(el);
|
|
712
|
+
var range = resolveEndRange(el);
|
|
713
|
+
var shouldFocus = (options === null || options === void 0 ? void 0 : options.focus) !== false;
|
|
714
|
+
/* 拿不到 Selection 时仍要把内容插进去,只是不主动落 caret。 */
|
|
715
|
+
var sel = shouldFocus ? window.getSelection() : null;
|
|
716
|
+
if (sel) {
|
|
717
|
+
sel.removeAllRanges();
|
|
718
|
+
sel.addRange(range);
|
|
719
|
+
}
|
|
720
|
+
insertSerializedAtRange(value, nextChips, range, el, {
|
|
721
|
+
focus: shouldFocus,
|
|
722
|
+
caret: !!sel
|
|
723
|
+
});
|
|
724
|
+
if (!shouldFocus) return;
|
|
725
|
+
placeCaretAtEnd(el);
|
|
726
|
+
if (sel && sel.rangeCount > 0) {
|
|
727
|
+
lastEditorRangeRef.current = sel.getRangeAt(0).cloneRange();
|
|
728
|
+
}
|
|
729
|
+
requestScrollCaretIntoView();
|
|
730
|
+
},
|
|
731
|
+
getValue: function getValue() {
|
|
732
|
+
var el = editorRef.current;
|
|
733
|
+
return el ? getPlainText(el) : '';
|
|
734
|
+
},
|
|
539
735
|
cancelTrigger: function cancelTrigger() {
|
|
540
736
|
if (!triggerRef.current) return;
|
|
541
737
|
triggerRef.current = null;
|
|
@@ -558,7 +754,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
558
754
|
return editorRef.current;
|
|
559
755
|
}
|
|
560
756
|
};
|
|
561
|
-
}, [onChange, _insertChip, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
|
|
757
|
+
}, [onChange, _insertChip, insertSerializedAtRange, requestScrollCaretIntoView, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
|
|
562
758
|
|
|
563
759
|
/* ─────────────────────────────────────
|
|
564
760
|
* 监听 selection 变化:方向键 / 鼠标点击移位都要重算 trigger
|
|
@@ -931,6 +1127,17 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
931
1127
|
if (e.button !== 0 || disabled) return;
|
|
932
1128
|
// shift+click 是扩选,不劫持
|
|
933
1129
|
if (e.shiftKey) return;
|
|
1130
|
+
|
|
1131
|
+
/* 「TAB 采纳」小块:点它等价于按 Tab(同一条采纳链路),且不要求点击前
|
|
1132
|
+
* caret 就停在 hint 之前。在 mousedown 阶段接管 + preventDefault,
|
|
1133
|
+
* 原生 caret 一帧都不会落到 hint 之后。 */
|
|
1134
|
+
if (e.target instanceof Element && e.target.closest(COMPOSER_HINT_ACCEPT_SELECTOR)) {
|
|
1135
|
+
e.preventDefault();
|
|
1136
|
+
acceptHintAsText({
|
|
1137
|
+
force: true
|
|
1138
|
+
});
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
934
1141
|
var el = editorRef.current;
|
|
935
1142
|
var hint = hintNodeRef.current;
|
|
936
1143
|
if (!el || !hint || !hint.isConnected) return;
|
|
@@ -961,7 +1168,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
961
1168
|
});
|
|
962
1169
|
placeCaretBeforeHint();
|
|
963
1170
|
requestScrollCaretIntoView();
|
|
964
|
-
}, [disabled, handlePointerInput, placeCaretBeforeHint, requestScrollCaretIntoView]);
|
|
1171
|
+
}, [acceptHintAsText, disabled, handlePointerInput, placeCaretBeforeHint, requestScrollCaretIntoView]);
|
|
965
1172
|
var writeClipboardPayload = useCallback(function (e, payload) {
|
|
966
1173
|
if (!payload) return false;
|
|
967
1174
|
e.clipboardData.setData(COMPOSER_CLIPBOARD_MIME, JSON.stringify(payload));
|
|
@@ -35,9 +35,13 @@ export interface UseChipManagerReturn {
|
|
|
35
35
|
* `focus: false` 时插入后不把焦点交给编辑器。
|
|
36
36
|
* 首屏还原 defaultValue / defaultChips 必须传 false:默认 focus() 会把
|
|
37
37
|
* 页面滚动到编辑器(浏览器「聚焦元素进入视口」),导致进页面就跳位置。
|
|
38
|
+
*
|
|
39
|
+
* `caret: false` 时不主动改写 selection(后台预填用,避免抢走用户
|
|
40
|
+
* 在别处的光标)。单行路径由调用方传自己的 range,不受影响。
|
|
38
41
|
*/
|
|
39
42
|
options?: {
|
|
40
43
|
focus?: boolean;
|
|
44
|
+
caret?: boolean;
|
|
41
45
|
}) => void;
|
|
42
46
|
/** 移除已从 DOM 摘下的 chip 状态(cut / 选区删除后)。 */
|
|
43
47
|
syncChipsAfterDomMutation: (editor: HTMLElement) => void;
|
|
@@ -16,7 +16,7 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { useCallback, useRef, useState } from 'react';
|
|
19
|
-
import { createLineRoot, ensureLineRoots, forEachMarkerSegment, getCurrentLineRoot, LINE_ATTR_NAME, stripInvisibleChars
|
|
19
|
+
import { createLineRoot, ensureCaretAnchorAfter, ensureLineRoots, forEachMarkerSegment, getCurrentLineRoot, LINE_ATTR_NAME, stripInvisibleChars } from "../utils";
|
|
20
20
|
|
|
21
21
|
/** chip 在 React 状态中的形态:data + 对应的 DOM host。 */
|
|
22
22
|
|
|
@@ -27,24 +27,6 @@ function createChipHost(data) {
|
|
|
27
27
|
host.setAttribute('data-odn-composer-chip-host', '');
|
|
28
28
|
return host;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 在 host 后面垫一个「不进 value」的 ZWSP 文本锚点并返回它。
|
|
33
|
-
*
|
|
34
|
-
* chip 后面没有正文时(行尾 / 后面只剩派生 hint / 后面紧跟着另一个 chip),
|
|
35
|
-
* caret 若只靠「元素偏移」落位,部分浏览器会把它带到结构块之后;垫一个只含
|
|
36
|
-
* ZWSP 的文本节点,caret 始终有真实文本落点,而 `getPlainText` 会把整段剔掉,
|
|
37
|
-
* 因此不会进 value / segments / 剪贴板。已有锚点直接复用,避免越积越多。
|
|
38
|
-
*/
|
|
39
|
-
function ensureCaretAnchorAfter(host) {
|
|
40
|
-
var _tail$textContent, _tail$textContent2;
|
|
41
|
-
var parent = host.parentNode;
|
|
42
|
-
var tail = host.nextSibling;
|
|
43
|
-
var reusable = (tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE && ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '') !== '' && stripInvisibleChars((_tail$textContent2 = tail.textContent) !== null && _tail$textContent2 !== void 0 ? _tail$textContent2 : '') === '' ? tail : null;
|
|
44
|
-
var anchor = reusable !== null && reusable !== void 0 ? reusable : document.createTextNode(ZWSP);
|
|
45
|
-
if (!reusable) parent.insertBefore(anchor, tail);
|
|
46
|
-
return anchor;
|
|
47
|
-
}
|
|
48
30
|
function createCaretAnchorAtBoundary(parent, offset) {
|
|
49
31
|
var _parent$childNodes, _parent$childNodes$sa;
|
|
50
32
|
var len = parent.childNodes.length;
|
|
@@ -255,7 +237,7 @@ onAfterMutate) {
|
|
|
255
237
|
}
|
|
256
238
|
|
|
257
239
|
// caret 落「拼回点」——粘贴段末尾、原 caret 后内容的起点。
|
|
258
|
-
var sel = window.getSelection();
|
|
240
|
+
var sel = (options === null || options === void 0 ? void 0 : options.caret) === false ? null : window.getSelection();
|
|
259
241
|
if (sel && lastLine) {
|
|
260
242
|
var r = document.createRange();
|
|
261
243
|
r.setStart(lastLine, caretBoundaryChildCount);
|
|
@@ -271,7 +253,7 @@ onAfterMutate) {
|
|
|
271
253
|
_line.appendChild(lineFrags[_i]);
|
|
272
254
|
editor.appendChild(_line);
|
|
273
255
|
}
|
|
274
|
-
var _sel = window.getSelection();
|
|
256
|
+
var _sel = (options === null || options === void 0 ? void 0 : options.caret) === false ? null : window.getSelection();
|
|
275
257
|
var _lastLine = editor.querySelector(":scope > [".concat(LINE_ATTR_NAME, "]:last-of-type"));
|
|
276
258
|
if (_sel && _lastLine) {
|
|
277
259
|
var _r = document.createRange();
|
package/dist/composer/index.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import React from 'react';
|
|
|
2
2
|
import type { Attachment, ChipData, ComposerSegment, SendMeta, SkillItem } from '../_genui-types';
|
|
3
3
|
import type { IconName } from '../icon';
|
|
4
4
|
import { type SkillSlotProps } from '../skill-slot';
|
|
5
|
+
import { type ComposerInsertOptions, type ComposerInsertPart, type ComposerInsertResult } from './insert';
|
|
5
6
|
import type { ComposerParamPanelContext } from './param-panel';
|
|
6
7
|
import './style';
|
|
8
|
+
export type { ComposerInsertOptions, ComposerInsertPart, ComposerInsertResult, } from './insert';
|
|
7
9
|
export type { ComposerParamPanelContext } from './param-panel';
|
|
8
10
|
export type ComposerTool = 'attachment' | 'webSearch' | 'skill';
|
|
9
11
|
export type ComposerVariant = 'full' | 'lite';
|
|
@@ -48,6 +50,38 @@ export interface ComposerRef {
|
|
|
48
50
|
* 返回插入的 chip 实例 id,可用于后续 `updateChipData` 更新状态。
|
|
49
51
|
*/
|
|
50
52
|
insertSkill: (skill: SkillItem) => string;
|
|
53
|
+
/**
|
|
54
|
+
* 命令式:插入一段内容,支持文本 / skill / 自定义 chip 混合,且只触发一次 onChange。
|
|
55
|
+
*
|
|
56
|
+
* 插入位置由 `options.position` 显式声明(当前仅支持 `'end'`):忽略 caret,
|
|
57
|
+
* 始终落在已有内容末尾——适合 Welcome Card / 建议项 / 外部面板「把东西塞进
|
|
58
|
+
* 输入框」的场景。与 `insertSkill`(依赖当前 caret)互补。
|
|
59
|
+
*
|
|
60
|
+
* `parts` 也接受单个 part 或一个字符串(等价于单个 text part)。
|
|
61
|
+
*
|
|
62
|
+
* 约定:
|
|
63
|
+
* - **不自动补空格**:文本与 chip 的间隔由业务在 text part 里显式写。
|
|
64
|
+
* - `skill` 走与菜单插入相同的 chip 结构(含 `hint` / `initialState`);
|
|
65
|
+
* `initialState: 'pending'` 且提供 `renderParamPanel` 时自动打开参数面板
|
|
66
|
+
* (`focus: false` 的后台预填不打开)。
|
|
67
|
+
* - `chip` 是逃生舱:mention / 自定义 kind / 已存草稿都可以直接传。
|
|
68
|
+
* - chip id 与编辑器已有 chip 冲突时自动换新 id,以返回的 `chipIds` 为准。
|
|
69
|
+
* - 受控 `value` 场景请在 onChange 内回写,与手动输入一致(否则一帧后会被
|
|
70
|
+
* controlled sync 拉回旧值)。
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* composerRef.current?.insertContent(
|
|
75
|
+
* [
|
|
76
|
+
* { type: 'text', text: '请基于这份数据' },
|
|
77
|
+
* { type: 'skill', skill: reportSkill },
|
|
78
|
+
* { type: 'text', text: '生成复盘报告' },
|
|
79
|
+
* ],
|
|
80
|
+
* { position: 'end' },
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
insertContent: (parts: ComposerInsertPart | ComposerInsertPart[] | string, options: ComposerInsertOptions) => ComposerInsertResult;
|
|
51
85
|
/**
|
|
52
86
|
* 更新编辑器内指定 chip 的数据(partial merge)。
|
|
53
87
|
* 典型用法:`updateChipData(chipId, { state: 'pending' })` 将 chip 置为待补充态。
|
package/dist/composer/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
2
2
|
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
3
|
-
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
|
|
4
3
|
import _createForOfIteratorHelper from "@babel/runtime/helpers/esm/createForOfIteratorHelper";
|
|
5
4
|
import _regeneratorRuntime from "@babel/runtime/helpers/esm/regeneratorRuntime";
|
|
6
5
|
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";
|
|
6
|
+
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
|
|
7
7
|
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
|
|
8
8
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { SKILL_CATEGORY_ICON } from "../_genui-inline-ref";
|
|
@@ -18,23 +18,10 @@ import SkillSlot from "../skill-slot";
|
|
|
18
18
|
import Tooltip from "../tooltip";
|
|
19
19
|
import { COMPOSER_EDITOR_MAX_ROWS_PX, ComposerEditor } from "./editor";
|
|
20
20
|
import { chipHasEditableParams, isMentionChip } from "./inline-ref";
|
|
21
|
+
import { buildInsertPayload, createSkillChipData } from "./insert";
|
|
21
22
|
import { rawValueToSegments, segmentsHasContent } from "./segments";
|
|
22
23
|
import "./style";
|
|
23
24
|
import { COMPOSER_SKILL_MENU_DENSITY_CAP_PX, filterSkillsByQuery, resolveComposerSkillMenuMaxHeight, resolveComposerSkillMenuWidth, shouldOpenSlashTriggerMenu } from "./utils";
|
|
24
|
-
function createInvocationChipData(skill) {
|
|
25
|
-
return {
|
|
26
|
-
id: "skill-".concat(skill.id, "-").concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 6)),
|
|
27
|
-
label: skill.label,
|
|
28
|
-
icon: skill.icon,
|
|
29
|
-
kind: 'invocation',
|
|
30
|
-
state: skill.initialState,
|
|
31
|
-
refId: skill.id,
|
|
32
|
-
payload: {},
|
|
33
|
-
/* 提示文案随 chip 快照下来(而不是渲染时回查技能目录):
|
|
34
|
-
* 草稿用 defaultValue + defaultChips 还原时,目录可能已经变了。 */
|
|
35
|
-
hint: skill.hint
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
25
|
var TOOL_BTN_ACTIVE = 'color-mix(in srgb, var(--odn-color-brand-6) 10%, transparent)';
|
|
39
26
|
var DEFAULT_ACCEPT = 'image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.csv,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.py,.java,.go,.rs,.c,.cpp,.zip,.rar,.7z';
|
|
40
27
|
function formatSizeMb(bytes) {
|
|
@@ -322,12 +309,49 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
322
309
|
},
|
|
323
310
|
insertSkill: function insertSkill(skill) {
|
|
324
311
|
var _editorRef$current8, _editorRef$current9;
|
|
325
|
-
var data =
|
|
312
|
+
var data = createSkillChipData(skill);
|
|
326
313
|
(_editorRef$current8 = editorRef.current) === null || _editorRef$current8 === void 0 || _editorRef$current8.insertChip(data);
|
|
327
314
|
(_editorRef$current9 = editorRef.current) === null || _editorRef$current9 === void 0 || _editorRef$current9.focus();
|
|
328
315
|
if (skill.initialState === 'pending') openParamPanel(data.id);
|
|
329
316
|
return data.id;
|
|
330
317
|
},
|
|
318
|
+
insertContent: function insertContent(parts, options) {
|
|
319
|
+
var editor = editorRef.current;
|
|
320
|
+
if (!editor) return {
|
|
321
|
+
chipIds: [],
|
|
322
|
+
value: ''
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/* 类型层 position 目前只开 'end';这里兜住 JS / any 调用方,
|
|
326
|
+
* 避免未来扩 'caret' 前被静默当成末尾处理。 */
|
|
327
|
+
var position = options === null || options === void 0 ? void 0 : options.position;
|
|
328
|
+
if (process.env.NODE_ENV !== 'production' && position && position !== 'end') {
|
|
329
|
+
// eslint-disable-next-line no-console
|
|
330
|
+
console.warn("[Composer] insertContent \u5F53\u524D\u4EC5\u652F\u6301 position: \"end\"\uFF0C\u6536\u5230\u300C".concat(position, "\u300D\uFF0C\u5DF2\u6309\u672B\u5C3E\u63D2\u5165\u5904\u7406\u3002"));
|
|
331
|
+
}
|
|
332
|
+
var payload = buildInsertPayload(parts, editor.getChips());
|
|
333
|
+
if (!payload) {
|
|
334
|
+
return {
|
|
335
|
+
chipIds: [],
|
|
336
|
+
value: editor.getValue()
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
editor.insertSerializedAtEnd(payload.value, payload.chips, options);
|
|
340
|
+
|
|
341
|
+
/* 与 insertSkill 对齐:pending skill 插入后自动打开参数面板。
|
|
342
|
+
* 一次 insertContent 可能带多个 pending,只开最后一个(用户最后看到的那块)。
|
|
343
|
+
* `focus: false` 是后台预填语义,不主动弹面板。 */
|
|
344
|
+
var pendingChip = _toConsumableArray(payload.chips).reverse().find(function (chip) {
|
|
345
|
+
return chip.state === 'pending' && !isMentionChip(chip);
|
|
346
|
+
});
|
|
347
|
+
if (pendingChip && (options === null || options === void 0 ? void 0 : options.focus) !== false) {
|
|
348
|
+
openParamPanel(pendingChip.id);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
chipIds: payload.chipIds,
|
|
352
|
+
value: editor.getValue()
|
|
353
|
+
};
|
|
354
|
+
},
|
|
331
355
|
updateChipData: function updateChipData(id, patch) {
|
|
332
356
|
var _editorRef$current10;
|
|
333
357
|
return (_editorRef$current10 = editorRef.current) === null || _editorRef$current10 === void 0 ? void 0 : _editorRef$current10.updateChipData(id, patch);
|
|
@@ -644,7 +668,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
644
668
|
* source = 'trigger' → editor.replaceTriggerWithChip 替换 `/` + query 整段
|
|
645
669
|
* 任一入口选完后都关闭对应 menu。chip data 用 skill.id + 一次性实例 id。 */
|
|
646
670
|
var handleSelectSkill = useCallback(function (skill, source) {
|
|
647
|
-
var data =
|
|
671
|
+
var data = createSkillChipData(skill);
|
|
648
672
|
if (source === 'trigger') {
|
|
649
673
|
var _editorRef$current16;
|
|
650
674
|
(_editorRef$current16 = editorRef.current) === null || _editorRef$current16 === void 0 || _editorRef$current16.replaceTriggerWithChip(data);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer · 命令式插入内容(insertContent)
|
|
3
|
+
*
|
|
4
|
+
* 业务场景:Welcome Card / 建议项 / 外部面板希望在「已有输入内容的末尾」一次性
|
|
5
|
+
* 塞入文本、skill chip 等,而不是依赖当前 caret(用户可能停在任意位置)。
|
|
6
|
+
*
|
|
7
|
+
* 职责边界:
|
|
8
|
+
* - 本模块只把 parts 编成 `{ value, chips }`(含 marker 的 raw value + 新 chip
|
|
9
|
+
* 快照),真正的 DOM 插入由 editor 的 `insertSerializedAtEnd` →
|
|
10
|
+
* `insertSerializedAtRange` 完成——与粘贴 / 草稿还原共用同一条链路,
|
|
11
|
+
* 因此 onChange / 派生 hint / 高度 / trigger 只会重算一次。
|
|
12
|
+
* - **不自动补空格**:文本与 chip 之间的间隔由业务自己在 text part 里写,
|
|
13
|
+
* 避免 value / segments 里出现用户没输入过的字符(与插入 skill 的既有约定一致)。
|
|
14
|
+
*/
|
|
15
|
+
import type { ChipData, SkillItem } from '../_genui-types';
|
|
16
|
+
/**
|
|
17
|
+
* insertContent 支持的内容片段:
|
|
18
|
+
* - `text`:普通文本(支持 `\n`;`\r\n` 会归一成 `\n`)
|
|
19
|
+
* - `skill`:按技能目录的条目生成一个 invocation chip(含 hint / initialState)
|
|
20
|
+
* - `chip`:逃生舱,业务自带完整 chip 数据(mention / 自定义 kind / 已存草稿)
|
|
21
|
+
*/
|
|
22
|
+
export type ComposerInsertPart = {
|
|
23
|
+
type: 'text';
|
|
24
|
+
text: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'skill';
|
|
27
|
+
skill: SkillItem;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'chip';
|
|
30
|
+
chip: ChipData;
|
|
31
|
+
};
|
|
32
|
+
export interface ComposerInsertOptions {
|
|
33
|
+
/**
|
|
34
|
+
* 插入位置。
|
|
35
|
+
*
|
|
36
|
+
* 当前仅支持 `'end'`(已有内容末尾);`'caret'` 预留在同一入口,
|
|
37
|
+
* 未来扩展时**不会改变现有调用**,`'end'` 的语义也保持不变。
|
|
38
|
+
*/
|
|
39
|
+
position: 'end';
|
|
40
|
+
/**
|
|
41
|
+
* 插入后是否聚焦编辑器,并把 caret 落到新内容末尾(同时滚到可见区)。
|
|
42
|
+
* 传 `false` 时只改内容,不动焦点、不滚动(如后台预填草稿)。
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
focus?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface ComposerInsertResult {
|
|
48
|
+
/** 实际插入的 chip id,顺序与 parts 中 chip 出现顺序一致;id 冲突时已换新 */
|
|
49
|
+
chipIds: string[];
|
|
50
|
+
/** 插入后的 raw value(含 marker),等价于本次 onChange 第一参 */
|
|
51
|
+
value: string;
|
|
52
|
+
}
|
|
53
|
+
/** buildInsertPayload 的产物:可直接交给 editor 插入。 */
|
|
54
|
+
export interface ComposerInsertPayload {
|
|
55
|
+
/** 待插入的 raw value(唯一数据源:marker 与 text 交替) */
|
|
56
|
+
value: string;
|
|
57
|
+
/** 待插入 chip 的完整数据(只含本次新增,不含编辑器里已有的) */
|
|
58
|
+
chips: ChipData[];
|
|
59
|
+
/** 本次实际使用的 chip id(与 chips 一一对应) */
|
|
60
|
+
chipIds: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 按 SkillItem 生成 chip 实例数据。
|
|
64
|
+
*
|
|
65
|
+
* hint 随 chip 快照下来(而不是渲染时回查技能目录):草稿用
|
|
66
|
+
* `defaultValue + defaultChips` 还原时,目录可能已经变了。
|
|
67
|
+
*/
|
|
68
|
+
export declare function createSkillChipData(skill: SkillItem): ChipData;
|
|
69
|
+
/**
|
|
70
|
+
* 把 parts 编成一次插入所需的 raw value + chips。
|
|
71
|
+
*
|
|
72
|
+
* 返回 `null` 表示没有可插入内容(空数组 / 全空文本),调用方应直接 no-op,
|
|
73
|
+
* 不要触发 onChange。
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildInsertPayload(parts: ComposerInsertPart | ComposerInsertPart[] | string, existingChips?: readonly ChipData[]): ComposerInsertPayload | null;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
|
+
import _typeof from "@babel/runtime/helpers/esm/typeof";
|
|
3
|
+
import _createForOfIteratorHelper from "@babel/runtime/helpers/esm/createForOfIteratorHelper";
|
|
4
|
+
/**
|
|
5
|
+
* Composer · 命令式插入内容(insertContent)
|
|
6
|
+
*
|
|
7
|
+
* 业务场景:Welcome Card / 建议项 / 外部面板希望在「已有输入内容的末尾」一次性
|
|
8
|
+
* 塞入文本、skill chip 等,而不是依赖当前 caret(用户可能停在任意位置)。
|
|
9
|
+
*
|
|
10
|
+
* 职责边界:
|
|
11
|
+
* - 本模块只把 parts 编成 `{ value, chips }`(含 marker 的 raw value + 新 chip
|
|
12
|
+
* 快照),真正的 DOM 插入由 editor 的 `insertSerializedAtEnd` →
|
|
13
|
+
* `insertSerializedAtRange` 完成——与粘贴 / 草稿还原共用同一条链路,
|
|
14
|
+
* 因此 onChange / 派生 hint / 高度 / trigger 只会重算一次。
|
|
15
|
+
* - **不自动补空格**:文本与 chip 之间的间隔由业务自己在 text part 里写,
|
|
16
|
+
* 避免 value / segments 里出现用户没输入过的字符(与插入 skill 的既有约定一致)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createMarker, stripInvisibleChars } from "./utils";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* insertContent 支持的内容片段:
|
|
23
|
+
* - `text`:普通文本(支持 `\n`;`\r\n` 会归一成 `\n`)
|
|
24
|
+
* - `skill`:按技能目录的条目生成一个 invocation chip(含 hint / initialState)
|
|
25
|
+
* - `chip`:逃生舱,业务自带完整 chip 数据(mention / 自定义 kind / 已存草稿)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** buildInsertPayload 的产物:可直接交给 editor 插入。 */
|
|
29
|
+
|
|
30
|
+
var randomSuffix = function randomSuffix() {
|
|
31
|
+
return "".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 6));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 按 SkillItem 生成 chip 实例数据。
|
|
36
|
+
*
|
|
37
|
+
* hint 随 chip 快照下来(而不是渲染时回查技能目录):草稿用
|
|
38
|
+
* `defaultValue + defaultChips` 还原时,目录可能已经变了。
|
|
39
|
+
*/
|
|
40
|
+
export function createSkillChipData(skill) {
|
|
41
|
+
return {
|
|
42
|
+
id: "skill-".concat(skill.id, "-").concat(randomSuffix()),
|
|
43
|
+
label: skill.label,
|
|
44
|
+
icon: skill.icon,
|
|
45
|
+
kind: 'invocation',
|
|
46
|
+
state: skill.initialState,
|
|
47
|
+
refId: skill.id,
|
|
48
|
+
payload: {},
|
|
49
|
+
hint: skill.hint
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 文本预处理:
|
|
55
|
+
* - `\r\n` / `\r` → `\n`,交给行模型拆行
|
|
56
|
+
* - 剔除零宽 marker 字符,避免业务文案里混入 `\u200b\u2063` 后被解析成 chip
|
|
57
|
+
* - 不 trim:首尾空格是业务显式传入的内容,库不替它做决定
|
|
58
|
+
*/
|
|
59
|
+
function normalizeText(text) {
|
|
60
|
+
return stripInvisibleChars(text.replace(/\r\n?/g, '\n'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* chip id 必须唯一:marker、Portal key、removeChip / updateChipData 都靠它定位。
|
|
65
|
+
* 冲突时换一个新 id,并在开发期提示——返回值里的 `chipIds` 才是最终生效的 id。
|
|
66
|
+
*/
|
|
67
|
+
function resolveUniqueChipId(chip, usedIds) {
|
|
68
|
+
if (chip.id && !usedIds.has(chip.id)) return chip.id;
|
|
69
|
+
var nextId = "".concat(chip.id || 'chip', "-").concat(randomSuffix());
|
|
70
|
+
while (usedIds.has(nextId)) nextId = "chip-".concat(randomSuffix());
|
|
71
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.warn("[Composer] insertContent \u6536\u5230\u91CD\u590D\u6216\u4E3A\u7A7A\u7684 chip id\u300C".concat(chip.id, "\u300D\uFF0C") + "\u5DF2\u6539\u7528\u300C".concat(nextId, "\u300D\u3002\u8BF7\u4EE5\u8FD4\u56DE\u7684 chipIds \u4E3A\u51C6\u3002"));
|
|
74
|
+
}
|
|
75
|
+
return nextId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 把 parts 编成一次插入所需的 raw value + chips。
|
|
80
|
+
*
|
|
81
|
+
* 返回 `null` 表示没有可插入内容(空数组 / 全空文本),调用方应直接 no-op,
|
|
82
|
+
* 不要触发 onChange。
|
|
83
|
+
*/
|
|
84
|
+
export function buildInsertPayload(parts) {
|
|
85
|
+
var existingChips = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
86
|
+
var list = typeof parts === 'string' ? [{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: parts
|
|
89
|
+
}] : Array.isArray(parts) ? parts : [parts];
|
|
90
|
+
var usedIds = new Set(existingChips.map(function (chip) {
|
|
91
|
+
return chip.id;
|
|
92
|
+
}));
|
|
93
|
+
var chips = [];
|
|
94
|
+
var chipIds = [];
|
|
95
|
+
var value = '';
|
|
96
|
+
var _iterator = _createForOfIteratorHelper(list),
|
|
97
|
+
_step;
|
|
98
|
+
try {
|
|
99
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
100
|
+
var part = _step.value;
|
|
101
|
+
if (!part || _typeof(part) !== 'object') continue;
|
|
102
|
+
if (part.type === 'text') {
|
|
103
|
+
if (typeof part.text === 'string') value += normalizeText(part.text);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
var chip = null;
|
|
107
|
+
if (part.type === 'skill') {
|
|
108
|
+
chip = part.skill ? createSkillChipData(part.skill) : null;
|
|
109
|
+
} else if (part.chip) {
|
|
110
|
+
chip = _objectSpread({}, part.chip);
|
|
111
|
+
}
|
|
112
|
+
if (!chip) continue;
|
|
113
|
+
chip.id = resolveUniqueChipId(chip, usedIds);
|
|
114
|
+
usedIds.add(chip.id);
|
|
115
|
+
chips.push(chip);
|
|
116
|
+
chipIds.push(chip.id);
|
|
117
|
+
value += createMarker(chip.id);
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
_iterator.e(err);
|
|
121
|
+
} finally {
|
|
122
|
+
_iterator.f();
|
|
123
|
+
}
|
|
124
|
+
if (!value) return null;
|
|
125
|
+
return {
|
|
126
|
+
value: value,
|
|
127
|
+
chips: chips,
|
|
128
|
+
chipIds: chipIds
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -309,6 +309,74 @@
|
|
|
309
309
|
white-space: pre-wrap;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/* ------------------------------------------------------------------
|
|
313
|
+
* hint 末尾的「TAB 采纳」小块(点击 = 按 Tab 采纳,见 editor.tsx)
|
|
314
|
+
*
|
|
315
|
+
* 与 hint 文本是同级节点,一起增删。它不是正文:contentEditable=false、
|
|
316
|
+
* getPlainText 整棵跳过,点它走 force 版 acceptHintAsText。
|
|
317
|
+
* 尺寸对齐 Figma 9857:75425(62 × 22 / 圆角 6 / 内层键帽 28 × 16):
|
|
318
|
+
* - 编辑器固定 font-size 14 / line-height 24,块高 22 + top 1 → 视觉居中
|
|
319
|
+
* 且不撑高行盒(改编辑器行高时要同步调这个 1px)
|
|
320
|
+
* - 底色 black-2(5%)、键帽 bg-elevated + black-5(12%) 1px 描边
|
|
321
|
+
* ----------------------------------------------------------------- */
|
|
322
|
+
[data-odn-composer] [data-odn-composer-hint-accept] {
|
|
323
|
+
display: inline-flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
justify-content: center;
|
|
326
|
+
gap: 3px;
|
|
327
|
+
box-sizing: border-box;
|
|
328
|
+
height: 22px;
|
|
329
|
+
margin: 1px 0 0 4px;
|
|
330
|
+
padding: 0 4px 0 3px;
|
|
331
|
+
border-radius: 6px;
|
|
332
|
+
background: var(--odn-color-black-2);
|
|
333
|
+
color: var(--odn-color-solid-black-9);
|
|
334
|
+
font-size: 12px;
|
|
335
|
+
line-height: 20px;
|
|
336
|
+
white-space: nowrap;
|
|
337
|
+
vertical-align: top;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
user-select: none;
|
|
340
|
+
pointer-events: auto;
|
|
341
|
+
transition: background 0.15s ease-out, color 0.15s ease-out;
|
|
342
|
+
/* 底部 5% 太淡,hover 抬到 12%(约 2.4 倍)并加深文字,让「可点」更明确;
|
|
343
|
+
* active 再深一档。 */
|
|
344
|
+
}
|
|
345
|
+
[data-odn-composer] [data-odn-composer-hint-accept]:hover {
|
|
346
|
+
background: var(--odn-color-black-5);
|
|
347
|
+
color: var(--odn-color-solid-black-10);
|
|
348
|
+
}
|
|
349
|
+
[data-odn-composer] [data-odn-composer-hint-accept]:active {
|
|
350
|
+
background: var(--odn-color-black-6);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
[data-odn-composer] [data-odn-composer-hint-accept-key] {
|
|
354
|
+
display: inline-flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: center;
|
|
357
|
+
box-sizing: border-box;
|
|
358
|
+
height: 16px;
|
|
359
|
+
padding: 0 4px;
|
|
360
|
+
border: 1px solid var(--odn-color-black-5);
|
|
361
|
+
border-radius: 4px;
|
|
362
|
+
/* 与 composer box 同为「浮在底层之上的面」,跟随主题变量而不是写死白色 */
|
|
363
|
+
background: var(--odn-color-bg-elevated);
|
|
364
|
+
font-size: 10px;
|
|
365
|
+
line-height: 10px;
|
|
366
|
+
letter-spacing: 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
[data-odn-composer] [data-odn-composer-hint-accept-label] {
|
|
370
|
+
font-size: 12px;
|
|
371
|
+
line-height: 20px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* 禁用态:hint 仍按内容结构展示,但小块不可点(与 editor 冻结一致) */
|
|
375
|
+
[data-odn-composer] [data-odn-composer-editor][data-odn-composer-editor-disabled] [data-odn-composer-hint-accept] {
|
|
376
|
+
cursor: not-allowed;
|
|
377
|
+
pointer-events: none;
|
|
378
|
+
}
|
|
379
|
+
|
|
312
380
|
/* ------------------------------------------------------------------
|
|
313
381
|
* Block 行模型(caret-rewrite step 1/5)
|
|
314
382
|
* editor 直接子节点都是 [data-composer-line] div,每行渲染为独立 block。
|
package/dist/composer/utils.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export declare const LINE_ATTR_NAME = "data-composer-line";
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const COMPOSER_HINT_ATTR = "data-odn-composer-skill-hint";
|
|
24
24
|
export declare const COMPOSER_HINT_SELECTOR = "[data-odn-composer-skill-hint]";
|
|
25
|
+
/**
|
|
26
|
+
* 派生 hint 末尾的「TAB 采纳」小块(可点击采纳)。
|
|
27
|
+
*
|
|
28
|
+
* 与 hint 文本是**兄弟节点**(不是子节点):hint 自身的 `textContent` 必须
|
|
29
|
+
* 保持纯提示文案——采纳时直接把这段文字落成正文,且 `getPlainText` /
|
|
30
|
+
* 调试面板都按 textContent 读它。小块同样不是正文,遍历时整棵跳过。
|
|
31
|
+
*/
|
|
32
|
+
export declare const COMPOSER_HINT_ACCEPT_ATTR = "data-odn-composer-hint-accept";
|
|
33
|
+
export declare const COMPOSER_HINT_ACCEPT_SELECTOR = "[data-odn-composer-hint-accept]";
|
|
25
34
|
/** 是否是行容器。 */
|
|
26
35
|
export declare function isLineRoot(node: Node | null | undefined): node is HTMLDivElement;
|
|
27
36
|
/** 创建一个新的空行容器。 */
|
|
@@ -113,6 +122,15 @@ export declare function getPlainText(editor: HTMLElement, chipIdAttr?: string):
|
|
|
113
122
|
* 空格 / 全角空格也算"已输入",只要文本非空就该收起 placeholder。
|
|
114
123
|
*/
|
|
115
124
|
export declare function isEditorEmpty(editor: HTMLElement, chipIdAttr?: string): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* 在 host 后面垫一个「不进 value」的 ZWSP 文本锚点并返回它。
|
|
127
|
+
*
|
|
128
|
+
* chip 后面没有正文时(行尾 / 后面只剩派生 hint / 后面紧跟着另一个 chip),
|
|
129
|
+
* caret 若只靠「元素偏移」落位,部分浏览器会把它带到结构块之后;垫一个只含
|
|
130
|
+
* ZWSP 的文本节点,caret 始终有真实文本落点,而 `getPlainText` 会把整段剔掉,
|
|
131
|
+
* 因此不进 value / segments / 剪贴板。已有锚点直接复用,避免越积越多。
|
|
132
|
+
*/
|
|
133
|
+
export declare function ensureCaretAnchorAfter(host: Node): Text;
|
|
116
134
|
/**
|
|
117
135
|
* 清理 contenteditable 删字后的幽灵节点:空 text、仅 chip 时的占位 <br>。
|
|
118
136
|
* 避免「文字 + invocation」删光文字后出现多余换行,以及 caret 卡在 chip 后误删 invocation。
|
package/dist/composer/utils.js
CHANGED
|
@@ -39,6 +39,16 @@ export var LINE_ATTR_NAME = 'data-composer-line';
|
|
|
39
39
|
export var COMPOSER_HINT_ATTR = 'data-odn-composer-skill-hint';
|
|
40
40
|
export var COMPOSER_HINT_SELECTOR = "[".concat(COMPOSER_HINT_ATTR, "]");
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 派生 hint 末尾的「TAB 采纳」小块(可点击采纳)。
|
|
44
|
+
*
|
|
45
|
+
* 与 hint 文本是**兄弟节点**(不是子节点):hint 自身的 `textContent` 必须
|
|
46
|
+
* 保持纯提示文案——采纳时直接把这段文字落成正文,且 `getPlainText` /
|
|
47
|
+
* 调试面板都按 textContent 读它。小块同样不是正文,遍历时整棵跳过。
|
|
48
|
+
*/
|
|
49
|
+
export var COMPOSER_HINT_ACCEPT_ATTR = 'data-odn-composer-hint-accept';
|
|
50
|
+
export var COMPOSER_HINT_ACCEPT_SELECTOR = "[".concat(COMPOSER_HINT_ACCEPT_ATTR, "]");
|
|
51
|
+
|
|
42
52
|
/** 是否是行容器。 */
|
|
43
53
|
export function isLineRoot(node) {
|
|
44
54
|
var _hasAttribute, _ref;
|
|
@@ -292,8 +302,8 @@ export function getPlainText(editor) {
|
|
|
292
302
|
var out = '';
|
|
293
303
|
var visit = function visit(node) {
|
|
294
304
|
if (node instanceof HTMLElement) {
|
|
295
|
-
// 派生 hint
|
|
296
|
-
if (node.hasAttribute(COMPOSER_HINT_ATTR)) return;
|
|
305
|
+
// 派生 hint(含末尾的「TAB 采纳」小块)整棵子树都不是正文,直接跳过
|
|
306
|
+
if (node.hasAttribute(COMPOSER_HINT_ATTR) || node.hasAttribute(COMPOSER_HINT_ACCEPT_ATTR)) return;
|
|
297
307
|
var id = node.dataset[chipIdAttr];
|
|
298
308
|
if (id) {
|
|
299
309
|
out += createMarker(id);
|
|
@@ -368,6 +378,24 @@ export function isEditorEmpty(editor) {
|
|
|
368
378
|
* chip 前的 leading <br> / 空 text node。
|
|
369
379
|
* ──────────────────────────────────────────────────────────────────── */
|
|
370
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 在 host 后面垫一个「不进 value」的 ZWSP 文本锚点并返回它。
|
|
383
|
+
*
|
|
384
|
+
* chip 后面没有正文时(行尾 / 后面只剩派生 hint / 后面紧跟着另一个 chip),
|
|
385
|
+
* caret 若只靠「元素偏移」落位,部分浏览器会把它带到结构块之后;垫一个只含
|
|
386
|
+
* ZWSP 的文本节点,caret 始终有真实文本落点,而 `getPlainText` 会把整段剔掉,
|
|
387
|
+
* 因此不进 value / segments / 剪贴板。已有锚点直接复用,避免越积越多。
|
|
388
|
+
*/
|
|
389
|
+
export function ensureCaretAnchorAfter(host) {
|
|
390
|
+
var _tail$textContent, _tail$textContent2;
|
|
391
|
+
var parent = host.parentNode;
|
|
392
|
+
var tail = host.nextSibling;
|
|
393
|
+
var reusable = (tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE && ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '') !== '' && stripInvisibleChars((_tail$textContent2 = tail.textContent) !== null && _tail$textContent2 !== void 0 ? _tail$textContent2 : '') === '' ? tail : null;
|
|
394
|
+
var anchor = reusable !== null && reusable !== void 0 ? reusable : document.createTextNode(ZWSP);
|
|
395
|
+
if (!reusable) parent.insertBefore(anchor, tail);
|
|
396
|
+
return anchor;
|
|
397
|
+
}
|
|
398
|
+
|
|
371
399
|
/**
|
|
372
400
|
* 清理 contenteditable 删字后的幽灵节点:空 text、仅 chip 时的占位 <br>。
|
|
373
401
|
* 避免「文字 + invocation」删光文字后出现多余换行,以及 caret 卡在 chip 后误删 invocation。
|
package/dist/index.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export { CHAT_LIST_PINNED_ID, CHAT_LIST_UNGROUPED_ID, default as ChatList, match
|
|
|
63
63
|
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';
|
|
64
64
|
export { default as CodeBlock, type CodeBlockProps } from './code-block';
|
|
65
65
|
export { default as Composer, type ComposerProps, type ComposerRef, type ComposerSkillSlotProps, type ComposerTool, type ComposerToolConfig, } from './composer';
|
|
66
|
+
export type { ComposerInsertOptions, ComposerInsertPart, ComposerInsertResult, } from './composer/insert';
|
|
66
67
|
export { type ComposerParamPanelContext } from './composer/param-panel';
|
|
67
68
|
export { default as ErrorBlock, type ErrorBlockProps } from './error-block';
|
|
68
69
|
export { default as Fab, type FabProps } from './fab';
|
package/dist/skill-slot/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import Icon from "../icon";
|
|
|
7
7
|
import Input from "../input";
|
|
8
8
|
import Popover from "../popover";
|
|
9
9
|
import { highlightQueryText } from "../stream-text/highlight-query-text";
|
|
10
|
+
import Tooltip from "../tooltip";
|
|
10
11
|
import "./style";
|
|
11
12
|
function skillMatchesQuery(skill, query) {
|
|
12
13
|
var _skill$description;
|
|
@@ -367,8 +368,15 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
|
|
|
367
368
|
};
|
|
368
369
|
var renderBarItem = function renderBarItem(skill) {
|
|
369
370
|
var disabled = !!skill.disabled;
|
|
370
|
-
|
|
371
|
+
|
|
372
|
+
/* bar 无行内描述:整条 bar 由外层 Tooltip.Group 共用一层浮层(暗色,见 style)。
|
|
373
|
+
* 这里只把「本项的描述」交给 Group,hover 绑定由 Group 自己挂到 button 上。 */
|
|
374
|
+
return /*#__PURE__*/React.createElement(Tooltip, {
|
|
371
375
|
key: skill.id,
|
|
376
|
+
popup: skill.description ? /*#__PURE__*/React.createElement("span", {
|
|
377
|
+
"data-odn-skill-slot-desc-full": true
|
|
378
|
+
}, skill.description) : null
|
|
379
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
372
380
|
type: "button",
|
|
373
381
|
"data-odn-skill-slot-item": true,
|
|
374
382
|
"data-odn-skill-slot-item-disabled": disabled || undefined,
|
|
@@ -379,21 +387,8 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
|
|
|
379
387
|
}
|
|
380
388
|
}, /*#__PURE__*/React.createElement(Icon, {
|
|
381
389
|
name: skill.icon,
|
|
382
|
-
size:
|
|
383
|
-
}), skill.label);
|
|
384
|
-
|
|
385
|
-
/* bar 无行内描述:整颗 chip hover 0.5s 后向上 Popover 出全文 */
|
|
386
|
-
if (skill.description) {
|
|
387
|
-
return /*#__PURE__*/React.createElement(Popover, {
|
|
388
|
-
key: skill.id,
|
|
389
|
-
placement: "top",
|
|
390
|
-
mouseEnterDelay: 0.5,
|
|
391
|
-
popup: /*#__PURE__*/React.createElement("span", {
|
|
392
|
-
"data-odn-skill-slot-desc-full": true
|
|
393
|
-
}, skill.description)
|
|
394
|
-
}, barButton);
|
|
395
|
-
}
|
|
396
|
-
return barButton;
|
|
390
|
+
size: 16
|
|
391
|
+
}), skill.label));
|
|
397
392
|
};
|
|
398
393
|
var listBody = skills.map(function (skill, index) {
|
|
399
394
|
if (variant === 'card') return renderCardItem(skill);
|
|
@@ -461,10 +456,27 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
|
|
|
461
456
|
} : {}), variant !== 'menu' && itemWidth !== undefined && itemWidth !== null ? {
|
|
462
457
|
'--odn-skill-slot-item-width': "".concat(itemWidth, "px")
|
|
463
458
|
} : {}), style);
|
|
464
|
-
|
|
459
|
+
|
|
460
|
+
/* bar:整条 bar 是一组 Tooltip(与 preview-panel 右上角按钮同款手感)——
|
|
461
|
+
* 共用一层浮层,已展开时在块之间横移是 0 延迟换文案 + 浮层跟着滑,
|
|
462
|
+
* 移出整组 200ms 后才收起,不会一块一块地闪断。
|
|
463
|
+
* 容器本身就是 Tooltip.Group 的 Tag,data-* / className / style 一并落上去。 */
|
|
464
|
+
if (variant === 'bar') {
|
|
465
|
+
return /*#__PURE__*/React.createElement(Tooltip.Group, {
|
|
466
|
+
as: "div",
|
|
467
|
+
"data-odn-skill-slot": true,
|
|
468
|
+
"data-odn-skill-slot-variant": "bar",
|
|
469
|
+
className: className,
|
|
470
|
+
style: rootStyle,
|
|
471
|
+
placement: "top",
|
|
472
|
+
mouseEnterDelay: 0.1,
|
|
473
|
+
popupClassName: "odn-skill-slot-bar-popup"
|
|
474
|
+
}, listBody);
|
|
475
|
+
}
|
|
476
|
+
if (variant === 'card') {
|
|
465
477
|
return /*#__PURE__*/React.createElement("div", {
|
|
466
478
|
"data-odn-skill-slot": true,
|
|
467
|
-
"data-odn-skill-slot-variant":
|
|
479
|
+
"data-odn-skill-slot-variant": "card",
|
|
468
480
|
className: className,
|
|
469
481
|
style: rootStyle
|
|
470
482
|
}, listBody);
|
|
@@ -237,39 +237,71 @@
|
|
|
237
237
|
margin: 0;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
/* ---- variant: bar ----
|
|
240
|
+
/* ---- variant: bar ----
|
|
241
|
+
* 白底胶囊 chip:999 圆角,14/22 文案 + 16 icon,块间距 12。
|
|
242
|
+
* 设计稿:默认态**无描边**(描边在稿里是隐藏的),hover 才浮出黑-3 细描边,
|
|
243
|
+
* 底色/文字不变——所以预置 1px 透明描边占位,避免 hover 时宽度跳 2px。 */
|
|
241
244
|
[data-odn-skill-slot][data-odn-skill-slot-variant=bar] {
|
|
242
245
|
display: flex;
|
|
243
246
|
flex-wrap: wrap;
|
|
244
|
-
gap:
|
|
247
|
+
gap: 12px;
|
|
245
248
|
}
|
|
246
249
|
|
|
247
250
|
[data-odn-skill-slot][data-odn-skill-slot-variant=bar] [data-odn-skill-slot-item] {
|
|
248
251
|
display: inline-flex;
|
|
249
252
|
align-items: center;
|
|
250
|
-
gap:
|
|
251
|
-
height:
|
|
253
|
+
gap: 4px;
|
|
254
|
+
height: 36px;
|
|
252
255
|
padding: 0 12px;
|
|
253
|
-
border-radius:
|
|
254
|
-
border: 1px solid
|
|
255
|
-
background: var(--odn-color-
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
border-radius: 999px;
|
|
257
|
+
border: 1px solid transparent;
|
|
258
|
+
background: var(--odn-color-white);
|
|
259
|
+
box-sizing: border-box;
|
|
260
|
+
/* 不参与压缩:容器放不下就整块换行。否则窄容器里 flex 会把块挤到
|
|
261
|
+
* 比文案还窄,14/22 的文案折成两行、溢出固定 36px 的胶囊。 */
|
|
262
|
+
flex: 0 0 auto;
|
|
263
|
+
white-space: nowrap;
|
|
264
|
+
font-size: 14px;
|
|
265
|
+
font-weight: 400;
|
|
266
|
+
line-height: 22px;
|
|
267
|
+
color: var(--odn-color-black-12);
|
|
258
268
|
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
|
259
269
|
}
|
|
260
270
|
[data-odn-skill-slot][data-odn-skill-slot-variant=bar] [data-odn-skill-slot-item]:not([data-odn-skill-slot-item-disabled]):hover {
|
|
261
|
-
border-color: var(--odn-color-black-
|
|
262
|
-
background: var(--odn-color-black-3);
|
|
263
|
-
color: var(--odn-color-black-12);
|
|
271
|
+
border-color: var(--odn-color-black-3);
|
|
264
272
|
}
|
|
265
273
|
[data-odn-skill-slot][data-odn-skill-slot-variant=bar] [data-odn-skill-slot-item]:not([data-odn-skill-slot-item-disabled]):active {
|
|
266
|
-
|
|
274
|
+
border-color: var(--odn-color-black-4);
|
|
267
275
|
}
|
|
268
276
|
[data-odn-skill-slot][data-odn-skill-slot-variant=bar] [data-odn-skill-slot-item]:not([data-odn-skill-slot-item-disabled]):focus-visible {
|
|
269
277
|
outline: none;
|
|
270
278
|
border-color: var(--odn-color-brand-6);
|
|
271
|
-
|
|
272
|
-
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* bar chip hover 的全文提示:暗色浮层(设计稿「文字提示」:向上、多行)。
|
|
282
|
+
* 整条 bar 走 Tooltip.Group,浮层是 tooltip;popupClassName 落在
|
|
283
|
+
* [data-odn-popup-content] 上,用 [data-type] 兜住 pop-base 的
|
|
284
|
+
* popover 白底 / tooltip #404040 两套默认值。 */
|
|
285
|
+
[data-odn-popup][data-type] [data-odn-popup-content].odn-skill-slot-bar-popup {
|
|
286
|
+
max-width: 400px;
|
|
287
|
+
padding: 4px 8px;
|
|
288
|
+
border-radius: 4px;
|
|
289
|
+
background-color: var(--odn-color-solid-black-11);
|
|
290
|
+
color: var(--odn-color-white);
|
|
291
|
+
font-size: 14px;
|
|
292
|
+
font-weight: 400;
|
|
293
|
+
line-height: 22px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
[data-odn-popup][data-type] [data-odn-popup-content].odn-skill-slot-bar-popup [data-odn-popup-arrow-fill] {
|
|
297
|
+
fill: var(--odn-color-solid-black-11);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* 浮层纯视觉遮挡,不接收鼠标:pointer-events 可继承,给外层一次即可。
|
|
301
|
+
* 多行 bar 里浮层会压在上一行的块上,若吃掉事件,块↔块之间就换不过去,
|
|
302
|
+
* 也会挡住下层页面/输入框。 */
|
|
303
|
+
[data-odn-popup]:has(> [data-odn-popup-content].odn-skill-slot-bar-popup) {
|
|
304
|
+
pointer-events: none;
|
|
273
305
|
}
|
|
274
306
|
|
|
275
307
|
/* ---- variant: card ---- */
|
|
@@ -366,4 +398,12 @@
|
|
|
366
398
|
color: var(--odn-color-black-9);
|
|
367
399
|
word-break: break-word;
|
|
368
400
|
white-space: pre-wrap;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* bar 的全文浮层里换成暗底白字 14/22(宽度由 .odn-skill-slot-bar-popup 的 400 兜住) */
|
|
404
|
+
.odn-skill-slot-bar-popup [data-odn-skill-slot-desc-full] {
|
|
405
|
+
max-width: 100%;
|
|
406
|
+
font-size: 14px;
|
|
407
|
+
line-height: 22px;
|
|
408
|
+
color: var(--odn-color-white);
|
|
369
409
|
}
|