@tfdesign/b-end 1.0.13 → 1.0.15

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 (35) hide show
  1. package/package.json +1 -1
  2. package/skills/tfds/CHECKLIST.md +5 -0
  3. package/skills/tfds/COMMON_FAILURES.md +48 -0
  4. package/skills/tfds/DESIGN_PRINCIPLES.md +5 -0
  5. package/skills/tfds/GLOBAL_DESIGN_RULES.md +31 -0
  6. package/skills/tfds/LAYOUT_RULES.md +31 -0
  7. package/skills/tfds/components.index.json +75 -27
  8. package/skills/tfds/components.summary.json +13 -13
  9. package/src/_b_end_runtime/components/Card.jsx +151 -13
  10. package/src/_b_end_runtime/components/Card.tokens.js +27 -3
  11. package/src/_b_end_runtime/components/CardPreview.jsx +11 -3
  12. package/src/_b_end_runtime/components/ChatMessage.jsx +59 -1
  13. package/src/_b_end_runtime/components/ConversationList.jsx +68 -68
  14. package/src/_b_end_runtime/components/ConversationList.tokens.js +5 -3
  15. package/src/_b_end_runtime/components/FullScreenPage.jsx +1 -0
  16. package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +13 -15
  17. package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +2 -0
  18. package/src/_b_end_runtime/components/Modal.jsx +1 -0
  19. package/src/_b_end_runtime/components/Sheet.jsx +1 -0
  20. package/src/_b_end_runtime/components/Table.jsx +7 -0
  21. package/src/_b_end_runtime/components/Tabs.jsx +46 -3
  22. package/src/_b_end_runtime/components/Tabs.tokens.js +3 -0
  23. package/src/_b_end_runtime/components/TagBar.jsx +2 -0
  24. package/src/_b_end_runtime/components/Toast.jsx +1 -0
  25. package/src/_b_end_runtime/components/Upload.jsx +1 -0
  26. package/src/_b_end_runtime/components.js +24 -11
  27. package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +548 -135
  28. package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +1 -1
  29. package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +6 -6
  30. package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +66 -5
  31. package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +50 -17
  32. package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +28 -78
  33. package/src/_b_end_runtime/patterns.js +32 -21
  34. package/src/_b_end_runtime/preview-registry.jsx +20 -4
  35. package/src/index.d.ts +4 -2
@@ -46,7 +46,7 @@ const DEFAULT_SECTIONS = [
46
46
  time: '13:32',
47
47
  avatarSrc: liSiruAvatar,
48
48
  tags: [
49
- { label: '异常监控提醒', variant: 'grey' },
49
+ { label: '异常提醒', variant: 'grey' },
50
50
  { label: '待干预', variant: 'red' },
51
51
  ],
52
52
  },
@@ -70,7 +70,7 @@ const DEFAULT_SECTIONS = [
70
70
  time: '12:40',
71
71
  avatarSrc: guoZhezhiAvatar,
72
72
  tags: [
73
- { label: '退款执行错误', variant: 'grey' },
73
+ { label: '退款异常', variant: 'grey' },
74
74
  { label: '待干预', variant: 'red' },
75
75
  ],
76
76
  },
@@ -89,7 +89,7 @@ const DEFAULT_SECTIONS = [
89
89
  time: '11:22',
90
90
  avatarSrc: chengchengAvatar,
91
91
  tags: [
92
- { label: '24时30分', variant: 'grey', iconName: 'alarm-clock-stroked' },
92
+ { label: '24时+', variant: 'grey', iconName: 'alarm-clock-stroked' },
93
93
  { label: '托管中', variant: 'green' },
94
94
  ],
95
95
  },
@@ -112,7 +112,7 @@ const DEFAULT_SECTIONS = [
112
112
  time: '09:46',
113
113
  avatarSrc: liuDelinAvatar,
114
114
  tags: [
115
- { label: '协商不成功', variant: 'grey' },
115
+ { label: '协商未成', variant: 'grey' },
116
116
  { label: '托管中', variant: 'green' },
117
117
  ],
118
118
  },
