one-design-next 0.0.35 → 0.0.37

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.
@@ -143,6 +143,13 @@ export interface SkillItem {
143
143
  */
144
144
  disabled?: boolean;
145
145
  }
146
+ /** 技能分组,用于在 SkillSlot menu 变体中按组名分隔展示。 */
147
+ export interface SkillGroup {
148
+ /** 分组名称(可空,空时不渲染标题) */
149
+ groupName: string;
150
+ /** 组内技能列表 */
151
+ skills: SkillItem[];
152
+ }
146
153
  /**
147
154
  * Composer 发送时的有序内容 IR(与编辑器内出现顺序一致)。
148
155
  *
@@ -19,6 +19,8 @@
19
19
  * 发送时经 `rawValueToSegments` 转为 `ComposerSegment` 的 invocation / mention。
20
20
  */
21
21
 
22
+ /** 技能分组,用于在 SkillSlot menu 变体中按组名分隔展示。 */
23
+
22
24
  /**
23
25
  * Composer 发送时的有序内容 IR(与编辑器内出现顺序一致)。
24
26
  *
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import type { Attachment, ComposerSegment, SkillItem, SendMeta } from '../_genui-types';
2
+ import type { Attachment, ComposerSegment, SkillGroup, SkillItem, SendMeta } from '../_genui-types';
3
3
  import type { ChipData } from '../_genui-types';
4
4
  import type { ComposerParamPanelContext } from './param-panel';
5
5
  import './style';
@@ -104,6 +104,15 @@ export interface ComposerProps {
104
104
  toolsLabeled?: boolean;
105
105
  /** 技能列表;当 tools 包含 'skill' 时生效 */
106
106
  skills?: SkillItem[];
107
+ /**
108
+ * 分组技能列表(优先级高于 `skills`)。
109
+ * menu 变体下按分组名分隔展示,组间带标题。
110
+ */
111
+ skillGroups?: SkillGroup[];
112
+ /**
113
+ * 技能管理回调。提供后会在 skill 弹窗底部渲染「管理技能」按钮。
114
+ */
115
+ onManageSkills?: () => void;
107
116
  /**
108
117
  * skill 弹窗列表的最大高度(px)。超出此高度后出现滚动条。
109
118
  * 不传则不限高(适合 skill 较少的场景)。
@@ -95,6 +95,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
95
95
  _ref$toolsLabeled = _ref.toolsLabeled,
96
96
  toolsLabeled = _ref$toolsLabeled === void 0 ? true : _ref$toolsLabeled,
97
97
  skills = _ref.skills,
98
+ skillGroups = _ref.skillGroups,
99
+ onManageSkills = _ref.onManageSkills,
98
100
  skillMenuMaxHeight = _ref.skillMenuMaxHeight,
99
101
  skillMenuWidth = _ref.skillMenuWidth,
100
102
  _ref$submitType = _ref.submitType,
@@ -545,20 +547,41 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
545
547
  });
546
548
  }, [skills, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query]);
547
549
 
548
- /* trigger 浮层渲染条件:editor 处于 `/` 触发态 + 业务方传了 skills。
550
+ /* grouped 版本的 trigger query 筛选:按组分别过滤,保留非空组。 */
551
+ var filteredSkillGroups = useMemo(function () {
552
+ var _triggerInfo$query2;
553
+ if (!skillGroups) return undefined;
554
+ var q = triggerInfo === null || triggerInfo === void 0 || (_triggerInfo$query2 = triggerInfo.query) === null || _triggerInfo$query2 === void 0 ? void 0 : _triggerInfo$query2.trim().toLowerCase();
555
+ if (!q) return skillGroups;
556
+ return skillGroups.map(function (g) {
557
+ return {
558
+ groupName: g.groupName,
559
+ skills: g.skills.filter(function (s) {
560
+ return s.label.toLowerCase().includes(q);
561
+ })
562
+ };
563
+ }).filter(function (g) {
564
+ return g.skills.length > 0;
565
+ });
566
+ }, [skillGroups, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query]);
567
+
568
+ /* trigger 浮层渲染条件:editor 处于 `/` 触发态 + 业务方传了 skills 或 skillGroups。
549
569
  * `@` 触发本轮不接(mention 数据源待业务接入),onTriggerChange 拿到也不弹。 */
