@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,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 };