@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.
- package/AI_READ_FIRST.md +131 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/package.json +67 -0
- package/scripts/check-tfds-contract.mjs +334 -0
- package/scripts/check-tfds-integration.mjs +263 -0
- package/scripts/postinstall-cursor-skill.mjs +382 -0
- package/scripts/setup.mjs +520 -0
- package/skills/tfds/CHECKLIST.md +205 -0
- package/skills/tfds/COMMON_FAILURES.md +238 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
- package/skills/tfds/LAYOUT_RECIPES.md +140 -0
- package/skills/tfds/LAYOUT_RULES.md +1355 -0
- package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
- package/skills/tfds/SKILL.md +188 -0
- package/skills/tfds/components.index.json +7305 -0
- package/skills/tfds/components.summary.json +1809 -0
- package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
- package/src/_b_end_runtime/components/Avatar.jsx +325 -0
- package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
- package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
- package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
- package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
- package/src/_b_end_runtime/components/Button.jsx +144 -0
- package/src/_b_end_runtime/components/Button.tokens.js +90 -0
- package/src/_b_end_runtime/components/Card.jsx +460 -0
- package/src/_b_end_runtime/components/Card.tokens.js +124 -0
- package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
- package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
- package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
- package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
- package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
- package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
- package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
- package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
- package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
- package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
- package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
- package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
- package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
- package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
- package/src/_b_end_runtime/components/Empty.jsx +141 -0
- package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
- package/src/_b_end_runtime/components/Form.jsx +609 -0
- package/src/_b_end_runtime/components/Form.tokens.js +77 -0
- package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
- package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
- package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
- package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
- package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
- package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
- package/src/_b_end_runtime/components/Icon.jsx +172 -0
- package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
- package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
- package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
- package/src/_b_end_runtime/components/Input.jsx +258 -0
- package/src/_b_end_runtime/components/Input.tokens.js +68 -0
- package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
- package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
- package/src/_b_end_runtime/components/Modal.jsx +155 -0
- package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
- package/src/_b_end_runtime/components/NavBar.jsx +842 -0
- package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
- package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
- package/src/_b_end_runtime/components/Radio.jsx +227 -0
- package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
- package/src/_b_end_runtime/components/Select.jsx +766 -0
- package/src/_b_end_runtime/components/Select.tokens.js +99 -0
- package/src/_b_end_runtime/components/Sheet.jsx +132 -0
- package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
- package/src/_b_end_runtime/components/Slider.jsx +346 -0
- package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
- package/src/_b_end_runtime/components/Switch.jsx +124 -0
- package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
- package/src/_b_end_runtime/components/Table.jsx +1338 -0
- package/src/_b_end_runtime/components/Table.tokens.js +147 -0
- package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
- package/src/_b_end_runtime/components/Tabs.jsx +149 -0
- package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
- package/src/_b_end_runtime/components/Tag.jsx +199 -0
- package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
- package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
- package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
- package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
- package/src/_b_end_runtime/components/TagInput.jsx +382 -0
- package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
- package/src/_b_end_runtime/components/TextArea.jsx +363 -0
- package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
- package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
- package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
- package/src/_b_end_runtime/components/Toast.jsx +120 -0
- package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
- package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
- package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
- package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
- package/src/_b_end_runtime/components/Upload.jsx +455 -0
- package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
- package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
- package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
- package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
- package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
- package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
- package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
- package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
- package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
- package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
- package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
- package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
- package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
- package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
- package/src/_b_end_runtime/components/tagShared.js +3 -0
- package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
- package/src/_b_end_runtime/components.js +3499 -0
- package/src/_b_end_runtime/index.js +9 -0
- package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
- package/src/_b_end_runtime/patterns.js +428 -0
- package/src/_b_end_runtime/preview-registry.jsx +4719 -0
- package/src/_b_end_runtime/teamMembers.js +56 -0
- package/src/_b_end_runtime/tokens.js +500 -0
- package/src/index.d.ts +1073 -0
- package/src/index.js +52 -0
- package/theme.css +350 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select — TOKEN_MAP(供平台属性面板展示)
|
|
3
|
+
*/
|
|
4
|
+
export const SELECT_TOKEN_MAP = {
|
|
5
|
+
base: [],
|
|
6
|
+
已选文字: [
|
|
7
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
8
|
+
{ label: '行高', cssProp: 'line-height', value: '20px' },
|
|
9
|
+
],
|
|
10
|
+
占位符: [
|
|
11
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
12
|
+
],
|
|
13
|
+
触发器: [
|
|
14
|
+
{ label: '触发器宽度', cssProp: 'width', value: 'w-full min-w-0(随父级;示意 300px 时外包 w-[300px])' },
|
|
15
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
|
|
16
|
+
{ label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
17
|
+
{ label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
|
|
18
|
+
{ label: '悬浮边框', cssProp: 'border-color', token: '--color-blueGrey-400', value: '#D0D5DD', state: 'hover' },
|
|
19
|
+
{ label: '展开边框', cssProp: 'border-color', token: '--color-primary', value: '#56D3BC', semanticRef: 'status-primary', state: 'open' },
|
|
20
|
+
{ label: '错态背景', cssProp: 'background', token: '--color-red-50', value: '#FEF2F1', semanticRef: 'status-danger', state: 'error' },
|
|
21
|
+
{ label: '错态悬浮', cssProp: 'background', token: '--color-red-100', value: '#FCD9D7', state: 'error+hover' },
|
|
22
|
+
{ label: '错态展开', cssProp: 'border-color', token: '--color-red-500', value: '#F74331', semanticRef: 'status-danger', state: 'error+open' },
|
|
23
|
+
{ label: '禁用背景', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
|
|
24
|
+
{ label: '透明度', cssProp: 'opacity', value: '0.6', state: 'disabled' },
|
|
25
|
+
],
|
|
26
|
+
标签选择: [
|
|
27
|
+
{ label: '分类归属', cssProp: 'mode', value: 'tag 属于 Select 的标签选择分类' },
|
|
28
|
+
{ label: '已选标签', cssProp: 'component', value: 'Tag,默认规格与 TagInput 保持一致;可按 option.variant 覆盖颜色' },
|
|
29
|
+
{ label: '折叠项', cssProp: 'component', value: 'Tag children="+N",沿用 TagInput 默认规格,宽度不足时显示' },
|
|
30
|
+
{ label: '更多浮窗', cssProp: 'component', value: 'Tooltip tone=light,仅展示被折叠的标签' },
|
|
31
|
+
{ label: '标签间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
32
|
+
{ label: '下拉项高度', cssProp: 'min-height', token: '--size-control-md', value: '36px' },
|
|
33
|
+
{ label: '下拉项内容', cssProp: 'component', value: 'Tag' },
|
|
34
|
+
],
|
|
35
|
+
下拉箭头: [
|
|
36
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
37
|
+
{ label: '尺寸', cssProp: 'width / height', value: '16px' },
|
|
38
|
+
],
|
|
39
|
+
清除按钮: [
|
|
40
|
+
{ label: '尺寸', cssProp: 'width / height', value: '16px' },
|
|
41
|
+
{ label: '常态色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled' },
|
|
42
|
+
{ label: '悬停色', cssProp: 'color', token: '--color-foreground-secondary', value: '#475467', state: 'hover', semanticRef: 'text-secondary' },
|
|
43
|
+
],
|
|
44
|
+
AI推荐列表: [
|
|
45
|
+
{ label: '上间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
46
|
+
{ label: '列表圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
|
|
47
|
+
{ label: '列表内边距', cssProp: 'padding', token: '--spacing-1', value: '4px' },
|
|
48
|
+
{ label: '条目间距', cssProp: 'gap', token: '--spacing-0_5', value: '2px' },
|
|
49
|
+
{ label: '列表背景', cssProp: 'background', 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%)' },
|
|
50
|
+
{ label: '条目圆角', cssProp: 'border-radius', token: '--radius-sm', value: '6px' },
|
|
51
|
+
{ label: '条目横向内边距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
|
|
52
|
+
{ label: '条目纵向内边距', cssProp: 'padding-block', token: '--spacing-1', value: '4px' },
|
|
53
|
+
{ label: 'Hover 背景', cssProp: 'background', token: '--gradient-ai-fill-2', value: 'linear-gradient(-45deg, rgba(254, 224, 253, 1) 0%, rgba(233, 218, 255, 1) 25%, rgba(213, 225, 255, 1) 48%, rgba(213, 243, 248, 1) 83%, rgba(213, 247, 242, 1) 100%)', state: 'hover' },
|
|
54
|
+
{ label: '文字颜色', cssProp: 'color', token: '--color-foreground-secondary', value: '#475467', semanticRef: 'text-secondary' },
|
|
55
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
56
|
+
{ label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
|
|
57
|
+
],
|
|
58
|
+
刷新推荐按钮: [
|
|
59
|
+
{ label: '图标尺寸', cssProp: 'width / height', token: '--spacing-4', value: '16px' },
|
|
60
|
+
{ label: '图标颜色', cssProp: 'background-image', token: '--gradient-ai-fill-3', value: 'linear-gradient(-45deg, rgba(255, 153, 248, 1) 0%, rgba(181, 131, 255, 1) 25%, rgba(114, 156, 255, 1) 48%, rgba(117, 218, 231, 1) 83%, rgba(115, 230, 204, 1) 100%)', semanticRef: 'ai-fill-3' },
|
|
61
|
+
],
|
|
62
|
+
下拉面板: [
|
|
63
|
+
{ label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
64
|
+
{ label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
|
|
65
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
|
|
66
|
+
{ label: '最大高度', cssProp: 'max-height', value: '240px' },
|
|
67
|
+
{ label: '阴影', cssProp: 'box-shadow', value: 'shadow-lg(与 Modal / TimePicker 浮层一致)' },
|
|
68
|
+
{ label: '竖向内距', cssProp: 'padding-block', token: '--spacing-1', value: '4px' },
|
|
69
|
+
],
|
|
70
|
+
选项: [
|
|
71
|
+
{ label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
72
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
73
|
+
{ label: '左右内距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
|
|
74
|
+
{ label: '上下内距', cssProp: 'padding-block', token: '--spacing-2', value: '8px' },
|
|
75
|
+
{ label: '悬浮背景', cssProp: 'background', token: '--color-blueGrey-50', value: '#FCFCFD', state: 'hover' },
|
|
76
|
+
{ label: '选中背景', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', semanticRef: 'status-primary.bg', state: 'active' },
|
|
77
|
+
{ label: '选中文字', cssProp: 'color', token: '--color-brand-500', value: '#56D3BC', semanticRef: 'status-primary', state: 'active' },
|
|
78
|
+
{ label: '禁用文字', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled', state: 'disabled' },
|
|
79
|
+
{ label: '透明度', cssProp: 'opacity', value: '0.5', state: 'disabled' },
|
|
80
|
+
],
|
|
81
|
+
空文案: [
|
|
82
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
83
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
84
|
+
],
|
|
85
|
+
sizes: {
|
|
86
|
+
sm: [
|
|
87
|
+
{ label: '最小高度', cssProp: 'min-height', value: '24px' },
|
|
88
|
+
{ label: '水平内距', cssProp: 'padding-inline', token: '--spacing-2', value: '8px' },
|
|
89
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-xs', value: '12px' },
|
|
90
|
+
{ label: '元素间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
91
|
+
],
|
|
92
|
+
md: [
|
|
93
|
+
{ label: '最小高度', cssProp: 'min-height', value: '36px' },
|
|
94
|
+
{ label: '水平内距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
|
|
95
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
96
|
+
{ label: '元素间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import Button from './Button';
|
|
4
|
+
|
|
5
|
+
/* ── 尺寸 → 面板宽度 ── */
|
|
6
|
+
const SIZE_CLASS = {
|
|
7
|
+
sm: 'w-[400px]',
|
|
8
|
+
md: 'w-[560px]',
|
|
9
|
+
lg: 'w-[800px]',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/* ── 面板容器 ── */
|
|
13
|
+
const PANEL = [
|
|
14
|
+
'flex flex-col items-stretch',
|
|
15
|
+
'bg-surface h-full px-6',
|
|
16
|
+
'shadow-xl',
|
|
17
|
+
].join(' ');
|
|
18
|
+
|
|
19
|
+
/* ── 头部 / 内容 / 底部 ── */
|
|
20
|
+
const HEADER = 'flex shrink-0 w-full items-start justify-between gap-2 py-6';
|
|
21
|
+
const TITLE = 'm-0 text-lg [font-weight:var(--font-semibold)] leading-6 text-foreground';
|
|
22
|
+
const SUBTITLE = 'm-0 text-sm font-normal leading-6 text-foreground-muted';
|
|
23
|
+
|
|
24
|
+
const BODY = 'flex-1 min-h-0 w-full overflow-y-auto';
|
|
25
|
+
|
|
26
|
+
const FOOTER = 'flex shrink-0 w-full flex-col py-6';
|
|
27
|
+
const FOOTER_ROW =
|
|
28
|
+
'flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2';
|
|
29
|
+
const FOOTER_HINT =
|
|
30
|
+
'm-0 min-w-[7.5rem] flex-1 text-xs font-normal leading-4 text-foreground-secondary';
|
|
31
|
+
const ACTIONS = 'flex shrink-0 items-center justify-end gap-3';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sheet — 侧边抽屉面板(仅面板本体,遮罩与滑入动画由外层处理)
|
|
35
|
+
* @prop {'sm'|'md'|'lg'} [size='md'] — 宽度档位
|
|
36
|
+
* @prop {boolean} [showFooterHint=true] — 是否显示底部提示
|
|
37
|
+
* @prop {string} [title='侧边标题'] — 标题
|
|
38
|
+
* @prop {string|null} [subtitle='侧边副标题'] — 副标题,为 null 时不展示
|
|
39
|
+
* @prop {string} [footerHint='提示信息'] — 底部提示文案
|
|
40
|
+
* @prop {string} [cancelText='取消'] — 取消按钮文案
|
|
41
|
+
* @prop {string} [confirmText='确定'] — 确定按钮文案
|
|
42
|
+
* @prop {function} [onClose=null] — 关闭回调,传入才显示关闭按钮
|
|
43
|
+
* @prop {function} [onCancel=null] — 取消回调,传入才显示取消按钮
|
|
44
|
+
* @prop {function} [onConfirm=null] — 确定回调,传入才显示确定按钮
|
|
45
|
+
* @prop {ReactNode} [footer=null] — 自定义底栏,传入则替换默认底栏
|
|
46
|
+
* @prop {string} [className=''] — 附加类名
|
|
47
|
+
* @prop {object} [style] — 内联样式
|
|
48
|
+
*/
|
|
49
|
+
export default function Sheet({
|
|
50
|
+
size = 'md',
|
|
51
|
+
title = '侧边标题',
|
|
52
|
+
subtitle = '侧边副标题',
|
|
53
|
+
children,
|
|
54
|
+
showFooterHint = true,
|
|
55
|
+
footerHint = '提示信息',
|
|
56
|
+
cancelText = '取消',
|
|
57
|
+
confirmText = '确定',
|
|
58
|
+
onCancel,
|
|
59
|
+
onConfirm,
|
|
60
|
+
onClose,
|
|
61
|
+
footer,
|
|
62
|
+
className = '',
|
|
63
|
+
style,
|
|
64
|
+
}) {
|
|
65
|
+
const uid = useId();
|
|
66
|
+
const titleId = `${uid}-title`;
|
|
67
|
+
const subtitleId = `${uid}-subtitle`;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className={[`tfds-sheet`, [PANEL, SIZE_CLASS[size], className].filter(Boolean).join(' ')].filter(Boolean).join(' ')}
|
|
72
|
+
style={style}
|
|
73
|
+
role="dialog"
|
|
74
|
+
aria-modal="true"
|
|
75
|
+
aria-labelledby={titleId}
|
|
76
|
+
{...(subtitle ? { 'aria-describedby': subtitleId } : {})}
|
|
77
|
+
data-tfds-component="Sheet">
|
|
78
|
+
<header className={HEADER}>
|
|
79
|
+
<div className="flex min-w-0 flex-1 flex-col items-start gap-0">
|
|
80
|
+
<h2 id={titleId} className={TITLE}>
|
|
81
|
+
{title}
|
|
82
|
+
</h2>
|
|
83
|
+
{subtitle ? (
|
|
84
|
+
<p id={subtitleId} className={SUBTITLE}>
|
|
85
|
+
{subtitle}
|
|
86
|
+
</p>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
{onClose ? (
|
|
90
|
+
<Button
|
|
91
|
+
type="button"
|
|
92
|
+
variant="ghost-black"
|
|
93
|
+
size="sm"
|
|
94
|
+
icon={<X size={16} strokeWidth={2} />}
|
|
95
|
+
iconOnly
|
|
96
|
+
onClick={onClose}
|
|
97
|
+
aria-label="关闭"
|
|
98
|
+
className="shrink-0"
|
|
99
|
+
/>
|
|
100
|
+
) : null}
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<div className={BODY}>{children}</div>
|
|
104
|
+
|
|
105
|
+
{footer != null ? (
|
|
106
|
+
<div className={FOOTER}>{footer}</div>
|
|
107
|
+
) : (
|
|
108
|
+
<footer className={FOOTER}>
|
|
109
|
+
<div className={FOOTER_ROW}>
|
|
110
|
+
{showFooterHint && footerHint ? (
|
|
111
|
+
<p className={FOOTER_HINT}>{footerHint}</p>
|
|
112
|
+
) : (
|
|
113
|
+
<span className="min-w-0 flex-1" />
|
|
114
|
+
)}
|
|
115
|
+
<div className={ACTIONS}>
|
|
116
|
+
{onCancel ? (
|
|
117
|
+
<Button type="button" variant="outline-black" size="md" onClick={onCancel}>
|
|
118
|
+
{cancelText}
|
|
119
|
+
</Button>
|
|
120
|
+
) : null}
|
|
121
|
+
{onConfirm ? (
|
|
122
|
+
<Button type="button" variant="primary" size="md" onClick={onConfirm}>
|
|
123
|
+
{confirmText}
|
|
124
|
+
</Button>
|
|
125
|
+
) : null}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</footer>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sheet — TOKEN_MAP(供平台属性面板展示)
|
|
3
|
+
*
|
|
4
|
+
* 分组顺序:文本元素 → 容器区域 → 尺寸 → 引用组件
|
|
5
|
+
* 大标题 → 描述文案 → 底部提示 → 标题栏 → 内容区 → 操作栏 → 面板 → sizes → 引用组件
|
|
6
|
+
*/
|
|
7
|
+
export const SHEET_TOKEN_MAP = {
|
|
8
|
+
base: [],
|
|
9
|
+
大标题: [
|
|
10
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
11
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-lg', value: '18px' },
|
|
12
|
+
{ label: '字重', cssProp: 'font-weight', token: '--font-semibold', value: '600(运行时使用 [font-weight:var(--font-semibold)])' },
|
|
13
|
+
{ label: '行高', cssProp: 'line-height', token: '--spacing-6', value: '24px' },
|
|
14
|
+
],
|
|
15
|
+
描述文案: [
|
|
16
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
17
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
18
|
+
{ label: '字重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
19
|
+
{ label: '行高', cssProp: 'line-height', token: '--spacing-6', value: '24px' },
|
|
20
|
+
],
|
|
21
|
+
底部提示: [
|
|
22
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground-secondary', value: '#475467', semanticRef: 'text-secondary' },
|
|
23
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-xs', value: '12px' },
|
|
24
|
+
{ label: '字重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
25
|
+
{ label: '行高', cssProp: 'line-height', token: '--spacing-4', value: '16px' },
|
|
26
|
+
],
|
|
27
|
+
标题栏: [
|
|
28
|
+
{ label: '上下间距', cssProp: 'padding-block', token: '--spacing-6', value: '24px' },
|
|
29
|
+
{ label: '元素间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
|
|
30
|
+
],
|
|
31
|
+
内容区: [
|
|
32
|
+
{ label: '溢出滚动', cssProp: 'overflow-y', value: 'auto' },
|
|
33
|
+
],
|
|
34
|
+
操作栏: [
|
|
35
|
+
{ label: '上下间距', cssProp: 'padding-block', token: '--spacing-6', value: '24px' },
|
|
36
|
+
{ label: '按钮间距', cssProp: 'gap', token: '--spacing-3', value: '12px' },
|
|
37
|
+
{ label: '行间距', cssProp: 'gap', token: '--spacing-6', value: '24px' },
|
|
38
|
+
],
|
|
39
|
+
面板: [
|
|
40
|
+
{ label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
41
|
+
{ label: '投影', cssProp: 'box-shadow', token: '--shadow-xl', value: '0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)' },
|
|
42
|
+
{ label: '左右间距', cssProp: 'padding-inline', token: '--spacing-6', value: '24px' },
|
|
43
|
+
{ label: '高度', cssProp: 'height', value: '100%' },
|
|
44
|
+
],
|
|
45
|
+
sizes: {
|
|
46
|
+
sm: [
|
|
47
|
+
{ label: '宽度', cssProp: 'width', value: '400px' },
|
|
48
|
+
],
|
|
49
|
+
md: [
|
|
50
|
+
{ label: '宽度', cssProp: 'width', value: '560px' },
|
|
51
|
+
],
|
|
52
|
+
lg: [
|
|
53
|
+
{ label: '宽度', cssProp: 'width', value: '800px' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
引用组件: [
|
|
57
|
+
{ label: '关闭按钮', cssProp: '—', value: 'Button ghost-black sm iconOnly' },
|
|
58
|
+
{ label: '取消按钮', cssProp: '—', value: 'Button outline-black md' },
|
|
59
|
+
{ label: '确定按钮', cssProp: '—', value: 'Button primary md' },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider — 滑动输入条
|
|
3
|
+
* @prop {number} [min=0] — 最小值
|
|
4
|
+
* @prop {number} [max=100] — 最大值
|
|
5
|
+
* @prop {number} [step=1] — 步长
|
|
6
|
+
* @prop {Array} value — 当前值(受控)
|
|
7
|
+
* @prop {Array} [defaultValue=[20,80]] — 默认值
|
|
8
|
+
* @prop {boolean} [disabled=false] — 是否禁用
|
|
9
|
+
* @prop {boolean} [showTooltip=true] — 是否在拖动时显示提示
|
|
10
|
+
* @prop {*} [marks=null] — 刻度标记
|
|
11
|
+
* @prop {function} [onChange=null] — 变化回调
|
|
12
|
+
* @prop {function} [onAfterChange=null] — 松手回调
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
+
import Tooltip from './Tooltip';
|
|
17
|
+
|
|
18
|
+
/* ── 容器与轨道 ── */
|
|
19
|
+
const WRAPPER_BASE = 'relative w-full min-w-0 max-w-full select-none';
|
|
20
|
+
const TRACK_AREA = 'relative h-[32px]';
|
|
21
|
+
const TRACK = 'absolute left-0 right-0 top-1/2 h-[4px] -translate-y-1/2 rounded-full bg-fill';
|
|
22
|
+
const TRACK_ACTIVE = 'absolute top-1/2 h-[4px] -translate-y-1/2 rounded-full bg-brand-500';
|
|
23
|
+
const TRACK_ACTIVE_DISABLED = 'absolute top-1/2 h-[4px] -translate-y-1/2 rounded-full bg-blueGrey-300';
|
|
24
|
+
|
|
25
|
+
/* ── 滑块 ── */
|
|
26
|
+
const HANDLE_BASE = [
|
|
27
|
+
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
|
|
28
|
+
'inline-flex size-[20px] items-center justify-center rounded-full',
|
|
29
|
+
'border-2 shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
|
30
|
+
'transition-[box-shadow,border-color,background-color] duration-120',
|
|
31
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-200 focus-visible:ring-offset-1',
|
|
32
|
+
].join(' ');
|
|
33
|
+
|
|
34
|
+
const HANDLE_ENABLED = [
|
|
35
|
+
'bg-white border-border-brand',
|
|
36
|
+
'hover:border-border-brand-strong',
|
|
37
|
+
'active:border-brand-700',
|
|
38
|
+
'cursor-grab',
|
|
39
|
+
].join(' ');
|
|
40
|
+
|
|
41
|
+
const HANDLE_DRAGGING = 'cursor-grabbing shadow-[0_0_0_4px_rgba(86,211,188,0.15)]';
|
|
42
|
+
|
|
43
|
+
const HANDLE_DISABLED = 'bg-blueGrey-300 border-blueGrey-300 cursor-not-allowed';
|
|
44
|
+
|
|
45
|
+
/* ── 提示与刻度 ── */
|
|
46
|
+
const TOOLTIP_TRIGGER = 'absolute inset-0';
|
|
47
|
+
const MARK_DOT_BASE = 'block size-[4px] rounded-full';
|
|
48
|
+
const MARK_LABEL = 'absolute left-1/2 top-full mt-[12px] -translate-x-1/2 text-[12px] leading-[18px] text-blueGrey-600 whitespace-nowrap';
|
|
49
|
+
|
|
50
|
+
function clamp(v, min, max) {
|
|
51
|
+
return Math.min(max, Math.max(min, v));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getPrecision(step) {
|
|
55
|
+
const text = String(step);
|
|
56
|
+
const dot = text.indexOf('.');
|
|
57
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function snap(raw, min, max, step) {
|
|
61
|
+
const safeStep = Number.isFinite(step) && step > 0 ? step : 1;
|
|
62
|
+
const precision = getPrecision(safeStep);
|
|
63
|
+
const base = Math.round(((raw - min) / safeStep)) * safeStep + min;
|
|
64
|
+
const rounded = Number(base.toFixed(precision));
|
|
65
|
+
return clamp(rounded, min, max);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSameValue(a, b) {
|
|
69
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
70
|
+
return a.every((v, i) => v === b[i]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeRaw(raw, isRange, min, max, step) {
|
|
74
|
+
if (isRange) {
|
|
75
|
+
const tuple = Array.isArray(raw) ? raw : [min, max];
|
|
76
|
+
let start = snap(Number(tuple[0]), min, max, step);
|
|
77
|
+
let end = snap(Number(tuple[1]), min, max, step);
|
|
78
|
+
if (!Number.isFinite(start)) start = min;
|
|
79
|
+
if (!Number.isFinite(end)) end = max;
|
|
80
|
+
if (start > end) [start, end] = [end, start];
|
|
81
|
+
return [start, end];
|
|
82
|
+
}
|
|
83
|
+
const single = Array.isArray(raw) ? raw[0] : raw;
|
|
84
|
+
const safe = Number.isFinite(Number(single)) ? Number(single) : min;
|
|
85
|
+
return [snap(safe, min, max, step)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function valueToPercent(v, min, max) {
|
|
89
|
+
if (max <= min) return 0;
|
|
90
|
+
return ((v - min) / (max - min)) * 100;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default function Slider({
|
|
94
|
+
min = 0,
|
|
95
|
+
max = 100,
|
|
96
|
+
step = 1,
|
|
97
|
+
value,
|
|
98
|
+
defaultValue = [20, 80],
|
|
99
|
+
disabled = false,
|
|
100
|
+
showTooltip = true,
|
|
101
|
+
marks,
|
|
102
|
+
onChange,
|
|
103
|
+
onAfterChange,
|
|
104
|
+
ariaLabel,
|
|
105
|
+
ariaLabelStart,
|
|
106
|
+
ariaLabelEnd,
|
|
107
|
+
ariaLabelledBy,
|
|
108
|
+
ariaDescribedBy,
|
|
109
|
+
className = '',
|
|
110
|
+
style,
|
|
111
|
+
...rest
|
|
112
|
+
}) {
|
|
113
|
+
const isControlled = value !== undefined;
|
|
114
|
+
const isRange = Array.isArray(isControlled ? value : defaultValue);
|
|
115
|
+
|
|
116
|
+
const normalize = useCallback(
|
|
117
|
+
(raw) => normalizeRaw(raw, isRange, min, max, step),
|
|
118
|
+
[isRange, min, max, step]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const [innerValue, setInnerValue] = useState(() => normalize(defaultValue));
|
|
122
|
+
const normalizedControlledValue = useMemo(() => (isControlled ? normalize(value) : null), [isControlled, normalize, value]);
|
|
123
|
+
const current = isControlled ? normalizedControlledValue : innerValue;
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!isControlled) setInnerValue(normalize(defaultValue));
|
|
127
|
+
}, [defaultValue, isControlled, normalize]);
|
|
128
|
+
|
|
129
|
+
const currentRef = useRef(current);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
currentRef.current = current;
|
|
132
|
+
}, [current]);
|
|
133
|
+
|
|
134
|
+
const trackRef = useRef(null);
|
|
135
|
+
const cleanupRef = useRef(null);
|
|
136
|
+
const [draggingIndex, setDraggingIndex] = useState(null);
|
|
137
|
+
|
|
138
|
+
const updateValue = useCallback((nextRaw, fireAfter = false) => {
|
|
139
|
+
const next = normalize(nextRaw);
|
|
140
|
+
if (isSameValue(next, currentRef.current)) {
|
|
141
|
+
if (fireAfter) {
|
|
142
|
+
const payload = isRange ? next : next[0];
|
|
143
|
+
onAfterChange?.(payload);
|
|
144
|
+
}
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isControlled) setInnerValue(next);
|
|
149
|
+
const payload = isRange ? next : next[0];
|
|
150
|
+
onChange?.(payload);
|
|
151
|
+
if (fireAfter) onAfterChange?.(payload);
|
|
152
|
+
return next;
|
|
153
|
+
}, [isControlled, isRange, normalize, onAfterChange, onChange]);
|
|
154
|
+
|
|
155
|
+
const valueFromClientX = useCallback((clientX) => {
|
|
156
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
157
|
+
if (!rect || rect.width <= 0) return null;
|
|
158
|
+
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
159
|
+
const raw = min + ratio * (max - min);
|
|
160
|
+
return snap(raw, min, max, step);
|
|
161
|
+
}, [max, min, step]);
|
|
162
|
+
|
|
163
|
+
const applyAtIndex = useCallback((base, index, nextPoint, fireAfter = false) => {
|
|
164
|
+
if (nextPoint == null) return;
|
|
165
|
+
|
|
166
|
+
if (!isRange) {
|
|
167
|
+
updateValue([nextPoint], fireAfter);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const next = [...base];
|
|
172
|
+
if (index === 0) next[0] = Math.min(nextPoint, next[1]);
|
|
173
|
+
else next[1] = Math.max(nextPoint, next[0]);
|
|
174
|
+
updateValue(next, fireAfter);
|
|
175
|
+
}, [isRange, updateValue]);
|
|
176
|
+
|
|
177
|
+
const clearListeners = useCallback(() => {
|
|
178
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
179
|
+
cleanupRef.current = null;
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
useEffect(() => () => clearListeners(), [clearListeners]);
|
|
183
|
+
|
|
184
|
+
const startDrag = useCallback((index, startEvent) => {
|
|
185
|
+
if (disabled) return;
|
|
186
|
+
startEvent.preventDefault();
|
|
187
|
+
startEvent.stopPropagation();
|
|
188
|
+
setDraggingIndex(index);
|
|
189
|
+
|
|
190
|
+
const onMove = (ev) => {
|
|
191
|
+
const nextPoint = valueFromClientX(ev.clientX);
|
|
192
|
+
applyAtIndex(currentRef.current, index, nextPoint, false);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const onUp = (ev) => {
|
|
196
|
+
const nextPoint = valueFromClientX(ev.clientX);
|
|
197
|
+
applyAtIndex(currentRef.current, index, nextPoint, true);
|
|
198
|
+
setDraggingIndex(null);
|
|
199
|
+
clearListeners();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
window.addEventListener('pointermove', onMove);
|
|
203
|
+
window.addEventListener('pointerup', onUp);
|
|
204
|
+
cleanupRef.current = () => {
|
|
205
|
+
window.removeEventListener('pointermove', onMove);
|
|
206
|
+
window.removeEventListener('pointerup', onUp);
|
|
207
|
+
};
|
|
208
|
+
}, [applyAtIndex, clearListeners, disabled, valueFromClientX]);
|
|
209
|
+
|
|
210
|
+
const onTrackPointerDown = useCallback((e) => {
|
|
211
|
+
if (disabled) return;
|
|
212
|
+
const point = valueFromClientX(e.clientX);
|
|
213
|
+
if (point == null) return;
|
|
214
|
+
|
|
215
|
+
if (!isRange) {
|
|
216
|
+
updateValue([point], true);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const [start, end] = currentRef.current;
|
|
221
|
+
const pick = Math.abs(point - start) <= Math.abs(point - end) ? 0 : 1;
|
|
222
|
+
applyAtIndex(currentRef.current, pick, point, true);
|
|
223
|
+
}, [applyAtIndex, disabled, isRange, updateValue, valueFromClientX]);
|
|
224
|
+
|
|
225
|
+
const onHandleKeyDown = useCallback((index, val) => (e) => {
|
|
226
|
+
if (disabled) return;
|
|
227
|
+
|
|
228
|
+
let nextPoint = val;
|
|
229
|
+
const largeStep = step * 10;
|
|
230
|
+
|
|
231
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') nextPoint = val - step;
|
|
232
|
+
else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') nextPoint = val + step;
|
|
233
|
+
else if (e.key === 'PageDown') nextPoint = val - largeStep;
|
|
234
|
+
else if (e.key === 'PageUp') nextPoint = val + largeStep;
|
|
235
|
+
else if (e.key === 'Home') nextPoint = min;
|
|
236
|
+
else if (e.key === 'End') nextPoint = max;
|
|
237
|
+
else return;
|
|
238
|
+
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
applyAtIndex(currentRef.current, index, snap(nextPoint, min, max, step), true);
|
|
241
|
+
}, [applyAtIndex, disabled, max, min, step]);
|
|
242
|
+
|
|
243
|
+
const [startVal, endVal] = isRange ? current : [min, current[0]];
|
|
244
|
+
const startPercent = valueToPercent(startVal, min, max);
|
|
245
|
+
const endPercent = valueToPercent(endVal, min, max);
|
|
246
|
+
const activeLeft = isRange ? startPercent : 0;
|
|
247
|
+
const activeWidth = isRange ? Math.max(0, endPercent - startPercent) : Math.max(0, endPercent);
|
|
248
|
+
|
|
249
|
+
const markItems = useMemo(() => {
|
|
250
|
+
if (!marks) return [];
|
|
251
|
+
return Object.entries(marks)
|
|
252
|
+
.map(([raw, label]) => {
|
|
253
|
+
const num = Number(raw);
|
|
254
|
+
if (!Number.isFinite(num)) return null;
|
|
255
|
+
const val = clamp(num, min, max);
|
|
256
|
+
const percent = valueToPercent(val, min, max);
|
|
257
|
+
const active = isRange ? (val >= startVal && val <= endVal) : (val <= current[0]);
|
|
258
|
+
return { key: raw, label, val, percent, active };
|
|
259
|
+
})
|
|
260
|
+
.filter(Boolean);
|
|
261
|
+
}, [current, endVal, isRange, marks, max, min, startVal]);
|
|
262
|
+
|
|
263
|
+
const wrapperClass = [
|
|
264
|
+
WRAPPER_BASE,
|
|
265
|
+
markItems.length > 0 ? 'pb-[30px]' : '',
|
|
266
|
+
disabled ? 'opacity-60' : '',
|
|
267
|
+
className,
|
|
268
|
+
].filter(Boolean).join(' ');
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className={[`tfds-slider`, wrapperClass].filter(Boolean).join(' ')} style={style} {...rest} data-tfds-component="Slider">
|
|
272
|
+
<div className={TRACK_AREA}>
|
|
273
|
+
<div ref={trackRef} className={TRACK} onPointerDown={onTrackPointerDown} role="presentation">
|
|
274
|
+
<div
|
|
275
|
+
className={disabled ? TRACK_ACTIVE_DISABLED : TRACK_ACTIVE}
|
|
276
|
+
style={{ left: `${activeLeft}%`, width: `${activeWidth}%` }}
|
|
277
|
+
/>
|
|
278
|
+
|
|
279
|
+
{markItems.map((mark) => (
|
|
280
|
+
<div key={mark.key} style={{ left: `${mark.percent}%` }} className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
281
|
+
<span
|
|
282
|
+
className={[
|
|
283
|
+
MARK_DOT_BASE,
|
|
284
|
+
disabled
|
|
285
|
+
? 'bg-blueGrey-500'
|
|
286
|
+
: mark.active
|
|
287
|
+
? 'bg-brand-500'
|
|
288
|
+
: 'bg-blueGrey-400',
|
|
289
|
+
].join(' ')}
|
|
290
|
+
/>
|
|
291
|
+
<span className={MARK_LABEL}>{mark.label}</span>
|
|
292
|
+
</div>
|
|
293
|
+
))}
|
|
294
|
+
|
|
295
|
+
{current.map((val, index) => {
|
|
296
|
+
const left = valueToPercent(val, min, max);
|
|
297
|
+
const handleClass = [
|
|
298
|
+
HANDLE_BASE,
|
|
299
|
+
disabled ? HANDLE_DISABLED : HANDLE_ENABLED,
|
|
300
|
+
draggingIndex === index ? HANDLE_DRAGGING : '',
|
|
301
|
+
].filter(Boolean).join(' ');
|
|
302
|
+
const shouldShowValueTooltip = showTooltip && draggingIndex === index;
|
|
303
|
+
const fallbackAriaLabel = isRange
|
|
304
|
+
? (index === 0 ? (ariaLabelStart || '起始值') : (ariaLabelEnd || '结束值'))
|
|
305
|
+
: (ariaLabel || '滑块值');
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<button
|
|
309
|
+
key={`${index}-${val}`}
|
|
310
|
+
type="button"
|
|
311
|
+
className={handleClass}
|
|
312
|
+
style={{ left: `${left}%` }}
|
|
313
|
+
onPointerDown={(e) => startDrag(index, e)}
|
|
314
|
+
onKeyDown={onHandleKeyDown(index, val)}
|
|
315
|
+
disabled={disabled}
|
|
316
|
+
role="slider"
|
|
317
|
+
aria-label={ariaLabelledBy ? undefined : fallbackAriaLabel}
|
|
318
|
+
aria-labelledby={ariaLabelledBy}
|
|
319
|
+
aria-describedby={ariaDescribedBy}
|
|
320
|
+
aria-valuemin={min}
|
|
321
|
+
aria-valuemax={max}
|
|
322
|
+
aria-valuenow={val}
|
|
323
|
+
aria-disabled={disabled}
|
|
324
|
+
>
|
|
325
|
+
{shouldShowValueTooltip ? (
|
|
326
|
+
<Tooltip
|
|
327
|
+
content={String(val)}
|
|
328
|
+
placement="top"
|
|
329
|
+
defaultOpen
|
|
330
|
+
triggerClassName={TOOLTIP_TRIGGER}
|
|
331
|
+
>
|
|
332
|
+
<span aria-hidden="true" />
|
|
333
|
+
</Tooltip>
|
|
334
|
+
) : null}
|
|
335
|
+
</button>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
Slider.displayName = 'Slider';
|
|
345
|
+
|
|
346
|
+
export { Slider };
|