one-design-next 0.0.82 → 0.0.84

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.
@@ -705,15 +705,26 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
705
705
  }, [filteredSkills, filteredSkillGroups, handleSelectSkill, triggerActiveIndex, triggerMenuVisible]);
706
706
 
707
707
  /* trigger 浮 anchor:跟随 caret 的 0×0 隐形元素,给 Popover 当 children。
708
- * Popover 需要 DOM children 当 reference,没有"虚拟 anchor" API,只能这么搞。 */
708
+ * Popover 需要 DOM children 当 reference,没有"虚拟 anchor" API,只能这么搞。
709
+ *
710
+ * 使用 position: absolute 而非 fixed——Drawer 等容器的 transform 会创建
711
+ * 新的 containing block,导致 fixed 定位失准。absolute 相对于最近 positioned
712
+ * 祖先([data-odn-composer] 有 position:relative),通过减去 offsetParent
713
+ * 的 viewport rect 得到正确的局部坐标。 */
709
714
  var triggerAnchorRef = useRef(null);
710
715
  useLayoutEffect(function () {
711
716
  var el = triggerAnchorRef.current;
712
717
  if (!el || !triggerInfo) return;
713
- el.style.left = "".concat(triggerInfo.rect.left, "px");
714
- /* 锚到 caret 行顶部,配合 placement=topLeft 让 menu 浮在输入内容上方,
715
- * 不遮挡正在输入的文字(空间不足时 Popover flip 会自动回落到下方)。 */
716
- el.style.top = "".concat(triggerInfo.rect.top, "px");
718
+ var offsetParent = el.offsetParent;
719
+ if (offsetParent) {
720
+ var parentRect = offsetParent.getBoundingClientRect();
721
+ el.style.left = "".concat(triggerInfo.rect.left - parentRect.left, "px");
722
+ el.style.top = "".concat(triggerInfo.rect.top - parentRect.top, "px");
723
+ } else {
724
+ // fallback:没有 offsetParent(比如 display:none),用 viewport 坐标
725
+ el.style.left = "".concat(triggerInfo.rect.left, "px");
726
+ el.style.top = "".concat(triggerInfo.rect.top, "px");
727
+ }
717
728
  }, [triggerInfo]);
718
729
 
719
730
  /* ---- Derived ---- */
@@ -894,7 +905,9 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
894
905
  },
895
906
  popupClassName: "odn-composer-skill-popup",
896
907
  popup: /*#__PURE__*/React.createElement(SkillSlot, {
908
+ key: toolMenuOpen ? 'open' : 'closed',
897
909
  variant: "menu",
910
+ searchable: true,
898
911
  skills: skillGroups ? undefined : skills,
899
912
  skillGroups: skillGroups,
900
913
  showGroupTitles: showGroupTitles,
@@ -1129,7 +1142,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1129
1142
  ref: triggerAnchorRef,
1130
1143
  "data-odn-composer-trigger-anchor": true,
1131
1144
  style: {
1132
- position: 'fixed',
1145
+ position: 'absolute',
1133
1146
  width: 0,
1134
1147
  height: 0,
1135
1148
  pointerEvents: 'none'
@@ -63,6 +63,11 @@ export interface SkillSlotProps {
63
63
  * 不传则保持默认布局宽度。
64
64
  */
65
65
  itemWidth?: number;
66
+ /**
67
+ * menu 变体:在列表顶部显示搜索框,按 label / description 即时过滤,命中字高亮。
68
+ * bar / card 忽略。本轮不做搜索框内的键盘导航。
69
+ */
70
+ searchable?: boolean;
66
71
  className?: string;
67
72
  style?: React.CSSProperties;
68
73
  }
@@ -4,8 +4,17 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
4
4
  import { Fragment, forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import HoverFill from "../hover-fill";
6
6
  import Icon from "../icon";
7
+ import Input from "../input";
7
8
  import Popover from "../popover";
9
+ import { highlightQueryText } from "../stream-text/highlight-query-text";
8
10
  import "./style";
11
+ function skillMatchesQuery(skill, query) {
12
+ var _skill$description;
13
+ var q = query.trim().toLowerCase();
14
+ if (!q) return true;
15
+ if (skill.label.toLowerCase().includes(q)) return true;
16
+ return ((_skill$description = skill.description) !== null && _skill$description !== void 0 ? _skill$description : '').toLowerCase().includes(q);
17
+ }
9
18
 
10
19
  /* 给 listbox / option 生成稳定且唯一的 id 前缀(aria-activedescendant 需要)。
11
20
  * 模块级自增 + useState 初始化,保证每个实例 mount 期间 id 稳定。 */
@@ -16,7 +25,8 @@ var skillSlotIdSeq = 0;
16
25
  * 始终挂载 Popover(用 visible 关掉未截断态),避免条件挂载导致 ref/测量丢帧。
17
26
  */
18
27
  function SkillDescPopover(_ref) {
19
- var description = _ref.description;
28
+ var description = _ref.description,
29
+ highlightQuery = _ref.highlightQuery;
20
30
  var ref = useRef(null);
21
31
  var _useState = useState(false),
22
32
  _useState2 = _slicedToArray(_useState, 2),
@@ -59,11 +69,11 @@ function SkillDescPopover(_ref) {
59
69
  visible: truncated ? undefined : false,
60
70
  popup: /*#__PURE__*/React.createElement("span", {
61
71
  "data-odn-skill-slot-desc-full": true
62
- }, description)
72
+ }, highlightQueryText(description, highlightQuery))
63
73
  }, /*#__PURE__*/React.createElement("span", {
64
74
  ref: ref,
65
75
  "data-odn-skill-slot-item-desc": true
66
- }, description));
76
+ }, highlightQueryText(description, highlightQuery)));
67
77
  }
