@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.
- package/package.json +1 -1
- package/skills/tfds/CHECKLIST.md +5 -0
- package/skills/tfds/COMMON_FAILURES.md +48 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +5 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +31 -0
- package/skills/tfds/LAYOUT_RULES.md +31 -0
- package/skills/tfds/components.index.json +75 -27
- package/skills/tfds/components.summary.json +13 -13
- package/src/_b_end_runtime/components/Card.jsx +151 -13
- package/src/_b_end_runtime/components/Card.tokens.js +27 -3
- package/src/_b_end_runtime/components/CardPreview.jsx +11 -3
- package/src/_b_end_runtime/components/ChatMessage.jsx +59 -1
- package/src/_b_end_runtime/components/ConversationList.jsx +68 -68
- package/src/_b_end_runtime/components/ConversationList.tokens.js +5 -3
- package/src/_b_end_runtime/components/FullScreenPage.jsx +1 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +13 -15
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +2 -0
- package/src/_b_end_runtime/components/Modal.jsx +1 -0
- package/src/_b_end_runtime/components/Sheet.jsx +1 -0
- package/src/_b_end_runtime/components/Table.jsx +7 -0
- package/src/_b_end_runtime/components/Tabs.jsx +46 -3
- package/src/_b_end_runtime/components/Tabs.tokens.js +3 -0
- package/src/_b_end_runtime/components/TagBar.jsx +2 -0
- package/src/_b_end_runtime/components/Toast.jsx +1 -0
- package/src/_b_end_runtime/components/Upload.jsx +1 -0
- package/src/_b_end_runtime/components.js +24 -11
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +548 -135
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +1 -1
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +6 -6
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +66 -5
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +50 -17
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +28 -78
- package/src/_b_end_runtime/patterns.js +32 -21
- package/src/_b_end_runtime/preview-registry.jsx +20 -4
- 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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
<
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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="
|
|
1105
|
+
icon={<Icon name="switch-horizontal-01-stroked" size={16} />}
|
|
1115
1106
|
iconOnly
|
|
1116
|
-
|
|
1107
|
+
tooltip={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
|
|
1108
|
+
onClick={handleVariantToggle}
|
|
1109
|
+
aria-label={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
|
|
1117
1110
|
/>
|
|
1118
|
-
|
|
1119
|
-
<
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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-
|
|
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' },
|
|
@@ -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
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
],
|
|
@@ -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
|
|
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
|
|
121
|
-
{
|
|
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"
|