@@ -248,7 +248,7 @@ const TIME = 'ml-auto shrink-0 pl-2 text-foreground-muted';
248
248
  const AVATAR_ONLY_HEADER = 'flex h-8 w-full items-center justify-center';
249
249
  const AVATAR_ONLY_LIST = 'flex min-h-0 flex-1 flex-col self-stretch gap-1 overflow-y-auto';
250
250
  const AVATAR_ONLY_ITEM = [
251
- 'relative inline-flex h-[68px] w-full items-center rounded-xl px-4',
251
+ 'relative inline-flex h-[68px] min-h-[68px] w-full shrink-0 items-center rounded-xl px-4',
252
252
  'transition-colors duration-150 hover:bg-fill active:bg-fill-active',
253
253
  'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200',
254
254
  ].join(' ');
@@ -276,10 +276,11 @@ const CARD_MESSAGE_REGION_GENERATING = 'h-[196px] gap-3';
276
276
  const CARD_MESSAGE_REGION_EDITABLE = 'h-[155px] gap-3';
277
277
  const CARD_MESSAGE_ROW_BASE = 'flex w-full';
278
278
  const CARD_MESSAGE_ROW_USER = 'justify-start';
279
- const CARD_MESSAGE_ROW_AGENT = 'justify-end';
279
+ const CARD_MESSAGE_ROW_OUTGOING = 'justify-end';
280
280
  const CARD_MESSAGE_BUBBLE_BASE = 'max-w-[calc(100%-24px)] rounded-lg px-2 py-1.5 text-xs leading-4 text-foreground';
281
281
  const CARD_MESSAGE_BUBBLE_USER = 'rounded-tl-none bg-fill';
282
- const CARD_MESSAGE_BUBBLE_AGENT = 'rounded-tr-none bg-[image:var(--gradient-ai-fill-1)]';
282
+ const CARD_MESSAGE_BUBBLE_BOT = 'rounded-tr-none bg-[image:var(--gradient-ai-fill-1)]';
283
+ const CARD_MESSAGE_BUBBLE_AGENT = 'rounded-tr-none bg-chat-outgoing';
283
284
  const CARD_DIVIDER = 'h-px w-full bg-border-default';
284
285
  const CARD_FOOTER_BASE = 'shrink-0';
285
286
  const CARD_FOOTER_REPLIED = 'px-4 py-4 text-xs leading-4 text-[#22272759]';
@@ -407,13 +408,17 @@ function sortCardTags(tags) {
407
408
  }
408
409
 