68
78
 
69
79
  /** 命令式句柄:供"焦点不在 menu 上"的宿主(如 composer 的 contenteditable)转发键盘。 */
@@ -82,15 +92,53 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
82
92
  menuMaxHeight = _ref2.menuMaxHeight,
83
93
  menuWidth = _ref2.menuWidth,
84
94
  itemWidth = _ref2.itemWidth,
95
+ _ref2$searchable = _ref2.searchable,
96
+ searchable = _ref2$searchable === void 0 ? false : _ref2$searchable,
85
97
  className = _ref2.className,
86
98
  style = _ref2.style;
87
- /* 优先使用 skillGroups,否则回退到 skills;两者都未提供则为空数组。 */
88
- var skills = useMemo(function () {
99
+ var searchableMenu = variant === 'menu' && searchable;
100
+ var _useState3 = useState(''),
101
+ _useState4 = _slicedToArray(_useState3, 2),
102
+ searchInput = _useState4[0],
103
+ setSearchInput = _useState4[1];
104
+ var _useState5 = useState(''),
105
+ _useState6 = _slicedToArray(_useState5, 2),
106
+ searchQuery = _useState6[0],
107
+ setSearchQuery = _useState6[1];
108
+ var composingRef = useRef(false);
109
+
110
+ /* 目录全集:过滤前的列表。目录为空才整棵不渲染。 */
111
+ var catalogSkills = useMemo(function () {
89
112
  if (skillGroups) return skillGroups.flatMap(function (g) {
90
113
  return g.skills;
91
114
  });
92
115
  return skillsProp !== null && skillsProp !== void 0 ? skillsProp : [];
93
116
  }, [skillGroups, skillsProp]);
117
+ var visibleGroups = useMemo(function () {
118
+ if (!skillGroups) return undefined;
119
+ if (!searchableMenu) return skillGroups;
120
+ return skillGroups.map(function (g) {
121
+ return {
122
+ groupName: g.groupName,
123
+ skills: g.skills.filter(function (s) {
124
+ return skillMatchesQuery(s, searchQuery);
125
+ })
126
+ };
127
+ }).filter(function (g) {
128
+ return g.skills.length > 0;
129
+ });
130
+ }, [skillGroups, searchableMenu, searchQuery]);
131
+
132
+ /* 优先使用过滤后的 skillGroups,否则回退到 skills。 */
133
+ var skills = useMemo(function () {
134
+ if (visibleGroups) return visibleGroups.flatMap(function (g) {
135
+ return g.skills;
136
+ });
137
+ if (!searchableMenu) return catalogSkills;
138
+ return catalogSkills.filter(function (s) {
139
+ return skillMatchesQuery(s, searchQuery);
140
+ });
141
+ }, [visibleGroups, catalogSkills, searchableMenu, searchQuery]);
94
142
  var showGroupTitles = showGroupTitlesProp !== null && showGroupTitlesProp !== void 0 ? showGroupTitlesProp : variant === 'menu';
95
143
  var groupedWithTitles = !!(skillGroups && showGroupTitles);
96
144
  /* menu 变体:HoverFill 完全只管鼠标交互(命令式状态机,不变);键盘 ↑↓
@@ -110,15 +158,15 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
110
158
  * 维护高亮 index,监听 ↑↓ / Home / End 移动、Enter / Space 选中,
111
159
  * 并自动跳过 disabled 项、把高亮项滚动进视口。 */
112
160
 
113
- var _useState3 = useState(function () {
161
+ var _useState7 = useState(function () {
114
162
  return "odn-skill-slot-".concat(++skillSlotIdSeq);
115
163
  }),
116
- _useState4 = _slicedToArray(_useState3, 1),
117
- listboxId = _useState4[0];
118
- var _useState5 = useState(-1),
119
- _useState6 = _slicedToArray(_useState5, 2),
120
- internalActiveIndex = _useState6[0],
121
- setInternalActiveIndex = _useState6[1];
164
+ _useState8 = _slicedToArray(_useState7, 1),
165
+ listboxId = _useState8[0];
166
+ var _useState9 = useState(-1),
167
+ _useState10 = _slicedToArray(_useState9, 2),
168
+ internalActiveIndex = _useState10[0],
169
+ setInternalActiveIndex = _useState10[1];
122
170
  var isControlled = activeIndex !== undefined && activeIndex !== null;
123
171
  var currentIndex = isControlled ? activeIndex : internalActiveIndex;
124
172
  var itemRefs = useRef([]);
@@ -230,7 +278,7 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
230
278
  if (e.currentTarget.contains(e.relatedTarget)) return;
231
279
  setInternalActiveIndex(-1);
232
280
  };
233
- if (skills.length === 0) return null;
281
+ if (catalogSkills.length === 0) return null;
234
282
 
235
283
  /* menu variant 有 scroll-wrap 时,滚动仅包裹技能列表,footer 固定在底部不参与滚动。 */
236
284
  var hasScrollWrap = variant === 'menu' && menuMaxHeight !== undefined && menuMaxHeight !== null;
@@ -279,8 +327,9 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
279
327
  "data-odn-skill-slot-item-content": true
280
328
  }, /*#__PURE__*/React.createElement("span", {
281
329
  "data-odn-skill-slot-item-label": true
282
- }, skill.label), skill.description ? /*#__PURE__*/React.createElement(SkillDescPopover, {
283
- description: skill.description
330
+ }, highlightQueryText(skill.label, searchableMenu ? searchQuery : undefined)), skill.description ? /*#__PURE__*/React.createElement(SkillDescPopover, {
331
+ description: skill.description,
332
+ highlightQuery: searchableMenu ? searchQuery : undefined
284
333
  }) : null)));
