@tfdesign/b-end 1.0.4

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.
Files changed (176) hide show
  1. package/AI_READ_FIRST.md +131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +353 -0
  4. package/package.json +67 -0
  5. package/scripts/check-tfds-contract.mjs +334 -0
  6. package/scripts/check-tfds-integration.mjs +263 -0
  7. package/scripts/postinstall-cursor-skill.mjs +382 -0
  8. package/scripts/setup.mjs +520 -0
  9. package/skills/tfds/CHECKLIST.md +205 -0
  10. package/skills/tfds/COMMON_FAILURES.md +238 -0
  11. package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
  12. package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
  13. package/skills/tfds/LAYOUT_RECIPES.md +140 -0
  14. package/skills/tfds/LAYOUT_RULES.md +1355 -0
  15. package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
  16. package/skills/tfds/SKILL.md +188 -0
  17. package/skills/tfds/components.index.json +7305 -0
  18. package/skills/tfds/components.summary.json +1809 -0
  19. package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
  20. package/src/_b_end_runtime/components/Avatar.jsx +325 -0
  21. package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
  22. package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
  23. package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
  24. package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
  25. package/src/_b_end_runtime/components/Button.jsx +144 -0
  26. package/src/_b_end_runtime/components/Button.tokens.js +90 -0
  27. package/src/_b_end_runtime/components/Card.jsx +460 -0
  28. package/src/_b_end_runtime/components/Card.tokens.js +124 -0
  29. package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
  30. package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
  31. package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
  32. package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
  33. package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
  34. package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
  35. package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
  36. package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
  37. package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
  38. package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
  39. package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
  40. package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
  41. package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
  42. package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
  43. package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
  44. package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
  45. package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
  46. package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
  47. package/src/_b_end_runtime/components/Empty.jsx +141 -0
  48. package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
  49. package/src/_b_end_runtime/components/Form.jsx +609 -0
  50. package/src/_b_end_runtime/components/Form.tokens.js +77 -0
  51. package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
  52. package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
  53. package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
  54. package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
  55. package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
  56. package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
  57. package/src/_b_end_runtime/components/Icon.jsx +172 -0
  58. package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
  59. package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
  60. package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
  61. package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
  62. package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
  63. package/src/_b_end_runtime/components/Input.jsx +258 -0
  64. package/src/_b_end_runtime/components/Input.tokens.js +68 -0
  65. package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
  66. package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
  67. package/src/_b_end_runtime/components/Modal.jsx +155 -0
  68. package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
  69. package/src/_b_end_runtime/components/NavBar.jsx +842 -0
  70. package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
  71. package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
  72. package/src/_b_end_runtime/components/Radio.jsx +227 -0
  73. package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
  74. package/src/_b_end_runtime/components/Select.jsx +766 -0
  75. package/src/_b_end_runtime/components/Select.tokens.js +99 -0
  76. package/src/_b_end_runtime/components/Sheet.jsx +132 -0
  77. package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
  78. package/src/_b_end_runtime/components/Slider.jsx +346 -0
  79. package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
  80. package/src/_b_end_runtime/components/Switch.jsx +124 -0
  81. package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
  82. package/src/_b_end_runtime/components/Table.jsx +1338 -0
  83. package/src/_b_end_runtime/components/Table.tokens.js +147 -0
  84. package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
  85. package/src/_b_end_runtime/components/Tabs.jsx +149 -0
  86. package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
  87. package/src/_b_end_runtime/components/Tag.jsx +199 -0
  88. package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
  89. package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
  90. package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
  91. package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
  92. package/src/_b_end_runtime/components/TagInput.jsx +382 -0
  93. package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
  94. package/src/_b_end_runtime/components/TextArea.jsx +363 -0
  95. package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
  96. package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
  97. package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
  98. package/src/_b_end_runtime/components/Toast.jsx +120 -0
  99. package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
  100. package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
  101. package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
  102. package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
  103. package/src/_b_end_runtime/components/Upload.jsx +455 -0
  104. package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
  105. package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
  106. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
  107. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
  108. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
  109. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
  110. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
  111. package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
  112. package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
  113. package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
  114. package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
  115. package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
  116. package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
  117. package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
  118. package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
  119. package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
  120. package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
  121. package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
  122. package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
  123. package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
  124. package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
  125. package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
  126. package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
  127. package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
  128. package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
  129. package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
  130. package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
  131. package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
  132. package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
  133. package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
  134. package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
  135. package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
  136. package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
  137. package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
  138. package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
  139. package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
  140. package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
  141. package/src/_b_end_runtime/components/table-assets/button.png +0 -0
  142. package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
  143. package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
  144. package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
  145. package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
  146. package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
  147. package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
  148. package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
  149. package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
  150. package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
  151. package/src/_b_end_runtime/components/tagShared.js +3 -0
  152. package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
  153. package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
  154. package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
  155. package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
  156. package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
  157. package/src/_b_end_runtime/components.js +3499 -0
  158. package/src/_b_end_runtime/index.js +9 -0
  159. package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
  160. package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
  161. package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
  162. package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
  163. package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
  164. package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
  165. package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
  166. package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
  167. package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
  168. package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
  169. package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
  170. package/src/_b_end_runtime/patterns.js +428 -0
  171. package/src/_b_end_runtime/preview-registry.jsx +4719 -0
  172. package/src/_b_end_runtime/teamMembers.js +56 -0
  173. package/src/_b_end_runtime/tokens.js +500 -0
  174. package/src/index.d.ts +1073 -0
  175. package/src/index.js +52 -0
  176. package/theme.css +350 -0