550
- var triggerMenuVisible = !!(triggerInfo && triggerInfo.trigger === '/' && skills && skills.length > 0);
570
+ var hasSkills = !!(skills && skills.length > 0) || !!(skillGroups && skillGroups.length > 0);
571
+ var triggerMenuVisible = !!(triggerInfo && triggerInfo.trigger === '/' && hasSkills);
551
572
 
552
573
  /* query 变化或浮层关闭 → 高亮项重置到「最上方的可用项」(跳过 disabled)。
553
574
  * 用 query 而不是 triggerInfo 整体当依赖:rect 变化(光标移位)不应该 reset。 */
554
575
  useEffect(function () {
555
- var _ref3;
556
- var list = (_ref3 = filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : skills) !== null && _ref3 !== void 0 ? _ref3 : [];
576
+ var _ref3, _ref4, _ref5;
577
+ var list = (_ref3 = (_ref4 = filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : skills) !== null && _ref4 !== void 0 ? _ref4 : (_ref5 = filteredSkillGroups !== null && filteredSkillGroups !== void 0 ? filteredSkillGroups : skillGroups) === null || _ref5 === void 0 ? void 0 : _ref5.flatMap(function (g) {
578
+ return g.skills;
579
+ })) !== null && _ref3 !== void 0 ? _ref3 : [];
557
580
  var first = list.findIndex(function (s) {
558
581
  return !s.disabled;
559
582
  });
560
583
  setTriggerActiveIndex(first === -1 ? 0 : first);
561
- }, [filteredSkills, skills, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query, triggerInfo == null]);
584
+ }, [filteredSkills, filteredSkillGroups, skills, skillGroups, triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.query, triggerInfo == null]);
562
585
 
563
586
  /* editor 在 trigger 激活时把 ↑↓Enter Esc Tab 丢过来。返回 true = editor 别管了。
564
587
  * - ↑↓ 改高亮 index(夹在 [0, max] 内)
@@ -582,9 +605,11 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
582
605
  return true;
583
606
  }
584
607
  if (e.key === 'Enter' && !e.shiftKey || e.key === 'Tab') {
585
- var _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
608
+ var _ref6, _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
586
609
  e.preventDefault();
587
- var _skill = (_triggerSkillSlotRef$3 = (_triggerSkillSlotRef$4 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$4 === void 0 ? void 0 : _triggerSkillSlotRef$4.getActiveSkill()) !== null && _triggerSkillSlotRef$3 !== void 0 ? _triggerSkillSlotRef$3 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex];
610
+ var _skill = (_ref6 = (_triggerSkillSlotRef$3 = (_triggerSkillSlotRef$4 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$4 === void 0 ? void 0 : _triggerSkillSlotRef$4.getActiveSkill()) !== null && _triggerSkillSlotRef$3 !== void 0 ? _triggerSkillSlotRef$3 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex]) !== null && _ref6 !== void 0 ? _ref6 : filteredSkillGroups === null || filteredSkillGroups === void 0 ? void 0 : filteredSkillGroups.flatMap(function (g) {
611
+ return g.skills;
612
+ })[triggerActiveIndex];
588
613
  if (_skill) handleSelectSkill(_skill, 'trigger');
589
614
  return true;
590
615
  }
@@ -596,7 +621,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
596
621
  return true;
597
622
  }
598
623
  return false;
599
- }, [filteredSkills, handleSelectSkill, triggerActiveIndex, triggerMenuVisible]);
624
+ }, [filteredSkills, filteredSkillGroups, handleSelectSkill, triggerActiveIndex, triggerMenuVisible]);
600
625
 
601
626
  /* trigger 浮 anchor:跟随 caret 的 0×0 隐形元素,给 Popover 当 children。
602
627
  * Popover 需要 DOM children 当 reference,没有"虚拟 anchor" API,只能这么搞。 */
@@ -688,7 +713,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
688
713
  }), effectiveLabeled && /*#__PURE__*/React.createElement("span", null, "\u8054\u7F51")));
689
714
  };