285
334
  };
286
335
 
@@ -365,7 +414,7 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
365
414
  var renderGroups = function renderGroups() {
366
415
  var flatIndex = 0;
367
416
  if (variant === 'menu') {
368
- return /*#__PURE__*/React.createElement(React.Fragment, null, skillGroups.map(function (group, groupIdx) {
417
+ return /*#__PURE__*/React.createElement(React.Fragment, null, visibleGroups.map(function (group, groupIdx) {
369
418
  return /*#__PURE__*/React.createElement(Fragment, {
370
419
  key: group.groupName || "_group_".concat(groupIdx)
371
420
  }, groupIdx > 0 ? /*#__PURE__*/React.createElement("div", {
@@ -378,7 +427,7 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
378
427
  }));
379
428
  }));
380
429
  }
381
- return /*#__PURE__*/React.createElement(React.Fragment, null, skillGroups.map(function (group, groupIdx) {
430
+ return /*#__PURE__*/React.createElement(React.Fragment, null, visibleGroups.map(function (group, groupIdx) {
382
431
  return /*#__PURE__*/React.createElement("div", {
383
432
  key: group.groupName || "_group_".concat(groupIdx),
384
433
  "data-odn-skill-slot-group": true
@@ -403,8 +452,41 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
403
452
 
404
453
  /* 有 skillGroups:始终按组遍历(保序);无标题时仍走分组 DOM,仅不渲染 header,
405
454
  * bar/card 用 group-items 承接原有 flex/grid,避免拍平后丢组边界。 */
406
- var listBody = skillGroups ? renderGroups() : renderFlatItems();
455
+ var listBody = visibleGroups ? renderGroups() : renderFlatItems();
407
456
  var manageFooter = renderManageFooter();
457
+ var searchBar = searchableMenu ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
458
+ "data-odn-skill-slot-search": true,
459
+ onCompositionStart: function onCompositionStart() {
460
+ composingRef.current = true;
461
+ },
462
+ onCompositionEnd: function onCompositionEnd(e) {
463
+ composingRef.current = false;
464
+ var value = e.target instanceof HTMLInputElement ? e.target.value : searchInput;
465
+ setSearchInput(value);
466
+ setSearchQuery(value);
467
+ }
468
+ }, /*#__PURE__*/React.createElement(Input, {
469
+ light: true,
470
+ size: "small",
471
+ allowClear: true,
472
+ placeholder: "\u641C\u7D22\u6280\u80FD\u540D\u79F0\u6216\u63CF\u8FF0",
473
+ value: searchInput,
474
+ onChange: function onChange(e, value) {
475
+ setSearchInput(value);
476
+ var composing = composingRef.current || e.nativeEvent.isComposing;
477
+ if (composing) return;
478
+ setSearchQuery(value);
479
+ },
480
+ leftElement: /*#__PURE__*/React.createElement(Icon, {
481
+ name: "search",
482
+ size: 14
483
+ })
484
+ })), /*#__PURE__*/React.createElement("div", {
485
+ "data-odn-skill-slot-divider": true
486
+ })) : null;
487
+ var emptyState = searchableMenu && skills.length === 0 ? /*#__PURE__*/React.createElement("div", {
488
+ "data-odn-skill-slot-empty": true
489
+ }, "\u672A\u627E\u5230\u76F8\u5173\u6280\u80FD") : null;
408
490
 
409
491
  /* 共享的属性容器:menu variant 的 aria / keyboard / 交互属性 */
410
492
  var listAttrs = variant === 'menu' ? {
@@ -421,29 +503,33 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref2, ref) {
421
503
  } : {}), variant !== 'menu' && itemWidth !== undefined && itemWidth !== null ? {
422
504
  '--odn-skill-slot-item-width': "".concat(itemWidth, "px")
423
505
  } : {}), style);
424
- if (hasScrollWrap) {
425
- /* 滚动仅包裹技能列表,footer 固定在底部不参与滚动 */
426
- return /*#__PURE__*/React.createElement("div", _extends({
506
+ if (variant !== 'menu') {
507
+ return /*#__PURE__*/React.createElement("div", {
427
508
  "data-odn-skill-slot": true,
428
509
  "data-odn-skill-slot-variant": variant,
429
510
  "data-odn-skill-slot-grouped": groupedWithTitles || undefined,
430
511
  className: className,
431
512
  style: rootStyle
432
- }, listAttrs), /*#__PURE__*/React.createElement("div", {
433
- "data-odn-skill-slot-scroll-wrap": true,
434
- style: {
435
- maxHeight: menuMaxHeight,
436
- overflowY: 'auto'
437
- }
438
- }, listBody), manageFooter);
513
+ }, listBody);
439
514
  }
440
- return /*#__PURE__*/React.createElement("div", _extends({
515
+
516
+ /* listbox 挂在列表容器上,搜索框 / footer 不进 listbox。 */
517
+ var menuList = /*#__PURE__*/React.createElement("div", _extends({
518
+ "data-odn-skill-slot-list": true
519
+ }, listAttrs), skills.length > 0 ? listBody : emptyState);
520
+ return /*#__PURE__*/React.createElement("div", {
441
521
  "data-odn-skill-slot": true,
442
522
  "data-odn-skill-slot-variant": variant,
443
523
  "data-odn-skill-slot-grouped": groupedWithTitles || undefined,
444
524
  className: className,
445
525
  style: rootStyle
446
- }, listAttrs), listBody, manageFooter);
526
+ }, searchBar, hasScrollWrap ? /*#__PURE__*/React.createElement("div", {
527
+ "data-odn-skill-slot-scroll-wrap": true,
528
+ style: {
529
+ maxHeight: menuMaxHeight,
530
+ overflowY: 'auto'
531
+ }
532
+ }, menuList) : menuList, manageFooter);
447
533
  });
448
534
  SkillSlot.displayName = 'SkillSlot';
449
535
  export default SkillSlot;
@@ -41,6 +41,76 @@
41
41
  outline: none;
42
42
  }
43
43
 
44
+ [data-odn-skill-slot-list] {
45
+ display: flex;
46
+ flex-direction: column;
47
+ width: 100%;
48
+ }
49
+ [data-odn-skill-slot-list]:focus, [data-odn-skill-slot-list]:focus-visible {
50
+ outline: none;
51
+ }
52
+
53
+ /* 顶栏滤镜行:与底部「技能管理」同构(无描边、热区不拉通栏)。
54
+ * 左右 4px 与菜单上下 padding 对齐;选择器压过 Input 默认描边 / 聚焦蓝框。 */
55
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-search] {
56
+ flex-shrink: 0;
57
+ margin: 0 4px;
58
+ }
59
+
60
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-search] [data-odn-input] {
61
+ display: flex;
62
+ width: 100%;
63
+ min-width: 0;
64
+ box-sizing: border-box;
65
+ align-items: center;
66
+ gap: 8px;
67
+ padding: 8px 12px;
68
+ font-size: 13px;
69
+ line-height: 22px;
70
+ color: var(--odn-color-black-12);
71
+ border: none;
72
+ border-radius: 4px;
73
+ background: transparent;
74
+ box-shadow: none;
75
+ outline: none;
76
+ transition: background-color 0.15s;
77
+ }
78
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-search] [data-odn-input][data-odn-input-focused=true] {
79
+ border: none;
80
+ box-shadow: none;
81
+ outline: none;
82
+ background: transparent;
83
+ }
84
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-search] [data-odn-input]:hover {
85
+ background: rgba(33, 34, 38, 0.05);
86
+ }
87
+
88
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-search] [data-odn-input-left-element] {
89
+ color: var(--odn-color-black-9);
90
+ }
91
+
92
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-search-mark] {
93
+ background: transparent;
94
+ color: var(--odn-color-brand-6);
95
+ padding: 0;
96
+ margin: 0;
97
+ border: 0;
98
+ border-radius: 0;
99
+ font: inherit;
100
+ letter-spacing: inherit;
101
+ word-spacing: inherit;
102
+ line-height: inherit;
103
+ vertical-align: baseline;
104
+ }
105
+
106
+ [data-odn-skill-slot-empty] {
107
+ padding: 20px 12px;
108
+ font-size: 13px;
109
+ line-height: 20px;
110
+ color: var(--odn-color-black-8);
111
+ text-align: center;
112
+ }
113
+
44
114
  [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-item-wrap] {
45
115
  display: block;
46
116
  }
@@ -171,9 +241,11 @@
171
241
  display: flex;
172
242
  align-items: center;
173
243
  justify-content: space-between;
174
- width: 100%;
244
+ box-sizing: border-box;
245
+ margin: 0 4px;
175
246
  padding: 8px 12px;
176
247
  border: none;
248
+ border-radius: 4px;
177
249
  background: transparent;
178
250
  font-size: 13px;
179
251
  color: var(--odn-color-black-9);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "license": "MIT",
6
6
  "type": "module",