409
410
  function ConversationCardMessage({ message }) {
410
- const isAgent = message.role === 'agent';
411
+ const isUser = message.role === 'user';
412
+ const isBot = message.role === 'bot';
413
+ const bubbleClassName = isUser
414
+ ? CARD_MESSAGE_BUBBLE_USER
415
+ : (isBot ? CARD_MESSAGE_BUBBLE_BOT : CARD_MESSAGE_BUBBLE_AGENT);
411
416
 
412
417
  return (
413
- <div className={[CARD_MESSAGE_ROW_BASE, isAgent ? CARD_MESSAGE_ROW_AGENT : CARD_MESSAGE_ROW_USER].join(' ')}>
418
+ <div className={[CARD_MESSAGE_ROW_BASE, isUser ? CARD_MESSAGE_ROW_USER : CARD_MESSAGE_ROW_OUTGOING].join(' ')}>
414
419
  <div className={[
415
420
  CARD_MESSAGE_BUBBLE_BASE,
416
- isAgent ? CARD_MESSAGE_BUBBLE_AGENT : CARD_MESSAGE_BUBBLE_USER,
421
+ bubbleClassName,
417
422
  ].join(' ')}>
418
423
  {message.text}
419
424
  </div>
@@ -499,6 +504,7 @@ function ConversationCardFooter({ card, onSend }) {
499
504
  icon={<Icon name="send-01-stroked" size={16} aria-hidden="true" />}
500
505
  iconOnly
501
506
  className="!h-6 !w-6 !rounded-md !p-1"
507
+ tooltip="发送回复"
502
508
  onKeyDown={(event) => {
503
509
  event.stopPropagation();
504
510
  }}
@@ -1049,17 +1055,16 @@ function ConversationListDefaultVariant({
1049
1055
  {!isCardVariant && isAvatarOnly ? (
1050
1056
  <>
1051
1057
  <div className={AVATAR_ONLY_HEADER}>
1052
- <Tooltip content="展开会话列表">
1053
- <Button
1054
- type="button"
1055
- variant="ghost-black"
1056
- size="sm"
1057
- icon={<Icon name="layout-right-stroked" size={16} />}
1058
- iconOnly
1059
- onClick={handleExpandFromAvatarOnly}
1060
- aria-label="展开会话列表"
1061
- />
1062
- </Tooltip>
1058
+ <Button
1059
+ type="button"
1060
+ variant="ghost-black"
1061
+ size="sm"
1062
+ icon={<Icon name="layout-right-stroked" size={16} />}
1063
+ iconOnly
1064
+ tooltip="展开会话列表"
1065
+ onClick={handleExpandFromAvatarOnly}
1066
+ aria-label="展开会话列表"
1067
+ />
1063
1068
  </div>
1064
1069
  <div className={AVATAR_ONLY_LIST}>
1065
1070
  {avatarOnlyItems.map((item) => (
@@ -1077,65 +1082,60 @@ function ConversationListDefaultVariant({
1077
1082
  <header className={HEADER}>
1078
1083
  <div className={HEADER_MAIN}>
1079
1084
  {!isCardVariant && collapsible ? (
1080
- <Tooltip content="收起会话列表">
1081
- <Button
1082
- type="button"
1083
- variant="ghost-black"
1084
- size="sm"
1085
- icon={<Icon name="layout-right-stroked" size={16} />}
1086
- iconOnly
1087
- onClick={handleCollapseToAvatarOnly}
1088
- aria-label="收起会话列表"
1089
- />
1090
- </Tooltip>
1085
+ <Button
1086
+ type="button"
1087
+ variant="ghost-black"
1088
+ size="sm"
1089
+ icon={<Icon name="layout-right-stroked" size={16} />}
1090
+ iconOnly
1091
+ tooltip="收起会话列表"
1092
+ onClick={handleCollapseToAvatarOnly}
1093
+ aria-label="收起会话列表"
1094
+ />
1091
1095
  ) : null}
1092
1096
  <h2 className={TITLE}>{title}</h2>
1093
1097
  </div>
1094
1098
  {showActions ? (
1095
1099
  <div className={ACTIONS}>
1096
1100
  {showLayoutToggle ? (
1097
- <Tooltip content={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}>
1098
- <Button
1099
- type="button"
1100
- variant="ghost-black"
1101
- size="sm"
1102
- icon={<Icon name="switch-horizontal-01-stroked" size={16} />}
1103
- iconOnly
1104
- onClick={handleVariantToggle}
1105
- aria-label={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
1106
- />
1107
- </Tooltip>
1108
- ) : null}
1109
- <Tooltip content="搜索会话">
1110
1101
  <Button
1111
1102
  type="button"
1112
1103
  variant="ghost-black"
1113
1104
  size="sm"
1114
- icon={<Icon name="search-lg-stroked" size={16} />}
1105
+ icon={<Icon name="switch-horizontal-01-stroked" size={16} />}
1115
1106
  iconOnly
1116
- aria-label="搜索会话"
1107
+ tooltip={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
1108
+ onClick={handleVariantToggle}
1109
+ aria-label={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
1117
1110
  />
1118
- </Tooltip>
1119
- <Tooltip content="筛选会话">
1120
- <Button
1121
- type="button"
1122
- variant="ghost-black"
1123
- size="sm"
1124
- icon={<Icon name="filter-funnel-01-stroked" size={16} />}
1125
- iconOnly
1126
- aria-label="筛选会话"
1127
- />
1128
- </Tooltip>
1129
- <Tooltip content="查看工单文件">
1130
- <Button
1131
- type="button"
1132
- variant="ghost-black"
1133
- size="sm"
1134
- icon={<Icon name="file-05-stroked" size={16} />}
1135
- iconOnly
1136
- aria-label="查看工单文件"
1137
- />
1138
- </Tooltip>
1111
+ ) : null}
1112
+ <Button
1113
+ type="button"
1114
+ variant="ghost-black"
1115
+ size="sm"
1116
+ icon={<Icon name="search-lg-stroked" size={16} />}
1117
+ iconOnly
1118
+ tooltip="搜索会话"
1119
+ aria-label="搜索会话"
1120
+ />
1121
+ <Button
1122
+ type="button"
1123
+ variant="ghost-black"
1124
+ size="sm"
1125
+ icon={<Icon name="filter-funnel-01-stroked" size={16} />}
1126
+ iconOnly
1127
+ tooltip="筛选会话"
1128
+ aria-label="筛选会话"
1129
+ />
1130
+ <Button
1131
+ type="button"
1132
+ variant="ghost-black"
1133
+ size="sm"
1134
+ icon={<Icon name="file-05-stroked" size={16} />}
1135
+ iconOnly
1136
+ tooltip="查看工单文件"
1137
+ aria-label="查看工单文件"
1138
+ />
1139
1139
  </div>
1140
1140
  ) : null}
1141
1141
  </header>
@@ -63,7 +63,7 @@ export const CONVERSATION_LIST_TOKEN_MAP = {
63
63
  ],
64
64
  收起态: [
65
65
  { label: '列宽', cssProp: 'width', value: '88px' },
66
- { label: '行高', cssProp: 'height', value: '68px' },
66
+ { label: '行高', cssProp: 'height/min-height', value: '68px(固定高度,不参与 flex 压缩;运行时需同时具备 h-[68px] min-h-[68px] shrink-0)' },
67
67
  { label: '选中投影', cssProp: 'box-shadow', token: '--shadow-list', value: '0 0 16px 0 rgba(101,115,137,0.06)', state: 'active' },
68
68
  { label: '选中左条', cssProp: 'background', token: '--color-data-0', value: '#24CDA5', state: 'active' },
69
69
  ],
@@ -93,14 +93,16 @@ export const CONVERSATION_LIST_TOKEN_MAP = {
93
93
  { label: '消息区高度', cssProp: 'height', value: '155px(输入文案)', state: 'card-editable' },
94
94
  { label: '消息区横向内边距', cssProp: 'padding-inline', token: '--spacing-4', value: '16px', state: 'card' },
95
95
  { label: '消息区底部内边距', cssProp: 'padding-bottom', token: '--spacing-2', value: '8px', state: 'card' },
96
- { label: '消息区滚动', cssProp: 'overflow-y', value: 'auto', state: 'card' },
96
+ { label: '消息区滚动', cssProp: 'overflow-y', value: 'auto(固定高度内滚动查看与右侧 IM 同源的完整气泡消息流,不允许按条数截断)', state: 'card' },
97
97
  { label: '消息气泡纵向间距', cssProp: 'row-gap', token: '--spacing-3', value: '12px', state: 'card' },
98
98
  ],
99
99
  卡片消息气泡: [
100
100
  { label: '用户气泡背景', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', state: 'card-user' },
101
- { label: 'AI 气泡背景', cssProp: 'background-image', token: '--gradient-ai-fill-1', value: 'linear-gradient(90deg, rgba(230, 247, 244, 1) 0%, rgba(239, 246, 255, 1) 55%, rgba(243, 245, 255, 1) 90%, rgba(252, 243, 255, 1) 100%)', state: 'card-agent' },
101
+ { label: 'AI 气泡背景', cssProp: 'background-image', token: '--gradient-ai-fill-1', value: 'linear-gradient(90deg, rgba(230, 247, 244, 1) 0%, rgba(239, 246, 255, 1) 55%, rgba(243, 245, 255, 1) 90%, rgba(252, 243, 255, 1) 100%)', state: 'card-bot' },
102
+ { label: '人工客服气泡背景', cssProp: 'background', token: '--color-chat-outgoing', value: 'var(--bg-chat-outgoing)', state: 'card-agent' },
102
103
  { label: '气泡圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px(对角缺口按消息方向处理)', state: 'card' },
103
104
  { label: '气泡字号', cssProp: 'font-size', token: '--text-xs', value: '12px', state: 'card' },
105
+ { label: '与右侧聊天一致性', cssProp: 'content/style-source', value: '卡片气泡是右侧 IM 对话气泡的缩小版完整消息流;文案、角色、颜色语义必须同源一致。user -> grey,bot -> ai 渐变,agent -> default 浅青;允许降低字号或隐藏辅助元素,但不允许截断消息条数', state: 'card-preview' },
104
106
  ],
105
107
  卡片底部: [
106
108
  { label: '已回复内边距', cssProp: 'padding', token: '--spacing-4', value: '16px', state: 'replied' },
@@ -75,6 +75,7 @@ export default function FullScreenPage({
75
75
  icon={<ArrowLeft size={16} strokeWidth={2} />}
76
76
  iconOnly
77
77
  onClick={onBack}
78
+ tooltip="返回"
78
79
  aria-label="返回"
79
80
  className="shrink-0"
80
81
  data-tfds-component="FullScreenPage.Back"
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * 用于客服工作台 / 在线 Agent / 工单详情右侧信息区,将多个可切换的信息面板
5
5
  * 组织为“主栏 + 拆分栏”结构。组件只负责框架、拆分/合并和 Tabs,不规定 tabs 内容。
6
+ * tabs 来自业务数据动态配置:1 个分类单栏展示,2 个分类最多双栏,3 个及以上分类且宽度足够时最多三栏。
7
+ * 嵌入客服工作台框架时,必须保留整体信息栏拖拽、内部栏间拖拽、拆分/合并和最小宽度保护。
6
8
  */
7
9
 
8
10
  import { useEffect, useMemo, useRef, useState } from 'react';
@@ -10,7 +12,6 @@ import Button from './Button';
10
12
  import FormTitle from './FormTitle';
11
13
  import Icon from './Icon';
12
14
  import Tabs from './Tabs';
13
- import Tooltip from './Tooltip';
14
15
 
15
16
  const DEFAULT_PANELS = [
16
17
  {
@@ -307,20 +308,17 @@ function PanelHeader({
307
308
 
308
309
  {!useTitleFallback ? (
309
310
  <div className={ACTIONS}>
310
- <Tooltip content={splitActionTooltip} placement="top">
311
- <span className="inline-flex">
312
- <Button
313
- type="button"
314
- variant="ghost-black"
315
- size="md"
316
- icon={<Icon name={splitActionIcon} size={16} />}
317
- iconOnly
318
- disabled={splitActionDisabled}
319
- aria-label={splitActionLabel}
320
- onClick={onSplitAction}
321
- />
322
- </span>
323
- </Tooltip>
311
+ <Button
312
+ type="button"
313
+ variant="ghost-black"
314
+ size="md"
315
+ icon={<Icon name={splitActionIcon} size={16} />}
316
+ iconOnly
317
+ tooltip={splitActionTooltip}
318
+ disabled={splitActionDisabled}
319
+ aria-label={splitActionLabel}
320
+ onClick={onSplitAction}
321
+ />
324
322
  </div>
325
323
  ) : null}
326
324
  <span className={HEADER_LINE} aria-hidden="true" />
@@ -17,6 +17,7 @@ export const INFO_DISPLAY_PANEL_TOKEN_MAP = {
17
17
  分栏: [
18
18
  { label: '布局模型', cssProp: 'layout', value: '主栏 + 拆分栏;主栏永远在最右侧,拆分栏固定追加在主栏左侧' },
19
19
  { label: '最大栏数', cssProp: 'max-columns', value: '3 栏(1 个主栏 + 最多 2 个拆分栏)' },
20
+ { label: '动态拆分能力', cssProp: 'split-model', value: '按业务 tabs 动态支持 tab1 / tab2 / tab3:1 个分类单栏展示,2 个分类最多双栏,3 个及以上分类且宽度足够时最多三栏' },
20
21
  { label: 'Tab数量上限', cssProp: 'max-columns', value: '可见栏数同时受 tab 总数限制:1 个 tab 不拆分,2 个 tab 最多 2 栏,>= 3 个 tab 才允许 3 栏' },
21
22
  { label: '单栏最小宽度', cssProp: 'minPanelWidth', value: '200px' },
22
23
  { label: '双栏门槛', cssProp: 'container-width', value: '>= 401px(200px * 2 + 1px 分隔线)' },
@@ -24,6 +25,7 @@ export const INFO_DISPLAY_PANEL_TOKEN_MAP = {
24
25
  { label: '栏间分隔线', cssProp: 'border-left', token: 'border-default', value: '1px' },
25
26
  { label: '空间不足策略', cssProp: 'split-limit', value: '容器宽度 < 401px 时只允许单栏;401px-601px 允许双栏;>= 602px 才允许三栏。宽度不足时主栏拆分按钮进入 Button disabled 态' },
26
27
  { label: '拖拽边界', cssProp: 'resize-handle', value: '组件整体宽度由左外边界单独拖拽;组件内部仅栏与栏之间的分隔线支持拖拽调宽' },
28
+ { label: '客服工作台保留能力', cssProp: 'customer-service-rule', value: '嵌入客服工作台框架时,必须完整保留整体信息栏拖拽、内部栏间拖拽、拆分/合并、宽度门槛和最小栏宽保护' },
27
29
  { label: '拖拽最小宽度', cssProp: 'min-column-width', value: '每栏最小 200px' },
28
30
  { label: '预览验证', cssProp: 'preview-resize', value: '预览态默认撑满容器,并支持从组件左侧边缘拖拽整体宽度,用于验证单栏 / 双栏 / 三栏门槛' },
29
31
  ],
@@ -122,6 +122,7 @@ export default function Modal({
122
122
  icon={<X size={16} strokeWidth={2} />}
123
123
  iconOnly
124
124
  onClick={onClose}
125
+ tooltip="关闭"
125
126
  aria-label="关闭"
126
127
  className="shrink-0"
127
128
  />
@@ -94,6 +94,7 @@ export default function Sheet({
94
94
  icon={<X size={16} strokeWidth={2} />}
95
95
  iconOnly
96
96
  onClick={onClose}
97
+ tooltip="关闭"
97
98
  aria-label="关闭"
98
99
  className="shrink-0"
99
100
  />
@@ -607,6 +607,7 @@ function renderTextButtonValue(value) {
607
607
  iconOnly
608
608
  icon={<Icon name={iconName} size="sm" />}
609
609
  className={[ICON_ONLY_BUTTON_RESET, TEXT_BUTTON_ICON_CLASS, 'ml-[2px]'].join(' ')}
610
+ tooltip={typeof value === 'object' ? value.tooltip || value.ariaLabel || value.label || '单元格操作' : '单元格操作'}
610
611
  aria-label="单元格操作"
611
612
  onClick={onClick}
612
613
  />
@@ -720,6 +721,7 @@ function renderActionsValue(value) {
720
721
  icon={<Icon name={action.iconName || 'dots-horizontal-stroked'} size="sm" />}
721
722
  className={[ICON_ONLY_BUTTON_RESET, MORE_ACTION_BUTTON_CLASS].join(' ')}
722
723
  onClick={action.onClick}
724
+ tooltip={action.tooltip || action.ariaLabel || action.label || '更多操作'}
723
725
  aria-label={action.ariaLabel || action.label || '更多操作'}
724
726
  />
725
727
  );
@@ -755,6 +757,7 @@ function renderDragHandleValue(value) {
755
757
  iconOnly
756
758
  icon={<Icon name="dots-grid-stroked" size="sm" />}
757
759
  className={[ICON_ONLY_BUTTON_RESET, DRAG_HANDLE_BUTTON_CLASS].join(' ')}
760
+ tooltip="拖拽排序"
758
761
  aria-label="拖拽排序"
759
762
  onClick={onClick}
760
763
  />
@@ -888,6 +891,7 @@ function CardFormActions({ record, context, onView, onMore }) {
888
891
  variant="ghost-black"
889
892
  iconOnly
890
893
  icon={<Icon name="dots-horizontal-stroked" />}
894
+ tooltip="更多"
891
895
  aria-label="更多"
892
896
  onClick={() => onMore?.(record, context)}
893
897
  />
@@ -956,6 +960,7 @@ function CardFormItem({
956
960
  variant="ghost-black"
957
961
  iconOnly
958
962
  icon={<Icon name="chevron-right-stroked" />}
963
+ tooltip={expanded ? '收起子项' : '展开子项'}
959
964
  aria-label={expanded ? '收起子项' : '展开子项'}
960
965
  aria-expanded={hasChildren ? expanded : undefined}
961
966
  onClick={(event) => {
@@ -1169,6 +1174,7 @@ export default function Table({
1169
1174
  className={[ICON_ONLY_BUTTON_RESET, PAGE_ARROW_BUTTON_CLASS].join(' ')}
1170
1175
  onClick={() => handlePageChange(safeCurrent - 1)}
1171
1176
  disabled={safeCurrent <= 1}
1177
+ tooltip="上一页"
1172
1178
  aria-label="上一页"
1173
1179
  data-tfds-component="Table.Pagination"
1174
1180
  />
@@ -1207,6 +1213,7 @@ export default function Table({
1207
1213
  className={[ICON_ONLY_BUTTON_RESET, PAGE_ARROW_BUTTON_CLASS].join(' ')}
1208
1214
  onClick={() => handlePageChange(safeCurrent + 1)}
1209
1215
  disabled={safeCurrent >= totalPages}
1216
+ tooltip="下一页"
1210
1217
  aria-label="下一页"
1211
1218
  data-tfds-component="Table.Pagination"
1212
1219
  />
@@ -12,12 +12,22 @@
12
12
  * @prop {number} [activeIndex=0] — 受控:当前选中索引
13
13
  * @prop {number} [defaultIndex=0] — 非受控初始索引
14
14
  * @prop {function} [onChange=null] — `(index) => void` 切换回调
15
+ * @prop {'auto'|'visible'} [overflow='auto'] — 宽度不足时默认横向滚动,禁止撑破卡片/页面容器
15
16
  * @prop {string} [className=''] — 根节点类名
16
17
  */
17
18
  import { useState } from 'react';
18
19
 
19
20
  /* ── 容器样式 ── */
20
- const CONTAINER_BASE = 'inline-flex items-center relative w-fit';
21
+ const VIEWPORT_BASE = 'tfds-tabs block max-w-full min-w-0';
22
+ const VIEWPORT_OVERFLOW = {
23
+ auto: [
24
+ 'overflow-x-auto overflow-y-hidden overscroll-x-contain',
25
+ '[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden',
26
+ ].join(' '),
27
+ visible: 'overflow-visible',
28
+ };
29
+
30
+ const CONTAINER_BASE = 'inline-flex w-max max-w-none items-center relative';
21
31
 
22
32
  const CONTAINER_VARIANT = {
23
33
  line: 'gap-2 bg-transparent border-b border-b-border',
@@ -98,27 +108,59 @@ export default function Tabs({
98
108
  activeIndex,
99
109
  defaultIndex = 0,
100
110
  onChange,
111
+ overflow = 'auto',
101
112
  className = '',
102
113
  ...rest
103
114
  }) {
104
115
  const [internalIndex, setInternalIndex] = useState(defaultIndex);
105
116
  const isControlled = activeIndex !== undefined;
106
117
  const currentIndex = isControlled ? (activeIndex ?? 0) : internalIndex;
118
+ const tabs = items ?? [];
107
119
 
108
120
  const handleClick = (index) => {
109
121
  if (!isControlled) setInternalIndex(index);
110
122
  onChange?.(index);
111
123
  };
112
124
 
125
+ const handleKeyDown = (event) => {
126
+ const lastIndex = tabs.length - 1;
127
+ if (lastIndex < 0) return;
128
+
129
+ const nextIndexMap = {
130
+ ArrowRight: Math.min(currentIndex + 1, lastIndex),
131
+ ArrowLeft: Math.max(currentIndex - 1, 0),
132
+ Home: 0,
133
+ End: lastIndex,
134
+ };
135
+ const nextIndex = nextIndexMap[event.key];
136
+ if (nextIndex === undefined || nextIndex === currentIndex) return;
137
+
138
+ event.preventDefault();
139
+ handleClick(nextIndex);
140
+ event.currentTarget.querySelectorAll('[role="tab"]')[nextIndex]?.focus();
141
+ };
142
+
113
143
  const containerCls = [
114
144
  CONTAINER_BASE,
115
145
  CONTAINER_VARIANT[variant],
146
+ ].filter(Boolean).join(' ');
147
+
148
+ const viewportCls = [
149
+ VIEWPORT_BASE,
150
+ VIEWPORT_OVERFLOW[overflow] || VIEWPORT_OVERFLOW.auto,
116
151
  className,
117
152
  ].filter(Boolean).join(' ');
118
153
 
119
154
  return (
120
- <div className={[`tfds-tabs`, containerCls].filter(Boolean).join(' ')} role="tablist" {...rest} data-tfds-component="Tabs">
121
- {(items ?? []).map((item, index) => {
155
+ <div
156
+ className={viewportCls}
157
+ role="tablist"
158
+ onKeyDown={handleKeyDown}
159
+ {...rest}
160
+ data-tfds-component="Tabs"
161
+ >
162
+ <div className={containerCls}>
163
+ {tabs.map((item, index) => {
122
164
  const isActive = index === currentIndex;
123
165
  const itemCls = [
124
166
  ITEM_BASE,
@@ -144,6 +186,7 @@ export default function Tabs({
144
186
  </button>
145
187
  );
146
188
  })}
189
+ </div>
147
190
  </div>
148
191
  );
149
192
  }
@@ -5,6 +5,9 @@
5
5
  export const TABS_TOKEN_MAP = {
6
6
  base: [
7
7
  { label: '默认尺寸', cssProp: 'size', value: 'SM(内容区 / 卡片内 / 表单分段 / 筛选维度默认)' },
8
+ { label: '宽度适配', cssProp: 'overflow-x', value: 'auto(默认启用横向滚动;Tabs 不允许撑破卡片 / 页面容器)' },
9
+ { label: '容器上限', cssProp: 'max-width', value: '100%(根节点必须 max-w-full + min-w-0)' },
10
+ { label: '内容宽度', cssProp: 'width', value: 'w-max(内层按 tab 内容自然宽度排列,外层负责裁切与滚动)' },
8
11
  { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
9
12
  { label: '行高', cssProp: 'line-height', value: '20px' },
10
13
  { label: '图标尺寸', cssProp: 'width/height', value: '16px' },
@@ -566,6 +566,7 @@ function BusinessSwitcher({
566
566
  iconOnly
567
567
  icon={<Icon name="switch-horizontal-01-stroked" size="xs" className="text-foreground-muted" aria-hidden="true" />}
568
568
  className="shrink-0"
569
+ tooltip="切换业务线"
569
570
  aria-label="切换业务线"
570
571
  aria-haspopup="menu"
571
572
  aria-expanded={menuOpen}
@@ -1097,6 +1098,7 @@ export default function TagBar({
1097
1098
  radius="rounded"
1098
1099
  iconOnly
1099
1100
  icon={<Icon name="layout-left-stroked" size="sm" aria-hidden="true" />}
1101
+ tooltip={isCollapsed ? '展开标签栏' : '收起标签栏'}
1100
1102
  aria-label={isCollapsed ? '展开标签栏' : '收起标签栏'}
1101
1103
  onClick={() => updateCollapsed(!isCollapsed)}
1102
1104
  data-tfds-component="TagBar.CollapseToggle"
@@ -111,6 +111,7 @@ export default function Toast({
111
111
  icon={<X size={16} strokeWidth={2} />}
112
112
  iconOnly
113
113
  onClick={onClose}
114
+ tooltip="关闭"
114
115
  aria-label="关闭"
115
116
  className="shrink-0 border-transparent bg-transparent shadow-none"
116
117
  />
@@ -413,6 +413,7 @@ export const Upload = ({
413
413
  radius="rounded"
414
414
  iconOnly
415
415
  icon={<Icon name="refresh-cw-01-stroked" size="xs" />}
416
+ tooltip="重试上传"
416
417
  onClick={(e) => {
417
418
  e.stopPropagation();
418
419
  retryItem(fileItem);