690
715
  var renderSkillBtn = function renderSkillBtn() {
691
- if (!skills || skills.length === 0) return null;
716
+ var hasAnySkills = skills && skills.length > 0 || skillGroups && skillGroups.some(function (g) {
717
+ return g.skills.length > 0;
718
+ });
719
+ if (!hasAnySkills) return null;
692
720
  return /*#__PURE__*/React.createElement(Popover, {
693
721
  key: "skill",
694
722
  trigger: "click",
@@ -703,14 +731,16 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
703
731
  popupClassName: "odn-composer-skill-popup",
704
732
  popup: /*#__PURE__*/React.createElement(SkillSlot, {
705
733
  variant: "menu",
706
- skills: skills,
734
+ skills: skillGroups ? undefined : skills,
735
+ skillGroups: skillGroups,
707
736
  menuMaxHeight: skillMenuMaxHeight,
708
737
  style: {
709
738
  width: skillMenuWidth !== null && skillMenuWidth !== void 0 ? skillMenuWidth : 260
710
739
  },
711
740
  onSelect: function onSelect(s) {
712
741
  return handleSelectSkill(s, 'btn');
713
- }
742
+ },
743
+ onManageSkills: onManageSkills
714
744
  })
715
745
  }, /*#__PURE__*/React.createElement(HoverFill, {
716
746
  "data-odn-composer-tool-btn-wrap": true,
@@ -731,7 +761,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
731
761
  "aria-disabled": disabled || undefined,
732
762
  title: effectiveLabeled ? undefined : '技能'
733
763
  }, /*#__PURE__*/React.createElement(Icon, {
734
- name: "puzzle",
764
+ name: "command",
735
765
  size: 16
736
766
  }), effectiveLabeled && /*#__PURE__*/React.createElement("span", null, "\u6280\u80FD"))));
737
767
  };
@@ -895,7 +925,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
895
925
 
896
926
  /* trigger 浮层:anchor 是 fixed 定位的 0×0 隐形 div,跟随 caret 处的 trigger rect。
897
927
  * 共用 SkillSlot 渲染,过滤后的列表为空时仍显示 menu(empty 视觉由 SkillSlot 自己处理)。 */