@@ -0,0 +1,1134 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import Button from './Button';
3
+ import Icon from './Icon';
4
+ import Input from './Input';
5
+
6
+ /**
7
+ * TagBar — B端业务标签栏(按 Figma 导航框架像素还原)
8
+ *
9
+ * 组件覆盖展开态树形标签栏与折叠态图标列,两种状态都保留业务切换、搜索入口和底部折叠按钮。
10
+ * 标签前置图标统一复用 B 端 Icon 库;业务图标使用设计稿中的品牌图。
11
+ *
12
+ * @prop {Array<{id: string, label: string, iconSrc?: string}>|null} [businesses=null] — 业务切换列表
13
+ * @prop {string|null} currentBusinessId — 当前业务 id(受控)
14
+ * @prop {string|null} [defaultBusinessId=null] — 非受控初始业务 id
15
+ * @prop {(businessId: string, meta: { business: {id: string, label: string, iconSrc?: string} | null }) => void} [onBusinessChange=null] — 业务切换回调
16
+ * @prop {Array<{id: string, label: string, iconName?: string, iconBgToken?: string, children?: any[]}>|null} [items=null] — 树形标签数据
17
+ * @prop {string|null} selectedItemId — 当前选中节点 id(受控)
18
+ * @prop {string|null} [defaultSelectedItemId=null] — 非受控初始选中节点 id
19
+ * @prop {(itemId: string, meta: { item: object, depth: number, businessId: string | null }) => void} [onSelect=null] — 节点点击回调
20
+ * @prop {string[]|null} expandedIds — 当前展开节点集合(受控)
21
+ * @prop {string[]} [defaultExpandedIds=[]] — 非受控初始展开节点集合
22
+ * @prop {(expandedIds: string[]) => void} [onExpandedChange=null] — 展开节点变化回调
23
+ * @prop {boolean} [searchable=true] — 是否显示搜索
24
+ * @prop {string} searchValue — 当前搜索关键字(受控)
25
+ * @prop {string} [defaultSearchValue=''] — 非受控初始搜索关键字
26
+ * @prop {(value: string) => void} [onSearchChange=null] — 搜索关键字变化回调
27
+ * @prop {boolean} [collapsible=true] — 是否显示折叠按钮
28
+ * @prop {boolean} collapsed — 当前收起状态(受控)
29
+ * @prop {boolean} [defaultCollapsed=false] — 非受控初始收起状态
30
+ * @prop {(collapsed: boolean) => void} [onCollapsedChange=null] — 收起状态变化回调
31
+ * @prop {'transparent'|'panel'} [tone='transparent'] — 背景风格:`transparent` 根节点透明底,用于①**外层独立白圆角卡**内嵌(大卡片/双白卡左列,由父容器提供白底与圆角)或②**单一大白卡**内左列(与主内容共白底、上下撑满);`panel` 为组件**自带浅灰底**的侧条样式,背景色固定为 `--color-blueGrey-100`,用于灰色独立导航条,**不是**白色独立卡片(白卡请外层 div + `transparent`)。
32
+ * @prop {boolean} [resizable=true] — 是否支持右侧拖拽调宽
33
+ * @prop {number} width — 当前宽度(受控)
34
+ * @prop {number} [defaultWidth=240] — 非受控初始宽度
35
+ * @prop {number} [minWidth=160] — 最小展开宽度
36
+ * @prop {number} [maxWidth=360] — 最大宽度
37
+ * @prop {number} [collapseThreshold=130] — 自动切图标态阈值
38
+ * @prop {(width: number) => void} [onWidthChange=null] — 宽度变化回调
39
+ * @prop {string} [className=''] — 附加类名
40
+ * @prop {React.CSSProperties|undefined} [style=undefined] — 附加样式
41
+ */
42
+
43
+ const DEFAULT_BUSINESS_ICON = 'icon-logo-douyin';
44
+ const DEFAULT_EXPANDED_WIDTH = 240;
45
+ const DEFAULT_COLLAPSED_WIDTH = 72;
46
+ const DEFAULT_MIN_WIDTH = 160;
47
+ const DEFAULT_MAX_WIDTH = 360;
48
+ const DEFAULT_COLLAPSE_THRESHOLD = 130;
49
+
50
+ export const TAGBAR_SAMPLE_BUSINESSES = [
51
+ { id: 'douyin-community', label: '抖音社区', iconSrc: DEFAULT_BUSINESS_ICON },
52
+ { id: 'douyin-local-services', label: '抖音生活服务', iconSrc: DEFAULT_BUSINESS_ICON },
53
+ { id: 'douyin-ecommerce', label: '抖音电商', iconSrc: DEFAULT_BUSINESS_ICON },
54
+ ];
55
+
56
+ function createThirdLevel(prefix, labels) {
57
+ return labels.map((label, index) => ({
58
+ id: `${prefix}-field-${index + 1}`,
59
+ label,
60
+ }));
61
+ }
62
+
63
+ function createSecondLevel(prefix, groups) {
64
+ return groups.map(({ id, label, fields }) => ({
65
+ id: `${prefix}-${id}`,
66
+ label,
67
+ children: createThirdLevel(`${prefix}-${id}`, fields),
68
+ }));
69
+ }
70
+
71
+ export const TAGBAR_SAMPLE_ITEMS = [
72
+ {
73
+ id: 'account',
74
+ label: '账号',
75
+ iconName: 'user-01-stroked',
76
+ iconBgToken: 'brand-50',
77
+ children: createSecondLevel('account', [
78
+ {
79
+ id: 'registration-login',
80
+ label: '账号注册/登录',
81
+ fields: ['手机号注册失败', '一键登录授权异常', '设备风控触发校验'],
82
+ },
83
+ {
84
+ id: 'realname-security',
85
+ label: '实名认证与安全',
86
+ fields: ['实名认证照片不通过', '人脸校验识别失败', '账号安全二次验证'],
87
+ },
88
+ {
89
+ id: 'recovery-cancel',
90
+ label: '找回与注销',
91
+ fields: ['账号找回申诉进度', '注销冷静期撤回', '原手机号不可用'],
92
+ },
93
+ ]),
94
+ },
95
+ {
96
+ id: 'base-product',
97
+ label: '基础产品',
98
+ iconName: 'layers-two-01-stroked',
99
+ iconBgToken: 'blue-50',
100
+ children: createSecondLevel('base-product', [
101
+ {
102
+ id: 'feed-browse',
103
+ label: '首页浏览',
104
+ fields: ['推荐流刷新卡顿', '视频自动播放异常', '首页缓存清理'],
105
+ },
106
+ {
107
+ id: 'comment-interaction',
108
+ label: '评论互动',
109
+ fields: ['评论发布失败', '置顶评论配置', '评论区折叠异常'],
110
+ },
111
+ {
112
+ id: 'message-center',
113
+ label: '消息中心',
114
+ fields: ['消息红点不同步', '通知聚合异常', '互动消息延迟'],
115
+ },
116
+ ]),
117
+ },
118
+ {
119
+ id: 'social',
120
+ label: '社交',
121
+ iconName: 'heart-circle-stroked',
122
+ iconBgToken: 'pink-50',
123
+ children: createSecondLevel('social', [
124
+ {
125
+ id: 'follow-graph',
126
+ label: '关注关系',
127
+ fields: ['关注失败重试', '粉丝关系同步延迟', '互关状态异常'],
128
+ },
129
+ {
130
+ id: 'private-chat',
131
+ label: '私信会话',
132
+ fields: ['私信发送失败', '消息已读回执异常', '会话置顶失效'],
133
+ },
134
+ {
135
+ id: 'group-interaction',
136
+ label: '群聊互动',
137
+ fields: ['群聊消息免打扰', '群成员邀请失败', '群公告发布异常'],
138
+ },
139
+ ]),
140
+ },
141
+ {
142
+ id: 'contribute',
143
+ label: '投稿',
144
+ iconName: 'edit-01-stroked',
145
+ iconBgToken: 'purple-50',
146
+ children: createSecondLevel('contribute', [
147
+ {
148
+ id: 'video-upload',
149
+ label: '视频上传',
150
+ fields: ['分片上传中断', '封面提取失败', '草稿同步异常'],
151
+ },
152
+ {
153
+ id: 'review-publish',
154
+ label: '审核发布',
155
+ fields: ['发布审核排队', '定时发布取消', '话题挂载失败'],
156
+ },
157
+ {
158
+ id: 'material-edit',
159
+ label: '素材编辑',
160
+ fields: ['字幕模板套用失败', '音乐卡点异常', '滤镜参数未保存'],
161
+ },
162
+ ]),
163
+ },
164
+ {
165
+ id: 'incentive',
166
+ label: '激励',
167
+ iconName: 'gift-01-stroked',
168
+ iconBgToken: 'purple-50',
169
+ children: createSecondLevel('incentive', [
170
+ {
171
+ id: 'creator-task',
172
+ label: '创作任务',
173
+ fields: ['任务报名资格校验', '任务进度回传异常', '任务奖励待发放'],
174
+ },
175
+ {
176
+ id: 'growth-rights',
177
+ label: '成长权益',
178
+ fields: ['成长等级升级延迟', '权益礼包领取失败', '专属标识展示异常'],
179
+ },
180
+ {
181
+ id: 'campaign-reward',
182
+ label: '活动奖励',
183
+ fields: ['挑战赛激励结算', '活动奖池分配异常', '奖励到账提醒缺失'],
184
+ },
185
+ ]),
186
+ },
187
+ {
188
+ id: 'monetization',
189
+ label: '作者变现',
190
+ iconName: 'credit-card-01-stroked',
191
+ iconBgToken: 'orange-50',
192
+ children: createSecondLevel('monetization', [
193
+ {
194
+ id: 'live-commerce',
195
+ label: '直播带货',
196
+ fields: ['直播商品讲解卡', '橱窗商品挂载失败', '讲解口令券发放异常'],
197
+ },
198
+ {
199
+ id: 'xingtu-cooperation',
200
+ label: '星图合作',
201
+ fields: ['商单任务待确认', '合作报价更新异常', '履约进度回传失败'],
202
+ },
203
+ {
204
+ id: 'commission-income',
205
+ label: '分佣收益',
206
+ fields: ['分佣结算待对账', '收益明细导出失败', '提现银行卡校验'],
207
+ },
208
+ ]),
209
+ },
210
+ {
211
+ id: 'content-traffic',
212
+ label: '内容与流量',
213
+ iconName: 'flip-forward-stroked',
214
+ iconBgToken: 'purple-50',
215
+ children: createSecondLevel('content-traffic', [
216
+ {
217
+ id: 'traffic-diagnosis',
218
+ label: '流量诊断',
219
+ fields: ['播放量波动分析', '完播率诊断建议', '流量来源拆解'],
220
+ },
221
+ {
222
+ id: 'distribution-strategy',
223
+ label: '内容分发',
224
+ fields: ['同城流量投放异常', '兴趣人群圈选失败', '分发实验配置冲突'],
225
+ },
226
+ {
227
+ id: 'fan-growth',
228
+ label: '粉丝增长',
229
+ fields: ['涨粉任务追踪', '粉丝画像更新延迟', '回访召回链路异常'],
230
+ },
231
+ ]),
232
+ },
233
+ {
234
+ id: 'search',
235
+ label: '搜索',
236
+ iconName: 'search-lg-stroked',
237
+ iconBgToken: 'cyan-50',
238
+ children: createSecondLevel('search', [
239
+ {
240
+ id: 'search-entry',
241
+ label: '搜索入口',
242
+ fields: ['顶部搜索推荐词', '热榜入口样式异常', '联想词刷新延迟'],
243
+ },
244
+ {
245
+ id: 'result-page',
246
+ label: '搜索结果',
247
+ fields: ['综合排序结果异常', '视频结果召回不足', '筛选项回显错误'],
248
+ },
249
+ {
250
+ id: 'hot-topic',
251
+ label: '热点承接',
252
+ fields: ['热点词配置生效慢', '活动词置顶失效', '趋势内容聚合异常'],
253
+ },
254
+ ]),
255
+ },
256
+ {
257
+ id: 'recommend',
258
+ label: '推荐',
259
+ iconName: 'star-01-stroked',
260
+ iconBgToken: 'yellow-50',
261
+ children: createSecondLevel('recommend', [
262
+ {
263
+ id: 'distribution-engine',
264
+ label: '推荐分发',
265
+ fields: ['首页推荐召回异常', '推荐池流量波动', '实验桶命中错误'],
266
+ },
267
+ {
268
+ id: 'interest-recall',
269
+ label: '兴趣召回',
270
+ fields: ['兴趣标签更新延迟', '相似作者召回不足', '兴趣探索流量异常'],
271
+ },
272
+ {
273
+ id: 'cold-start',
274
+ label: '冷启动扶持',
275
+ fields: ['新作者扶持策略', '冷启动内容曝光不足', '扶持任务统计延迟'],
276
+ },
277
+ ]),
278
+ },
279
+ {
280
+ id: 'commercialization',
281
+ label: '商业化',
282
+ iconName: 'shopping-bag-03-stroked',
283
+ iconBgToken: 'green-50',
284
+ children: createSecondLevel('commercialization', [
285
+ {
286
+ id: 'ads-delivery',
287
+ label: '广告投放',
288
+ fields: ['广告计划审核中', '人群包同步失败', '预算扣减异常'],
289
+ },
290
+ {
291
+ id: 'shop-operation',
292
+ label: '小店经营',
293
+ fields: ['商品上下架失败', '店铺装修配置丢失', '店铺分履约异常'],
294
+ },
295
+ {
296
+ id: 'live-deal',
297
+ label: '直播成交',
298
+ fields: ['直播成交转化下降', '订单核销数据延迟', '直播优惠券失效'],
299
+ },
300
+ ]),
301
+ },
302
+ {
303
+ id: 'ecosystem-governance',
304
+ label: '生态治理',
305
+ iconName: 'globe-01-stroked',
306
+ iconBgToken: 'blue-50',
307
+ children: createSecondLevel('ecosystem-governance', [
308
+ {
309
+ id: 'violation-control',
310
+ label: '违规治理',
311
+ fields: ['违规内容巡检任务', '账号处罚申诉中', '规则命中解释缺失'],
312
+ },
313
+ {
314
+ id: 'copyright-complaint',
315
+ label: '版权投诉',
316
+ fields: ['侵权视频下架流程', '版权白名单维护', '投诉工单回执异常'],
317
+ },
318
+ {
319
+ id: 'risk-inspection',
320
+ label: '风险巡检',
321
+ fields: ['黑产行为识别', '风险账号聚合看板', '风险策略回滚记录'],
322
+ },
323
+ ]),
324
+ },
325
+ {
326
+ id: 'service-guarantee',
327
+ label: '服务保障',
328
+ iconName: 'heart-hand-stroked',
329
+ iconBgToken: 'orange-50',
330
+ children: createSecondLevel('service-guarantee', [
331
+ {
332
+ id: 'ticket-service',
333
+ label: '工单服务',
334
+ fields: ['工单创建失败', '工单流转超时', '客服接单率预警'],
335
+ },
336
+ {
337
+ id: 'after-sale-compensation',
338
+ label: '售后赔付',
339
+ fields: ['退款赔付审核中', '赔付金额校验异常', '售后补贴到账慢'],
340
+ },
341
+ {
342
+ id: 'appeal-support',
343
+ label: '申诉保障',
344
+ fields: ['封禁申诉待补材料', '处罚申诉进度查询', '申诉结果通知缺失'],
345
+ },
346
+ ]),
347
+ },
348
+ ];
349
+
350
+ const ROOT_BASE = 'tfds-tag-bar relative flex h-full min-h-0 shrink-0 flex-col select-none transition-[width] duration-200 ease-out';
351
+ const MAIN_WRAP = 'flex min-h-0 flex-1 flex-col gap-3';
352
+ const SCROLL_AREA = 'flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto';
353
+ const ICON_BUTTON = 'inline-flex h-9 w-9 items-center justify-center rounded-[8px] text-foreground-muted transition-colors duration-150 hover:bg-fill focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200';
354
+ const ROW_BASE = 'group relative flex w-full items-center rounded-[8px] text-left text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200';
355
+ const ROW_BG_WRAP_SECONDARY = 'relative flex h-full w-full min-w-0 flex-1 pl-1 pr-1 -ml-1';
356
+ const ROW_BG_LAYER_SECONDARY = 'pointer-events-none absolute inset-0 rounded-[8px] transition-colors duration-150 group-hover:bg-fill';
357
+ const ROW_BG_WRAP_PRIMARY = 'flex h-9 w-full min-w-0 overflow-hidden rounded-[8px] transition-colors duration-150 group-hover:bg-fill';
358
+ const ROW_BG_WRAP_TERTIARY = 'flex h-full w-full min-w-0 flex-1 overflow-hidden rounded-[8px] transition-colors duration-150 group-hover:bg-fill';
359
+ const ROW_CONTENT = 'relative z-10 flex h-full w-full min-w-0 flex-1 items-center gap-1';
360
+ const ROW_CONTENT_PRIMARY = 'flex h-9 w-full min-w-0 items-center gap-1 px-3';
361
+ const ROW_CONTENT_TERTIARY = 'flex h-full w-full min-w-0 flex-1 items-center gap-1 px-2';
362
+ const BUSINESS_ROW = 'group flex h-9 w-full items-center gap-2 rounded-[8px] px-3 transition-colors duration-150';
363
+ const BUSINESS_MENU = 'absolute left-0 right-0 top-[calc(100%+8px)] z-30 flex flex-col gap-[4px] rounded-[8px] border border-border-default bg-surface p-1 shadow-lg';
364
+ const BUSINESS_MENU_ITEM = 'flex w-full items-center gap-2 rounded-[8px] px-3 py-2 text-left text-sm leading-5 text-foreground transition-colors duration-150 hover:bg-fill focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200';
365
+ const RESIZE_HANDLE_CLASS = 'absolute top-0 bottom-0 -right-[6px] z-20 w-3 cursor-col-resize touch-none select-none focus-visible:outline-none';
366
+ const RESIZE_HANDLE_BAR_CLASS = 'absolute right-[5px] top-0 bottom-0 w-[2px] rounded-full bg-transparent transition-colors duration-150';
367
+ const CHEVRON_BUTTON_CLASS = 'inline-flex h-3 w-4 shrink-0 items-center justify-center rounded-[4px] text-foreground-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200';
368
+
369
+ const ICON_BG_CLASS = {
370
+ 'brand-50': 'bg-brand-50 text-brand-500',
371
+ 'blueGrey-100': 'bg-blueGrey-100 text-blueGrey-700',
372
+ 'blue-50': 'bg-blue-50 text-blue-600',
373
+ 'pink-50': 'bg-pink-50 text-pink-600',
374
+ 'purple-50': 'bg-purple-50 text-purple-600',
375
+ 'orange-50': 'bg-orange-50 text-orange-600',
376
+ 'cyan-50': 'bg-cyan-50 text-cyan-600',
377
+ 'yellow-50': 'bg-yellow-50 text-yellow-600',
378
+ 'green-50': 'bg-green-50 text-green-600',
379
+ };
380
+
381
+ function flattenIds(items, bucket = []) {
382
+ items.forEach((item) => {
383
+ bucket.push(item.id);
384
+ if (Array.isArray(item.children) && item.children.length > 0) flattenIds(item.children, bucket);
385
+ });
386
+ return bucket;
387
+ }
388
+
389
+ function buildTopLevelSelectionMap(items, bucket = new Map(), topLevelItem = null) {
390
+ items.forEach((item) => {
391
+ const resolvedTopLevelItem = topLevelItem || item;
392
+ bucket.set(item.id, resolvedTopLevelItem);
393
+ if (Array.isArray(item.children) && item.children.length > 0) {
394
+ buildTopLevelSelectionMap(item.children, bucket, resolvedTopLevelItem);
395
+ }
396
+ });
397
+ return bucket;
398
+ }
399
+
400
+ function normalizeSearchText(value) {
401
+ return String(value || '').trim().toLowerCase().replace(/\s+/g, '');
402
+ }
403
+
404
+ function renderHighlightedLabel(label, keyword) {
405
+ const text = String(label || '');
406
+ const normalizedKeyword = normalizeSearchText(keyword);
407
+
408
+ if (!normalizedKeyword) return text;
409
+
410
+ const normalizedChars = [];
411
+ for (let index = 0; index < text.length; index += 1) {
412
+ const currentChar = text[index];
413
+ if (/\s/.test(currentChar)) continue;
414
+ normalizedChars.push({
415
+ originalIndex: index,
416
+ char: currentChar.toLowerCase(),
417
+ });
418
+ }
419
+
420
+ const normalizedText = normalizedChars.map((item) => item.char).join('');
421
+ const matchStart = normalizedText.indexOf(normalizedKeyword);
422
+ if (matchStart === -1) return text;
423
+
424
+ const matchEnd = matchStart + normalizedKeyword.length - 1;
425
+ const startIndex = normalizedChars[matchStart]?.originalIndex ?? 0;
426
+ const endIndex = normalizedChars[matchEnd]?.originalIndex ?? (text.length - 1);
427
+
428
+ return (
429
+ <>
430
+ {startIndex > 0 ? text.slice(0, startIndex) : null}
431
+ <span className="text-brand-500">{text.slice(startIndex, endIndex + 1)}</span>
432
+ {endIndex + 1 < text.length ? text.slice(endIndex + 1) : null}
433
+ </>
434
+ );
435
+ }
436
+
437
+ function filterTreeItems(items, keyword) {
438
+ if (!keyword) return items;
439
+
440
+ return items.reduce((acc, item) => {
441
+ const labelMatched = normalizeSearchText(item.label).includes(keyword);
442
+ const children = Array.isArray(item.children) ? item.children : [];
443
+ const nextChildren = children.length > 0 ? filterTreeItems(children, keyword) : [];
444
+
445
+ if (labelMatched) {
446
+ acc.push({ ...item, children });
447
+ return acc;
448
+ }
449
+
450
+ if (nextChildren.length > 0) {
451
+ acc.push({ ...item, children: nextChildren });
452
+ }
453
+
454
+ return acc;
455
+ }, []);
456
+ }
457
+
458
+ function collectExpandableIds(items, bucket = []) {
459
+ items.forEach((item) => {
460
+ if (Array.isArray(item.children) && item.children.length > 0) {
461
+ bucket.push(item.id);
462
+ collectExpandableIds(item.children, bucket);
463
+ }
464
+ });
465
+ return bucket;
466
+ }
467
+
468
+ function toggleId(list, id) {
469
+ return list.includes(id) ? list.filter((item) => item !== id) : [...list, id];
470
+ }
471
+
472
+ function clampWidth(value, min, max) {
473
+ return Math.min(Math.max(value, min), max);
474
+ }
475
+
476
+ function getBusiness(businesses, businessId) {
477
+ return businesses.find((item) => item.id === businessId) || businesses[0] || { id: 'douyin-community', label: '抖音社区', iconSrc: DEFAULT_BUSINESS_ICON };
478
+ }
479
+
480
+ function getRowMetrics(depth) {
481
+ if (depth <= 0) return { height: 36, paddingLeft: 12, fontClass: 'text-sm [font-weight:var(--font-semibold)] leading-5', textWidth: 'min-w-0 truncate' };
482
+ if (depth === 1) return { height: 32, paddingLeft: 36, fontClass: 'text-sm font-normal leading-5', textWidth: 'min-w-0 flex-1 truncate' };
483
+ return { height: 32, paddingLeft: 60, fontClass: 'text-xs font-normal leading-4', textWidth: 'min-w-0 flex-1 truncate' };
484
+ }
485
+
486
+ function getGuidePositions(depth) {
487
+ if (depth === 1) return [19.5];
488
+ if (depth >= 2) return [19.5, 43.5];
489
+ return [];
490
+ }
491
+
492
+ /** 子树容器内连续竖线(与各行 padding 对齐;高度随子块内容自适应,避免逐行 border 拼接缝) */
493
+ function TreeChildBlockGuides({ parentDepth }) {
494
+ const childDepth = parentDepth + 1;
495
+ const positions = getGuidePositions(childDepth);
496
+ if (positions.length === 0) return null;
497
+
498
+ return (
499
+ <div className="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden>
500
+ {positions.map((left, index) => (
501
+ <span
502
+ key={`tree-guide-${parentDepth}-${index}`}
503
+ className="absolute top-0 bottom-0 border-l border-border-default"
504
+ style={{ left: `${left}px` }}
505
+ />
506
+ ))}
507
+ </div>
508
+ );
509
+ }
510
+
511
+ function BusinessIcon({ iconName }) {
512
+ return (
513
+ <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-[4px] bg-grey-950">
514
+ <Icon name={iconName || DEFAULT_BUSINESS_ICON} size="sm" aria-hidden="true" />
515
+ </span>
516
+ );
517
+ }
518
+
519
+ function SearchField({ value, onChange, autoFocus = false }) {
520
+ return (
521
+ <Input
522
+ className="!w-full"
523
+ prefix={<Icon name="search-lg-stroked" size="sm" aria-hidden="true" />}
524
+ placeholder="搜索标签"
525
+ value={value}
526
+ onChange={onChange}
527
+ autoFocus={autoFocus}
528
+ aria-label="搜索标签"
529
+ />
530
+ );
531
+ }
532
+
533
+ function BusinessSwitcher({
534
+ businesses,
535
+ currentBusinessId,
536
+ selected,
537
+ menuOpen,
538
+ onSelect,
539
+ onToggleMenu,
540
+ onBusinessChange,
541
+ menuRef,
542
+ }) {
543
+ const currentBusiness = getBusiness(businesses, currentBusinessId);
544
+
545
+ return (
546
+ <div className="relative" data-tfds-component="TagBar.BusinessSwitcher">
547
+ <div
548
+ className={`${BUSINESS_ROW} ${selected ? 'bg-fill' : 'hover:bg-fill'}`}
549
+ >
550
+ <button
551
+ type="button"
552
+ className="flex min-w-0 flex-1 items-center gap-2 text-left"
553
+ aria-label={currentBusiness.label}
554
+ aria-pressed={selected}
555
+ onClick={onSelect}
556
+ data-tfds-component="TagBar.BusinessSwitcher"
557
+ >
558
+ <BusinessIcon iconName={currentBusiness.iconSrc} />
559
+ <span className="min-w-0 truncate text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground">{currentBusiness.label}</span>
560
+ </button>
561
+
562
+ <Button
563
+ variant="ghost-black"
564
+ size="sm"
565
+ radius="rounded"
566
+ iconOnly
567
+ icon={<Icon name="switch-horizontal-01-stroked" size="xs" className="text-foreground-muted" aria-hidden="true" />}
568
+ className="shrink-0"
569
+ aria-label="切换业务线"
570
+ aria-haspopup="menu"
571
+ aria-expanded={menuOpen}
572
+ onClick={onToggleMenu}
573
+ data-tfds-component="TagBar.BusinessToggle"
574
+ />
575
+ </div>
576
+
577
+ {menuOpen ? (
578
+ <div ref={menuRef} className={BUSINESS_MENU} role="menu" aria-label="业务线列表">
579
+ {businesses.map((business) => {
580
+ const active = business.id === currentBusiness.id;
581
+ return (
582
+ <button
583
+ key={business.id}
584
+ type="button"
585
+ role="menuitemradio"
586
+ aria-checked={active}
587
+ className={`${BUSINESS_MENU_ITEM} ${active ? 'bg-fill' : ''}`}
588
+ onClick={() => onBusinessChange(business.id)}
589
+ data-tfds-component="TagBar.BusinessOption"
590
+ >
591
+ <BusinessIcon iconName={business.iconSrc} />
592
+ <span className="min-w-0 flex-1 truncate">{business.label}</span>
593
+ {active ? <Icon name="check-stroked" size="xs" className="text-foreground-muted" aria-hidden="true" /> : null}
594
+ </button>
595
+ );
596
+ })}
597
+ </div>
598
+ ) : null}
599
+ </div>
600
+ );
601
+ }
602
+
603
+ function TagBarNode({
604
+ item,
605
+ depth,
606
+ collapsed,
607
+ expandedIdSet,
608
+ selectedItemId,
609
+ highlightedKeyword,
610
+ onToggleExpand,
611
+ onSelect,
612
+ }) {
613
+ const metrics = getRowMetrics(depth);
614
+ const hasChildren = Array.isArray(item.children) && item.children.length > 0;
615
+ const isExpanded = expandedIdSet.has(item.id);
616
+ const showChevron = depth <= 1;
617
+ const chevronName = hasChildren && isExpanded ? 'chevron-down-stroked' : 'chevron-up-stroked';
618
+ const iconTone = ICON_BG_CLASS[item.iconBgToken] || ICON_BG_CLASS['blueGrey-100'];
619
+ const isSelected = selectedItemId === item.id;
620
+ const isSecondary = depth === 1;
621
+ const rowBgWrapClass = depth === 0 ? ROW_BG_WRAP_PRIMARY : (depth >= 2 ? ROW_BG_WRAP_TERTIARY : ROW_BG_WRAP_SECONDARY);
622
+ const rowContentClass = depth === 0 ? ROW_CONTENT_PRIMARY : (depth >= 2 ? ROW_CONTENT_TERTIARY : ROW_CONTENT);
623
+ const chevronAriaLabel = isExpanded ? `收起${item.label}` : `展开${item.label}`;
624
+
625
+ if (collapsed) {
626
+ return (
627
+ <button
628
+ type="button"
629
+ className={`inline-flex h-8 w-9 items-center justify-center rounded-[8px] transition-colors duration-150 hover:bg-fill focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200 ${isSelected ? 'bg-fill' : ''}`}
630
+ aria-label={item.label}
631
+ title={item.label}
632
+ onClick={() => onSelect(item, depth)}
633
+ data-tfds-component="TagBar.Node"
634
+ >
635
+ {depth === 0 ? (
636
+ <span className={`inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-[4px] ${iconTone}`}>
637
+ <Icon name={item.iconName || 'globe-01-stroked'} size="xs" aria-hidden="true" />
638
+ </span>
639
+ ) : null}
640
+ </button>
641
+ );
642
+ }
643
+
644
+ return (
645
+ <div className="relative z-[1] flex flex-col gap-[2px]">
646
+ <button
647
+ type="button"
648
+ className={ROW_BASE}
649
+ style={{
650
+ height: `${metrics.height}px`,
651
+ paddingLeft: depth === 0 ? '0px' : `${metrics.paddingLeft}px`,
652
+ paddingRight: depth === 0 ? '0px' : '0px',
653
+ }}
654
+ aria-expanded={hasChildren ? isExpanded : undefined}
655
+ aria-label={item.label}
656
+ onClick={() => onSelect(item, depth)}
657
+ data-tfds-component="TagBar.Node"
658
+ >
659
+ <span className={`${rowBgWrapClass} ${!isSecondary && isSelected ? 'bg-fill' : ''}`}>
660
+ {isSecondary ? <span className={`${ROW_BG_LAYER_SECONDARY} ${isSelected ? 'bg-fill' : ''}`} aria-hidden="true" /> : null}
661
+ <span className={rowContentClass}>
662
+ {showChevron ? (
663
+ hasChildren ? (
664
+ <button
665
+ type="button"
666
+ className={CHEVRON_BUTTON_CLASS}
667
+ aria-label={chevronAriaLabel}
668
+ aria-expanded={isExpanded}
669
+ onClick={(event) => {
670
+ event.preventDefault();
671
+ event.stopPropagation();
672
+ onToggleExpand(item.id);
673
+ }}
674
+ data-tfds-component="TagBar.Expand"
675
+ >
676
+ <Icon name={chevronName} size="xs" aria-hidden="true" />
677
+ </button>
678
+ ) : (
679
+ <span className={CHEVRON_BUTTON_CLASS} aria-hidden="true">
680
+ <Icon name={chevronName} size="xs" aria-hidden="true" />
681
+ </span>
682
+ )
683
+ ) : null}
684
+
685
+ {depth === 0 && item.iconName ? (
686
+ <span className={`inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-[4px] ${iconTone}`}>
687
+ <Icon name={item.iconName} size="xs" aria-hidden="true" />
688
+ </span>
689
+ ) : null}
690
+
691
+ <span className={`${metrics.textWidth} ${metrics.fontClass}`}>{renderHighlightedLabel(item.label, highlightedKeyword)}</span>
692
+ </span>
693
+ </span>
694
+ </button>
695
+
696
+ {hasChildren && isExpanded ? (
697
+ <div className="relative flex flex-col gap-[2px]">
698
+ <TreeChildBlockGuides parentDepth={depth} />
699
+ {item.children.map((child) => (
700
+ <TagBarNode
701
+ key={child.id}
702
+ item={child}
703
+ depth={depth + 1}
704
+ collapsed={collapsed}
705
+ expandedIdSet={expandedIdSet}
706
+ selectedItemId={selectedItemId}
707
+ highlightedKeyword={highlightedKeyword}
708
+ onToggleExpand={onToggleExpand}
709
+ onSelect={onSelect}
710
+ />
711
+ ))}
712
+ </div>
713
+ ) : null}
714
+ </div>
715
+ );
716
+ }
717
+
718
+ export default function TagBar({
719
+ businesses = null,
720
+ currentBusinessId,
721
+ defaultBusinessId = null,
722
+ onBusinessChange = null,
723
+ items = null,
724
+ selectedItemId,
725
+ defaultSelectedItemId = 'account-registration-login-field-1',
726
+ onSelect = null,
727
+ expandedIds,
728
+ defaultExpandedIds = ['account', 'account-registration-login'],
729
+ onExpandedChange = null,
730
+ searchable = true,
731
+ searchValue,
732
+ defaultSearchValue = '',
733
+ onSearchChange = null,
734
+ collapsible = true,
735
+ collapsed,
736
+ defaultCollapsed = false,
737
+ onCollapsedChange = null,
738
+ tone = 'transparent',
739
+ resizable = true,
740
+ width,
741
+ defaultWidth = DEFAULT_EXPANDED_WIDTH,
742
+ minWidth = DEFAULT_MIN_WIDTH,
743
+ maxWidth = DEFAULT_MAX_WIDTH,
744
+ collapseThreshold = DEFAULT_COLLAPSE_THRESHOLD,
745
+ onWidthChange = null,
746
+ className = '',
747
+ style,
748
+ }) {
749
+ const resolvedBusinesses = Array.isArray(businesses) && businesses.length > 0 ? businesses : TAGBAR_SAMPLE_BUSINESSES;
750
+ const resolvedItems = Array.isArray(items) && items.length > 0 ? items : TAGBAR_SAMPLE_ITEMS;
751
+ const validItemIds = useMemo(() => flattenIds(resolvedItems, []), [resolvedItems]);
752
+ const topLevelSelectionMap = useMemo(() => buildTopLevelSelectionMap(resolvedItems), [resolvedItems]);
753
+
754
+ const isBusinessControlled = currentBusinessId !== undefined;
755
+ const isSelectedControlled = selectedItemId !== undefined;
756
+ const isExpandedControlled = expandedIds !== undefined;
757
+ const isSearchControlled = searchValue !== undefined;
758
+ const isCollapsedControlled = collapsed !== undefined;
759
+ const isWidthControlled = width !== undefined;
760
+ const normalizedMaxWidth = Math.max(maxWidth, minWidth, DEFAULT_COLLAPSED_WIDTH);
761
+ const normalizedCollapseThreshold = Math.min(Math.max(collapseThreshold, DEFAULT_COLLAPSED_WIDTH), normalizedMaxWidth);
762
+ const normalizedDefaultWidth = clampWidth(defaultWidth, DEFAULT_COLLAPSED_WIDTH, normalizedMaxWidth);
763
+
764
+ const [innerBusinessId, setInnerBusinessId] = useState(defaultBusinessId || resolvedBusinesses[0]?.id || null);
765
+ const [innerSelectedItemId, setInnerSelectedItemId] = useState(defaultSelectedItemId);
766
+ const [innerExpandedIds, setInnerExpandedIds] = useState(Array.isArray(defaultExpandedIds) ? defaultExpandedIds : ['account', 'account-registration-login']);
767
+ const [innerSearchValue, setInnerSearchValue] = useState(defaultSearchValue);
768
+ const [innerManualCollapsed, setInnerManualCollapsed] = useState(defaultCollapsed);
769
+ const [innerAutoCollapsed, setInnerAutoCollapsed] = useState(false);
770
+ const [innerWidth, setInnerWidth] = useState(normalizedDefaultWidth);
771
+ const [lastExpandedWidth, setLastExpandedWidth] = useState(Math.max(normalizedDefaultWidth, minWidth));
772
+ const [focusSearchOnExpand, setFocusSearchOnExpand] = useState(false);
773
+ const [businessMenuOpen, setBusinessMenuOpen] = useState(false);
774
+ const [isResizeActive, setIsResizeActive] = useState(false);
775
+ const dragStateRef = useRef(null);
776
+ const businessMenuRef = useRef(null);
777
+ const businessAreaRef = useRef(null);
778
+
779
+ const resolvedBusinessId = isBusinessControlled ? currentBusinessId : innerBusinessId;
780
+ const resolvedSelectedItemId = isSelectedControlled ? selectedItemId : innerSelectedItemId;
781
+ const resolvedExpandedIds = isExpandedControlled ? (Array.isArray(expandedIds) ? expandedIds : []) : innerExpandedIds;
782
+ const resolvedSearchValue = isSearchControlled ? searchValue : innerSearchValue;
783
+ const resolvedTargetWidth = isWidthControlled ? clampWidth(width, DEFAULT_COLLAPSED_WIDTH, normalizedMaxWidth) : innerWidth;
784
+ const isCollapsed = isCollapsedControlled ? collapsed : (innerManualCollapsed || innerAutoCollapsed);
785
+ const renderedWidth = isCollapsed ? DEFAULT_COLLAPSED_WIDTH : resolvedTargetWidth;
786
+
787
+ useEffect(() => {
788
+ if (!resolvedBusinesses.some((item) => item.id === resolvedBusinessId) && !isBusinessControlled) {
789
+ setInnerBusinessId(resolvedBusinesses[0]?.id || null);
790
+ }
791
+ }, [resolvedBusinesses, resolvedBusinessId, isBusinessControlled]);
792
+
793
+ useEffect(() => {
794
+ const validSelectableIds = new Set([...validItemIds, ...resolvedBusinesses.map((item) => item.id)]);
795
+ if (resolvedSelectedItemId && !validSelectableIds.has(resolvedSelectedItemId) && !isSelectedControlled) {
796
+ setInnerSelectedItemId(defaultSelectedItemId);
797
+ }
798
+ }, [resolvedSelectedItemId, validItemIds, resolvedBusinesses, isSelectedControlled, defaultSelectedItemId]);
799
+
800
+ useEffect(() => {
801
+ if (!isCollapsed && focusSearchOnExpand) {
802
+ setFocusSearchOnExpand(false);
803
+ }
804
+ }, [isCollapsed, focusSearchOnExpand]);
805
+
806
+ useEffect(() => {
807
+ if (!businessMenuOpen) return undefined;
808
+
809
+ function handlePointerDown(event) {
810
+ const target = event.target;
811
+ if (businessAreaRef.current?.contains?.(target)) return;
812
+ setBusinessMenuOpen(false);
813
+ }
814
+
815
+ document.addEventListener('mousedown', handlePointerDown);
816
+ return () => document.removeEventListener('mousedown', handlePointerDown);
817
+ }, [businessMenuOpen]);
818
+
819
+ useEffect(() => {
820
+ if (resolvedTargetWidth > normalizedCollapseThreshold) {
821
+ setLastExpandedWidth((prev) => (prev === resolvedTargetWidth ? prev : resolvedTargetWidth));
822
+ }
823
+ }, [resolvedTargetWidth, normalizedCollapseThreshold]);
824
+
825
+ const normalizedKeyword = searchable ? normalizeSearchText(resolvedSearchValue) : '';
826
+
827
+ const visibleItems = useMemo(() => (normalizedKeyword ? filterTreeItems(resolvedItems, normalizedKeyword) : resolvedItems), [resolvedItems, normalizedKeyword]);
828
+ const effectiveExpandedIds = useMemo(() => (normalizedKeyword ? collectExpandableIds(visibleItems, []) : resolvedExpandedIds), [normalizedKeyword, visibleItems, resolvedExpandedIds]);
829
+ const expandedIdSet = useMemo(() => new Set(effectiveExpandedIds), [effectiveExpandedIds]);
830
+ const businessIdSet = useMemo(() => new Set(resolvedBusinesses.map((item) => item.id)), [resolvedBusinesses]);
831
+
832
+ const currentBusiness = getBusiness(resolvedBusinesses, resolvedBusinessId);
833
+ const collapsedTreeSelectedItemId = useMemo(() => {
834
+ if (!resolvedSelectedItemId || businessIdSet.has(resolvedSelectedItemId)) return null;
835
+ return topLevelSelectionMap.get(resolvedSelectedItemId)?.id || null;
836
+ }, [resolvedSelectedItemId, businessIdSet, topLevelSelectionMap]);
837
+ const renderedSelectedItemId = isCollapsed ? collapsedTreeSelectedItemId : resolvedSelectedItemId;
838
+ const isCollapsedBusinessSelected = isCollapsed && resolvedSelectedItemId === currentBusiness.id;
839
+
840
+ /**
841
+ * 宽度策略:
842
+ * - 展开态:宽度在 [minWidth, maxWidth] 内(避免出现“非折叠但过窄”的尴尬态)
843
+ * - 折叠态:渲染宽度固定为 DEFAULT_COLLAPSED_WIDTH,内部仍可更新 lastExpandedWidth 以便恢复
844
+ */
845
+ const clampExpandedWidth = useCallback((value) => clampWidth(value, minWidth, normalizedMaxWidth), [minWidth, normalizedMaxWidth]);
846
+
847
+ const updateWidth = useCallback((nextWidth) => {
848
+ const clampedWidth = clampWidth(nextWidth, DEFAULT_COLLAPSED_WIDTH, normalizedMaxWidth);
849
+ if (!isWidthControlled) {
850
+ setInnerWidth(clampedWidth);
851
+ }
852
+ if (clampedWidth > normalizedCollapseThreshold) {
853
+ setLastExpandedWidth(clampedWidth);
854
+ }
855
+ onWidthChange?.(clampedWidth);
856
+ return clampedWidth;
857
+ }, [isWidthControlled, normalizedCollapseThreshold, normalizedMaxWidth, onWidthChange]);
858
+
859
+ const updateCollapsed = useCallback((nextCollapsed, options = {}) => {
860
+ const { source = 'manual', restoreWidth: shouldRestoreWidth = true } = options;
861
+ if (!isCollapsedControlled) {
862
+ if (nextCollapsed) {
863
+ setInnerManualCollapsed(source === 'manual');
864
+ setInnerAutoCollapsed(source === 'auto');
865
+ } else {
866
+ setInnerManualCollapsed(false);
867
+ setInnerAutoCollapsed(false);
868
+ }
869
+ }
870
+ if (!nextCollapsed && shouldRestoreWidth) {
871
+ const nextExpandedWidth = Math.max(lastExpandedWidth, minWidth);
872
+ updateWidth(nextExpandedWidth);
873
+ }
874
+ onCollapsedChange?.(nextCollapsed);
875
+ }, [isCollapsedControlled, lastExpandedWidth, minWidth, onCollapsedChange, updateWidth]);
876
+
877
+ const handleSearchChange = useCallback((event) => {
878
+ const nextValue = event.target.value;
879
+ if (!isSearchControlled) setInnerSearchValue(nextValue);
880
+ onSearchChange?.(nextValue);
881
+ }, [isSearchControlled, onSearchChange]);
882
+
883
+ const handleBusinessSelect = useCallback((businessId) => {
884
+ const business = resolvedBusinesses.find((item) => item.id === businessId) || null;
885
+ if (!isBusinessControlled) setInnerBusinessId(businessId);
886
+ if (!isSelectedControlled) setInnerSelectedItemId(businessId);
887
+ setBusinessMenuOpen(false);
888
+ onBusinessChange?.(businessId, { business });
889
+ }, [resolvedBusinesses, isBusinessControlled, isSelectedControlled, onBusinessChange]);
890
+
891
+ const handleSelectBusinessTag = useCallback(() => {
892
+ if (!isSelectedControlled) {
893
+ setInnerSelectedItemId(resolvedBusinessId);
894
+ }
895
+ onSelect?.(resolvedBusinessId, {
896
+ item: getBusiness(resolvedBusinesses, resolvedBusinessId),
897
+ depth: 0,
898
+ businessId: resolvedBusinessId || null,
899
+ });
900
+ }, [isSelectedControlled, onSelect, resolvedBusinesses, resolvedBusinessId]);
901
+
902
+ const handleSelectItem = useCallback((item, depth) => {
903
+ if (!isSelectedControlled) {
904
+ setInnerSelectedItemId(item.id);
905
+ }
906
+ onSelect?.(item.id, { item, depth, businessId: resolvedBusinessId || null });
907
+
908
+ }, [isSelectedControlled, onSelect, resolvedBusinessId, resolvedExpandedIds, isExpandedControlled, onExpandedChange]);
909
+
910
+ const handleToggleExpand = useCallback((itemId) => {
911
+ const nextExpandedIds = toggleId(resolvedExpandedIds, itemId);
912
+ if (!isExpandedControlled) setInnerExpandedIds(nextExpandedIds);
913
+ onExpandedChange?.(nextExpandedIds);
914
+ }, [resolvedExpandedIds, isExpandedControlled, onExpandedChange]);
915
+
916
+ const handleSearchButtonClick = useCallback(() => {
917
+ if (isCollapsed) {
918
+ setFocusSearchOnExpand(true);
919
+ updateCollapsed(false);
920
+ }
921
+ }, [isCollapsed, updateCollapsed]);
922
+
923
+ const handleResizePointerDown = useCallback((event) => {
924
+ if (!resizable) return;
925
+
926
+ event.preventDefault();
927
+ setIsResizeActive(true);
928
+ dragStateRef.current = {
929
+ startX: event.clientX,
930
+ startWidth: isCollapsed ? Math.max(lastExpandedWidth, minWidth) : resolvedTargetWidth,
931
+ latestWidth: isCollapsed ? Math.max(lastExpandedWidth, minWidth) : resolvedTargetWidth,
932
+ latestRawWidth: isCollapsed ? Math.max(lastExpandedWidth, minWidth) : resolvedTargetWidth,
933
+ };
934
+
935
+ if (!isCollapsedControlled) {
936
+ if (innerManualCollapsed) setInnerManualCollapsed(false);
937
+ if (innerAutoCollapsed) setInnerAutoCollapsed(false);
938
+ }
939
+
940
+ const previousUserSelect = document.body.style.userSelect;
941
+ document.body.style.userSelect = 'none';
942
+
943
+ const handlePointerMove = (moveEvent) => {
944
+ if (!dragStateRef.current) return;
945
+ const deltaX = moveEvent.clientX - dragStateRef.current.startX;
946
+ const nextRawWidth = dragStateRef.current.startWidth + deltaX;
947
+ const nextWidth = clampExpandedWidth(nextRawWidth);
948
+ const clampedWidth = updateWidth(nextWidth);
949
+ dragStateRef.current.latestWidth = clampedWidth;
950
+ dragStateRef.current.latestRawWidth = nextRawWidth;
951
+ };
952
+
953
+ const stopDragging = () => {
954
+ const finalRawWidth = dragStateRef.current?.latestRawWidth ?? dragStateRef.current?.startWidth ?? resolvedTargetWidth;
955
+ dragStateRef.current = null;
956
+ setIsResizeActive(false);
957
+ document.body.style.userSelect = previousUserSelect;
958
+ window.removeEventListener('pointermove', handlePointerMove);
959
+ window.removeEventListener('pointerup', stopDragging);
960
+ // 松手时才做折叠判定:避免拖拽过程中闪烁
961
+ if (finalRawWidth < normalizedCollapseThreshold) {
962
+ updateCollapsed(true, { source: 'auto', restoreWidth: false });
963
+ }
964
+ };
965
+
966
+ window.addEventListener('pointermove', handlePointerMove);
967
+ window.addEventListener('pointerup', stopDragging, { once: true });
968
+ }, [
969
+ resizable,
970
+ isCollapsed,
971
+ lastExpandedWidth,
972
+ minWidth,
973
+ resolvedTargetWidth,
974
+ isCollapsedControlled,
975
+ innerManualCollapsed,
976
+ innerAutoCollapsed,
977
+ normalizedCollapseThreshold,
978
+ clampExpandedWidth,
979
+ updateWidth,
980
+ updateCollapsed,
981
+ ]);
982
+
983
+ const handleResizeKeyDown = useCallback((event) => {
984
+ if (!resizable) return;
985
+
986
+ let nextWidth = isCollapsed ? Math.max(lastExpandedWidth, minWidth) : resolvedTargetWidth;
987
+ if (event.key === 'ArrowLeft') nextWidth -= 16;
988
+ if (event.key === 'ArrowRight') nextWidth += 16;
989
+ if (event.key === 'Home') nextWidth = normalizedCollapseThreshold;
990
+ if (event.key === 'End') nextWidth = normalizedMaxWidth;
991
+ if (nextWidth === (isCollapsed ? Math.max(lastExpandedWidth, minWidth) : resolvedTargetWidth)) return;
992
+
993
+ event.preventDefault();
994
+ if (!isCollapsedControlled) {
995
+ if (innerManualCollapsed) setInnerManualCollapsed(false);
996
+ if (innerAutoCollapsed) setInnerAutoCollapsed(false);
997
+ }
998
+ updateWidth(clampExpandedWidth(nextWidth));
999
+ if (nextWidth < normalizedCollapseThreshold) {
1000
+ updateCollapsed(true, { source: 'auto', restoreWidth: false });
1001
+ }
1002
+ }, [
1003
+ resizable,
1004
+ isCollapsed,
1005
+ lastExpandedWidth,
1006
+ minWidth,
1007
+ resolvedTargetWidth,
1008
+ normalizedCollapseThreshold,
1009
+ normalizedMaxWidth,
1010
+ isCollapsedControlled,
1011
+ innerManualCollapsed,
1012
+ innerAutoCollapsed,
1013
+ clampExpandedWidth,
1014
+ updateWidth,
1015
+ updateCollapsed,
1016
+ ]);
1017
+
1018
+ const rootBackgroundClass =
1019
+ tone === 'panel'
1020
+ ? 'bg-blueGrey-100'
1021
+ : 'bg-transparent';
1022
+
1023
+ return (
1024
+ <aside
1025
+ className={`${ROOT_BASE} ${rootBackgroundClass} ${className}`.trim()}
1026
+ style={{ width: `${renderedWidth}px`, padding: '16px', ...style }}
1027
+ aria-label="标签栏"
1028
+ data-tfds-component="TagBar"
1029
+ >
1030
+ <div className={MAIN_WRAP}>
1031
+ {isCollapsed && searchable ? (
1032
+ <button type="button" className={`${ICON_BUTTON} mt-0`} aria-label="搜索标签" onClick={handleSearchButtonClick} data-tfds-component="TagBar.Search">
1033
+ <Icon name="search-lg-stroked" size="sm" aria-hidden="true" />
1034
+ </button>
1035
+ ) : searchable ? (
1036
+ <SearchField value={resolvedSearchValue || ''} onChange={handleSearchChange} autoFocus={focusSearchOnExpand} />
1037
+ ) : null}
1038
+
1039
+ <div className={SCROLL_AREA}>
1040
+ <div className="relative">
1041
+ {isCollapsed ? (
1042
+ <button
1043
+ type="button"
1044
+ className={`inline-flex h-9 w-9 items-center justify-center rounded-[8px] transition-colors duration-150 ${isCollapsedBusinessSelected ? 'bg-fill' : 'hover:bg-fill'}`}
1045
+ aria-label={currentBusiness.label}
1046
+ aria-pressed={isCollapsedBusinessSelected}
1047
+ onClick={handleSelectBusinessTag}
1048
+ data-tfds-component="TagBar.BusinessSwitcher"
1049
+ >
1050
+ <BusinessIcon iconName={currentBusiness.iconSrc} />
1051
+ </button>
1052
+ ) : (
1053
+ <div ref={businessAreaRef}>
1054
+ <BusinessSwitcher
1055
+ businesses={resolvedBusinesses}
1056
+ currentBusinessId={currentBusiness.id}
1057
+ selected={resolvedSelectedItemId === currentBusiness.id}
1058
+ menuOpen={businessMenuOpen}
1059
+ onSelect={handleSelectBusinessTag}
1060
+ onToggleMenu={() => setBusinessMenuOpen((open) => !open)}
1061
+ onBusinessChange={handleBusinessSelect}
1062
+ menuRef={businessMenuRef}
1063
+ />
1064
+ </div>
1065
+ )}
1066
+ </div>
1067
+
1068
+ {visibleItems.length > 0 ? (
1069
+ <div className="flex flex-col gap-[2px]">
1070
+ {visibleItems.map((item) => (
1071
+ <TagBarNode
1072
+ key={item.id}
1073
+ item={item}
1074
+ depth={0}
1075
+ collapsed={isCollapsed}
1076
+ expandedIdSet={expandedIdSet}
1077
+ selectedItemId={renderedSelectedItemId}
1078
+ highlightedKeyword={normalizedKeyword}
1079
+ onToggleExpand={handleToggleExpand}
1080
+ onSelect={handleSelectItem}
1081
+ />
1082
+ ))}
1083
+ </div>
1084
+ ) : (
1085
+ <div className={`${isCollapsed ? 'h-8 w-9' : 'px-3 py-6'} flex items-center justify-center rounded-[8px] bg-fill text-center text-[12px] leading-4 text-foreground-muted`}>
1086
+ {isCollapsed ? <Icon name="search-lg-stroked" size="xs" aria-hidden="true" /> : '未找到匹配标签'}
1087
+ </div>
1088
+ )}
1089
+ </div>
1090
+ </div>
1091
+
1092
+ {collapsible ? (
1093
+ <div className="mt-2">
1094
+ <Button
1095
+ variant="ghost-black"
1096
+ size="md"
1097
+ radius="rounded"
1098
+ iconOnly
1099
+ icon={<Icon name="layout-left-stroked" size="sm" aria-hidden="true" />}
1100
+ aria-label={isCollapsed ? '展开标签栏' : '收起标签栏'}
1101
+ onClick={() => updateCollapsed(!isCollapsed)}
1102
+ data-tfds-component="TagBar.CollapseToggle"
1103
+ />
1104
+ </div>
1105
+ ) : null}
1106
+
1107
+ {resizable ? (
1108
+ <div
1109
+ className={RESIZE_HANDLE_CLASS}
1110
+ role="separator"
1111
+ aria-label="调整标签栏宽度"
1112
+ aria-orientation="vertical"
1113
+ aria-valuemin={DEFAULT_COLLAPSED_WIDTH}
1114
+ aria-valuemax={normalizedMaxWidth}
1115
+ aria-valuenow={Math.round(renderedWidth)}
1116
+ tabIndex={0}
1117
+ onPointerDown={handleResizePointerDown}
1118
+ onKeyDown={handleResizeKeyDown}
1119
+ onPointerEnter={() => setIsResizeActive(true)}
1120
+ onPointerLeave={() => {
1121
+ if (!dragStateRef.current) setIsResizeActive(false);
1122
+ }}
1123
+ onFocus={() => setIsResizeActive(true)}
1124
+ onBlur={() => {
1125
+ if (!dragStateRef.current) setIsResizeActive(false);
1126
+ }}
1127
+ data-tfds-component="TagBar.ResizeHandle"
1128
+ >
1129
+ <span className={`${RESIZE_HANDLE_BAR_CLASS} ${isResizeActive ? 'bg-border-default' : 'bg-transparent'}`} />
1130
+ </div>
1131
+ ) : null}
1132
+ </aside>
1133
+ );
1134
+ }