898
- var triggerMenu = skills && skills.length > 0 ? /*#__PURE__*/React.createElement(Popover, {
928
+ var triggerMenu = hasSkills ? /*#__PURE__*/React.createElement(Popover, {
899
929
  trigger: "click",
900
930
  arrowed: false,
901
931
  placement: "topLeft",
@@ -912,7 +942,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
912
942
  popup: /*#__PURE__*/React.createElement(SkillSlot, {
913
943
  ref: triggerSkillSlotRef,
914
944
  variant: "menu",
915
- skills: filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : [],
945
+ skills: skillGroups ? undefined : filteredSkills !== null && filteredSkills !== void 0 ? filteredSkills : [],
946
+ skillGroups: filteredSkillGroups,
916
947
  activeIndex: triggerActiveIndex,
917
948
  onActiveIndexChange: setTriggerActiveIndex,
918
949
  menuMaxHeight: skillMenuMaxHeight,
@@ -921,7 +952,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
921
952
  },
922
953
  onSelect: function onSelect(s) {
923
954
  return handleSelectSkill(s, 'trigger');
924
- }
955
+ },
956
+ onManageSkills: onManageSkills
925
957
  })
926
958
  }, /*#__PURE__*/React.createElement("div", {
927
959
  ref: triggerAnchorRef,
package/dist/index.d.ts CHANGED
@@ -76,6 +76,6 @@ export { default as StreamPulse, type StreamPulseProps } from './stream-pulse';
76
76
  export { default as Suggestions, type SuggestionsProps } from './suggestions';
77
77
  export { default as UserBubble, type UserBubbleProps } from './user-bubble';
78
78
  export { default as Welcome, type WelcomeProps } from './welcome';
79
- export type { ToolCall, ToolStatus, Source, Attachment, PreviewTarget, PreviewTab, SendMeta, ComposerSegment, ComposerTextSegment, ComposerInvocationSegment, ComposerMentionSegment, TextData, InvocationData, MentionData, InlineRefState, SkillItem, AgentEvent, Conversation, MessageSnapshot, ChatMessage } from './_genui-types';
79
+ export type { ToolCall, ToolStatus, Source, Attachment, PreviewTarget, PreviewTab, SendMeta, ComposerSegment, ComposerTextSegment, ComposerInvocationSegment, ComposerMentionSegment, TextData, InvocationData, MentionData, InlineRefState, SkillItem, SkillGroup, AgentEvent, Conversation, MessageSnapshot, ChatMessage } from './_genui-types';
80
80
  export { ComposerSegmentType } from './_genui-types';
81
81
  export { rawValueToSegments, segmentsToReadableText, segmentsHasContent, isTextSegment, isInvocationSegment, isMentionSegment, type SegmentsToReadableTextOptions, } from './composer/segments';
@@ -1,12 +1,25 @@
1
1
  /// <reference types="react" />
2
- import type { SkillItem } from '../_genui-types';
2
+ import type { SkillGroup, SkillItem } from '../_genui-types';
3
3
  import './style';
4
4
  export interface SkillSlotProps {
5
- skills: SkillItem[];
5
+ /**
6
+ * 技能列表(扁平结构)。
7
+ * 当 `skillGroups` 未提供时生效;提供 `skillGroups` 则忽略此项。
8
+ */
9
+ skills?: SkillItem[];
10
+ /**
11
+ * 分组技能列表。menu 变体下按组名分隔展示,组间带标题。
12
+ * 优先级高于 `skills`。
13
+ */
14
+ skillGroups?: SkillGroup[];
6
15
  variant?: 'menu' | 'bar' | 'card';
7
16
  /** 选中态(业务"已选"语义)。命中的 item 视觉上 active + disabled 互斥。 */
8
17
  value?: string | null;
9
18
  onSelect?: (skill: SkillItem) => void;
19
+ /**
20
+ * 技能管理回调。提供后 menu 变体会在列表底部渲染「管理技能」按钮。
21
+ */
22
+ onManageSkills?: () => void;
10
23
  /**
11
24
  * 受控的"键盘高亮"项 index(区别于 `value` 的"业务选中"语义)。仅 menu 变体响应。
12
25
  *
@@ -1,19 +1,17 @@
1
1
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
- var _excluded = ["width"];
3
2
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
4
3
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5
4
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
6
5
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
7
6
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
8
- function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
9
- function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
7
+ function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
10
8
  function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
11
9
  function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
12
10
  function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
13
11
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
14
12
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
15
13
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
16
- import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
14
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
17
15
  import HoverFill from "../hover-fill";
18
16
  import Icon from "../icon";
19
17
  import Tooltip from "../tooltip";
@@ -26,17 +24,26 @@ var skillSlotIdSeq = 0;
26
24
  /** 命令式句柄:供"焦点不在 menu 上"的宿主(如 composer 的 contenteditable)转发键盘。 */
27
25
 
28
26
  export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref, ref) {
29
- var skills = _ref.skills,
27
+ var skillsProp = _ref.skills,
28
+ skillGroups = _ref.skillGroups,
30
29
  _ref$variant = _ref.variant,
31
30
  variant = _ref$variant === void 0 ? 'bar' : _ref$variant,
32
31
  value = _ref.value,
33
32
  onSelect = _ref.onSelect,
33
+ onManageSkills = _ref.onManageSkills,
34
34
  activeIndex = _ref.activeIndex,
35
35
  onActiveIndexChange = _ref.onActiveIndexChange,
36
36
  menuMaxHeight = _ref.menuMaxHeight,
37
37
  itemWidth = _ref.itemWidth,
38
38
  className = _ref.className,
39
39
  style = _ref.style;
40
+ /* 优先使用 skillGroups,否则回退到 skills;两者都未提供则为空数组。 */
41
+ var skills = useMemo(function () {
42
+ if (skillGroups) return skillGroups.flatMap(function (g) {
43
+ return g.skills;
44
+ });
45
+ return skillsProp !== null && skillsProp !== void 0 ? skillsProp : [];
46
+ }, [skillGroups, skillsProp]);
40
47
  /* menu 变体:HoverFill 完全只管鼠标交互(命令式状态机,不变);键盘 ↑↓
41
48
  * 走独立的 CSS [data-odn-skill-slot-item-highlighted] 视觉层。
42
49
  *
@@ -176,99 +183,133 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref, ref) {
176
183
  };
177
184
  if (skills.length === 0) return null;
178
185
 
179
- /* menu variant 有 scroll-wrap 时,width 挂在外层 scroll-wrap 上;
180
- * 没有 scroll-wrap 时 width 留在 list div 上。
181
- * 这里把 style 拆成两部分:widthStyle 单独提取,listStyle 是其余属性。 */
186
+ /* menu variant 有 scroll-wrap 时,滚动仅包裹技能列表,footer 固定在底部不参与滚动。 */
182
187
  var hasScrollWrap = variant === 'menu' && menuMaxHeight != null;
183
- var _ref2 = style !== null && style !== void 0 ? style : {},
184
- styleWidth = _ref2.width,
185
- listStyleRest = _objectWithoutProperties(_ref2, _excluded);
186
- var widthStyle = styleWidth != null ? {
187
- width: styleWidth
188
- } : {};
189
- var listContent = /*#__PURE__*/React.createElement("div", {
190
- "data-odn-skill-slot": true,
191
- "data-odn-skill-slot-variant": variant,
192
- id: variant === 'menu' ? listboxId : undefined,
193
- role: variant === 'menu' ? 'listbox' : undefined,
194
- tabIndex: variant === 'menu' ? 0 : undefined,
195
- "aria-activedescendant": variant === 'menu' && currentIndex >= 0 ? "".concat(listboxId, "-opt-").concat(currentIndex) : undefined,
196
- onKeyDown: variant === 'menu' ? handleMenuKeyDown : undefined,
197
- onFocus: variant === 'menu' ? handleMenuFocus : undefined,
198
- onBlur: variant === 'menu' ? handleMenuBlur : undefined,
199
- className: className,
200
- style: _objectSpread(_objectSpread({}, hasScrollWrap ? listStyleRest : style), variant !== 'menu' && itemWidth != null ? {
201
- '--odn-skill-slot-item-width': "".concat(itemWidth, "px")
202
- } : {})
203
- }, skills.map(function (skill, index) {
188
+
189
+ /* 渲染单个菜单项(menu variant)——提取为独立函数以便扁平/分组共用。 */
190
+ var renderMenuItem = function renderMenuItem(skill, index) {
204
191
  var active = value === skill.id;
205
192
  var highlighted = currentIndex === index;
206
- /* disabled 与 active 互斥时,active 优先(已选中不应再渲染禁用样态)。
207
- * onSelect 通过 button[disabled] 自然短路;HoverFill 也走 disabled 关掉 hover 反馈。 */
208
193
  var disabled = !!skill.disabled && !active;
209
- if (variant === 'card') {
210
- return /*#__PURE__*/React.createElement("button", {
211
- key: skill.id,
212
- type: "button",
213
- "data-odn-skill-slot-item": true,
214
- "data-odn-skill-slot-item-active": active || undefined,
215
- "data-odn-skill-slot-item-disabled": disabled || undefined,
216
- disabled: disabled,
217
- "aria-disabled": disabled || undefined,
218
- onClick: function onClick() {
219
- return onSelect === null || onSelect === void 0 ? void 0 : onSelect(skill);
194
+ return /*#__PURE__*/React.createElement(HoverFill, {
195
+ key: skill.id,
196
+ "data-odn-skill-slot-item-wrap": true,
197
+ bgClassName: "odn-skill-slot-item-bg",
198
+ disabled: active || disabled,
199
+ onMouseEnter: function onMouseEnter() {
200
+ if (disabled) return;
201
+ if (isControlled) {
202
+ if (onActiveIndexChange && activeIndex !== index) onActiveIndexChange(index);
203
+ } else if (internalActiveIndex !== index) {
204
+ setInternalActiveIndex(index);
220
205
  }
221
- }, /*#__PURE__*/React.createElement("span", {
222
- "data-odn-skill-slot-card-header": true
223
- }, /*#__PURE__*/React.createElement("span", {
224
- "data-odn-skill-slot-item-icon": true
225
- }, /*#__PURE__*/React.createElement(Icon, {
226
- name: skill.icon,
227
- size: 16
228
- })), /*#__PURE__*/React.createElement("span", {
229
- "data-odn-skill-slot-item-label": true
230
- }, skill.label)), skill.description && /*#__PURE__*/React.createElement("span", {
231
- "data-odn-skill-slot-item-desc": true
232
- }, skill.description));
233
- }
234
- if (variant === 'menu') {
235
- /* HoverFill 包 button:鼠标 hover 视觉由 HoverFill 命令式控制;
236
- * 键盘高亮走 button 上的 data-odn-skill-slot-item-highlighted ::before 层。
237
- * 两层互斥(hover 时不画 ::before),颜色同值,视觉一致。 */
238
- return /*#__PURE__*/React.createElement(HoverFill, {
239
- key: skill.id,
240
- "data-odn-skill-slot-item-wrap": true,
241
- bgClassName: "odn-skill-slot-item-bg",
242
- disabled: active || disabled,
243
- onMouseEnter: function onMouseEnter() {
244
- /* 鼠标进入:把高亮 index 同步过来,让鼠标/键盘两条轨道始终指向同一项,
245
- * 避免鼠标移开后键盘 active 还指向另一项造成"鬼影高亮"。
246
- * 受控走 onActiveIndexChange,非受控更新内部 state。 */
247
- if (disabled) return;
248
- if (isControlled) {
249
- if (onActiveIndexChange && activeIndex !== index) onActiveIndexChange(index);
250
- } else if (internalActiveIndex !== index) {
251
- setInternalActiveIndex(index);
206
+ },
207
+ onMouseDown: function onMouseDown(e) {
208
+ return e.preventDefault();
209
+ }
210
+ }, /*#__PURE__*/React.createElement("button", {
211
+ type: "button",
212
+ id: "".concat(listboxId, "-opt-").concat(index),
213
+ ref: function ref(el) {
214
+ itemRefs.current[index] = el;
215
+ },
216
+ "data-odn-skill-slot-item": true,
217
+ "data-odn-skill-slot-item-active": active || undefined,
218
+ "data-odn-skill-slot-item-highlighted": highlighted || undefined,
219
+ "data-odn-skill-slot-item-disabled": disabled || undefined,
220
+ role: "option",
221
+ "aria-selected": highlighted,
222
+ tabIndex: -1,
223
+ disabled: disabled,
224
+ "aria-disabled": disabled || undefined,
225
+ onClick: function onClick() {
226
+ return onSelect === null || onSelect === void 0 ? void 0 : onSelect(skill);
227
+ }
228
+ }, /*#__PURE__*/React.createElement(Icon, {
229
+ name: skill.icon,
230
+ size: 14
231
+ }), /*#__PURE__*/React.createElement("span", {
232
+ "data-odn-skill-slot-item-content": true
233
+ }, /*#__PURE__*/React.createElement("span", {
234
+ "data-odn-skill-slot-item-label": true
235
+ }, skill.label), skill.description && /*#__PURE__*/React.createElement("span", {
236
+ "data-odn-skill-slot-item-desc": true
237
+ }, skill.description))));
238
+ };
239
+
240
+ /* 渲染底部"技能管理"按钮(仅 menu 变体 + 提供 onManageSkills 时。不参与键盘导航)。 */
241
+ var renderManageFooter = function renderManageFooter() {
242
+ if (variant !== 'menu' || !onManageSkills) return null;
243
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
244
+ "data-odn-skill-slot-divider": true
245
+ }), /*#__PURE__*/React.createElement("button", {
246
+ type: "button",
247
+ "data-odn-skill-slot-manage-btn": true,
248
+ onMouseDown: function onMouseDown(e) {
249
+ return e.preventDefault();
250
+ },
251
+ onClick: onManageSkills
252
+ }, /*#__PURE__*/React.createElement("span", null, "\u6280\u80FD\u7BA1\u7406"), /*#__PURE__*/React.createElement(Icon, {
253
+ name: "right",
254
+ size: 12
255
+ })));
256
+ };
257
+
258
+ /* 分组模式下渲染 menu 列表(带分组标题)。 */
259
+ var renderMenuGroups = function renderMenuGroups() {
260
+ var flatIndex = 0;
261
+ return /*#__PURE__*/React.createElement(React.Fragment, null, skillGroups.map(function (group) {
262
+ return /*#__PURE__*/React.createElement("div", {
263
+ key: group.groupName || "_group_".concat(flatIndex)
264
+ }, group.groupName && /*#__PURE__*/React.createElement("div", {
265
+ "data-odn-skill-slot-group-header": true
266
+ }, group.groupName), group.skills.map(function (skill) {
267
+ var idx = flatIndex++;
268
+ return renderMenuItem(skill, idx);
269
+ }));
270
+ }));
271
+ };
272
+
273
+ /* 扁平模式下渲染所有项(menu / bar / card)。 */
274
+ var renderFlatItems = function renderFlatItems() {
275
+ return skills.map(function (skill, index) {
276
+ var active = value === skill.id;
277
+ var highlighted = currentIndex === index;
278
+ var disabled = !!skill.disabled && !active;
279
+ if (variant === 'card') {
280
+ return /*#__PURE__*/React.createElement("button", {
281
+ key: skill.id,
282
+ type: "button",
283
+ "data-odn-skill-slot-item": true,
284
+ "data-odn-skill-slot-item-active": active || undefined,
285
+ "data-odn-skill-slot-item-disabled": disabled || undefined,
286
+ disabled: disabled,
287
+ "aria-disabled": disabled || undefined,
288
+ onClick: function onClick() {
289
+ return onSelect === null || onSelect === void 0 ? void 0 : onSelect(skill);
252
290
  }
253
- }
254
- /* 阻止 contenteditable 在 mousedown 时 blur,否则点 menu item 的瞬间
255
- * editor 失焦 → trigger probe / chip 插入 selection 拿不到位置。 */,
256
- onMouseDown: function onMouseDown(e) {
257
- return e.preventDefault();
258
- }
259
- }, /*#__PURE__*/React.createElement("button", {
291
+ }, /*#__PURE__*/React.createElement("span", {
292
+ "data-odn-skill-slot-card-header": true
293
+ }, /*#__PURE__*/React.createElement("span", {
294
+ "data-odn-skill-slot-item-icon": true
295
+ }, /*#__PURE__*/React.createElement(Icon, {
296
+ name: skill.icon,
297
+ size: 16
298
+ })), /*#__PURE__*/React.createElement("span", {
299
+ "data-odn-skill-slot-item-label": true
300
+ }, skill.label)), skill.description && /*#__PURE__*/React.createElement("span", {
301
+ "data-odn-skill-slot-item-desc": true
302
+ }, skill.description));
303
+ }
304
+ if (variant === 'menu') {
305
+ return renderMenuItem(skill, index);
306
+ }
307
+ var barButton = /*#__PURE__*/React.createElement("button", {
308
+ key: skill.id,
260
309
  type: "button",
261
- id: "".concat(listboxId, "-opt-").concat(index),
262
- ref: function ref(el) {
263
- itemRefs.current[index] = el;
264
- },
265
310
  "data-odn-skill-slot-item": true,
266
311
  "data-odn-skill-slot-item-active": active || undefined,
267
- "data-odn-skill-slot-item-highlighted": highlighted || undefined,
268
312
  "data-odn-skill-slot-item-disabled": disabled || undefined,
269
- role: "option",
270
- "aria-selected": highlighted,
271
- tabIndex: -1,
272
313
  disabled: disabled,
273
314
  "aria-disabled": disabled || undefined,
274
315
  onClick: function onClick() {
@@ -277,52 +318,54 @@ export var SkillSlot = /*#__PURE__*/forwardRef(function SkillSlot(_ref, ref) {
277
318
  }, /*#__PURE__*/React.createElement(Icon, {
278
319
  name: skill.icon,
279
320
  size: 14
280
- }), /*#__PURE__*/React.createElement("span", {
281
- "data-odn-skill-slot-item-content": true
282
- }, /*#__PURE__*/React.createElement("span", {
283
- "data-odn-skill-slot-item-label": true
284
- }, skill.label), skill.description && /*#__PURE__*/React.createElement("span", {
285
- "data-odn-skill-slot-item-desc": true
286
- }, skill.description))));
287
- }
288
- var barButton = /*#__PURE__*/React.createElement("button", {
289
- key: skill.id,
290
- type: "button",
291
- "data-odn-skill-slot-item": true,
292
- "data-odn-skill-slot-item-active": active || undefined,
293
- "data-odn-skill-slot-item-disabled": disabled || undefined,
294
- disabled: disabled,
295
- "aria-disabled": disabled || undefined,
296
- onClick: function onClick() {
297
- return onSelect === null || onSelect === void 0 ? void 0 : onSelect(skill);
321
+ }), skill.label);
322
+ if (skill.description) {
323
+ return /*#__PURE__*/React.createElement(Tooltip, {
324
+ key: skill.id,
325
+ popup: skill.description,
326
+ placement: "top",
327
+ offset: 6
328
+ }, barButton);
298
329
  }
299
- }, /*#__PURE__*/React.createElement(Icon, {
300
- name: skill.icon,
301
- size: 14
302
- }), skill.label);
330
+ return barButton;
331
+ });
332
+ };
333
+ var listBody = variant === 'menu' && skillGroups ? renderMenuGroups() : renderFlatItems();
334
+ var manageFooter = renderManageFooter();
303
335
 
304
- /* description Tooltip 给可点击项使用——禁用项 button[disabled] 本身不会触发
305
- * hover/focus,Tooltip 自然不弹;保持包裹结构无副作用。 */
306
- if (skill.description) {
307
- return /*#__PURE__*/React.createElement(Tooltip, {
308
- key: skill.id,
309
- popup: skill.description,
310
- placement: "top",
311
- offset: 6
312
- }, barButton);
313
- }
314
- return barButton;
315
- }));
336
+ /* 共享的属性容器:menu variant aria / keyboard / 交互属性 */
337
+ var listAttrs = variant === 'menu' ? {
338
+ id: listboxId,
339
+ role: 'listbox',
340
+ tabIndex: 0,
341
+ 'aria-activedescendant': currentIndex >= 0 ? "".concat(listboxId, "-opt-").concat(currentIndex) : undefined,
342
+ onKeyDown: handleMenuKeyDown,
343
+ onFocus: handleMenuFocus,
344
+ onBlur: handleMenuBlur
345
+ } : {};
316
346
  if (hasScrollWrap) {
317
- return /*#__PURE__*/React.createElement("div", {
347
+ /* 滚动仅包裹技能列表,footer 固定在底部不参与滚动 */
348
+ return /*#__PURE__*/React.createElement("div", _extends({
349
+ "data-odn-skill-slot": true,
350
+ "data-odn-skill-slot-variant": variant,
351
+ className: className,
352
+ style: style
353
+ }, listAttrs), /*#__PURE__*/React.createElement("div", {
318
354
  "data-odn-skill-slot-scroll-wrap": true,
319
- style: _objectSpread({
355
+ style: {
320
356
  maxHeight: menuMaxHeight,
321
357
  overflowY: 'auto'
322
- }, widthStyle)
323
- }, listContent);
358
+ }
359
+ }, listBody), manageFooter);
324
360
  }
325
- return listContent;
361
+ return /*#__PURE__*/React.createElement("div", _extends({
362
+ "data-odn-skill-slot": true,
363
+ "data-odn-skill-slot-variant": variant,
364
+ className: className,
365
+ style: _objectSpread(_objectSpread({}, variant !== 'menu' && itemWidth != null ? {
366
+ '--odn-skill-slot-item-width': "".concat(itemWidth, "px")
367
+ } : {}), style)
368
+ }, listAttrs), listBody, manageFooter);
326
369
  });
327
370
  SkillSlot.displayName = 'SkillSlot';
328
371
  export default SkillSlot;
@@ -20,16 +20,13 @@
20
20
  }
21
21
 
22
22
  /* ---- menu 滚动包装层 ----
23
- * max-height 由外部通过 style 传入;overflow-y: auto 由 JS 内联写入。
24
- * 统一细滚动条样式,并继承 popup 圆角裁剪。 */
23
+ * max-height / overflow-y: auto 由 JS 内联传入。
24
+ * 统一细滚动条样式。 */
25
25
  [data-odn-skill-slot-scroll-wrap] {
26
26
  overflow-y: auto;
27
+ width: 100%;
27
28
  scrollbar-width: thin;
28
29
  scrollbar-color: var(--odn-color-border) transparent;
29
- /* 内层 list 撑满 scroll-wrap 宽度 */
30
- }
31
- [data-odn-skill-slot-scroll-wrap] > [data-odn-skill-slot] {
32
- width: 100%;
33
30
  }
34
31
 
35
32
  /* ---- variant: menu ---- */
@@ -101,6 +98,48 @@
101
98
  font-size: 12px;
102
99
  }
103
100
 
101
+ /* ---- menu 分组标题 ---- */
102
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-group-header] {
103
+ padding: 6px 12px 2px;
104
+ font-size: 11px;
105
+ font-weight: 600;
106
+ color: var(--odn-color-black-8);
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.5px;
109
+ line-height: 20px;
110
+ user-select: none;
111
+ pointer-events: none;
112
+ }
113
+
114
+ /* ---- menu 底部分隔线 ---- */
115
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-divider] {
116
+ margin: 4px 0;
117
+ height: 1px;
118
+ background: var(--odn-color-black-5);
119
+ }
120
+
121
+ /* ---- menu 底部管理按钮 ---- */
122
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-manage-btn] {
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ width: 100%;
127
+ padding: 8px 12px;
128
+ border: none;
129
+ background: transparent;
130
+ font-size: 13px;
131
+ color: var(--odn-color-black-9);
132
+ cursor: pointer;
133
+ font-family: inherit;
134
+ transition: background-color 0.15s;
135
+ }
136
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-manage-btn]:hover {
137
+ background: rgba(33, 34, 38, 0.05);
138
+ }
139
+ [data-odn-skill-slot][data-odn-skill-slot-variant=menu] [data-odn-skill-slot-manage-btn]:active {
140
+ background: rgba(33, 34, 38, 0.08);
141
+ }
142
+
104
143
  /* ---- variant: bar ---- */
105
144
  [data-odn-skill-slot][data-odn-skill-slot-variant=bar] {
106
145
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "packageManager": "pnpm@11.8.0",
6
6
  "module": "dist/index.js",