@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,146 @@
1
+ /**
2
+ * Toast — TOKEN_MAP(供平台属性面板展示)
3
+ * 组件实现代码见 Toast.jsx
4
+ *
5
+ * 设计对齐 HiUI Toast 条:圆角 12px(lg)、内距 12px、行内水平间距 8px(gap-x-2)、主文案 14px 半粗、
6
+ * 轻阴影双层(0 0 1px + 0 4px 14px);四语义类型对应品牌/绿/橙/红浅底与同色描边。
7
+ */
8
+ export const TOAST_TOKEN_MAP = {
9
+ 容器: [
10
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-lg', value: '12px' },
11
+ {
12
+ label: '语义描边',
13
+ cssProp: 'border-color',
14
+ value: '与 type 同色 1px',
15
+ state: 'bordered=true',
16
+ },
17
+ {
18
+ label: '语义描边',
19
+ cssProp: 'border-color',
20
+ value: 'transparent(border-transparent)',
21
+ state: 'bordered=false',
22
+ },
23
+ { label: '内距', cssProp: 'padding', token: '--spacing-3', value: '12px' },
24
+ { label: '最小高度', cssProp: 'min-height', value: '44px' },
25
+ { label: '行内水平间距', cssProp: 'column-gap', token: '--spacing-2', value: '8px' },
26
+ {
27
+ label: '宽度',
28
+ cssProp: 'width',
29
+ value: 'fit-content,随内容撑开',
30
+ },
31
+ {
32
+ label: '最大宽度',
33
+ cssProp: 'max-width',
34
+ value: 'min(100%, 560px),防超长与窄屏溢出',
35
+ },
36
+ { label: '阴影', cssProp: 'box-shadow', value: '0 0 1px rgba(0,0,0,0.3), 0 4px 14px rgba(0,0,0,0.1)' },
37
+ {
38
+ label: '信息底',
39
+ cssProp: 'background',
40
+ token: '--color-brand-50',
41
+ value: '#EAFAF6',
42
+ state: 'type=info',
43
+ },
44
+ {
45
+ label: '信息边',
46
+ cssProp: 'border-color',
47
+ token: '--color-teal-600',
48
+ value: '#129683',
49
+ state: 'type=info',
50
+ },
51
+ {
52
+ label: '成功底',
53
+ cssProp: 'background',
54
+ token: '--color-green-50',
55
+ value: '#F0FDF4',
56
+ state: 'type=success',
57
+ },
58
+ {
59
+ label: '成功边',
60
+ cssProp: 'border-color',
61
+ token: '--color-green-500',
62
+ value: '#3EB346',
63
+ state: 'type=success',
64
+ },
65
+ {
66
+ label: '警示底',
67
+ cssProp: 'background',
68
+ token: '--color-orange-50',
69
+ value: '#FFF5EE',
70
+ state: 'type=warning',
71
+ },
72
+ {
73
+ label: '警示边',
74
+ cssProp: 'border-color',
75
+ token: '--color-orange-500',
76
+ value: '#FA8B14',
77
+ state: 'type=warning',
78
+ },
79
+ {
80
+ label: '错误底',
81
+ cssProp: 'background',
82
+ token: '--color-red-50',
83
+ value: '#FEF2F1',
84
+ state: 'type=error',
85
+ },
86
+ {
87
+ label: '错误边',
88
+ cssProp: 'border-color',
89
+ token: '--color-red-500',
90
+ value: '#F74331',
91
+ state: 'type=error',
92
+ },
93
+ ],
94
+ 主文案: [
95
+ { label: '颜色', cssProp: 'color', token: '--color-foreground', value: '#182230' },
96
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
97
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
98
+ { label: '字重', cssProp: 'font-weight', value: '600' },
99
+ ],
100
+ 状态图标: [
101
+ { label: '左侧外边距', cssProp: 'margin-left', token: '--spacing-1', value: '4px' },
102
+ { label: '尺寸', cssProp: 'width', token: '--spacing-5', value: '20px' },
103
+ {
104
+ label: '信息色',
105
+ cssProp: 'color',
106
+ token: '--color-teal-600',
107
+ value: '#129683',
108
+ state: 'type=info',
109
+ },
110
+ {
111
+ label: '成功色',
112
+ cssProp: 'color',
113
+ token: '--color-green-500',
114
+ value: '#3EB346',
115
+ state: 'type=success',
116
+ },
117
+ {
118
+ label: '警示色',
119
+ cssProp: 'color',
120
+ token: '--color-orange-500',
121
+ value: '#FA8B14',
122
+ state: 'type=warning',
123
+ },
124
+ {
125
+ label: '错误色',
126
+ cssProp: 'color',
127
+ token: '--color-red-500',
128
+ value: '#F74331',
129
+ state: 'type=error',
130
+ },
131
+ ],
132
+ 文字操作: [
133
+ { label: '组件', cssProp: '—', value: 'Button variant=text-brand size=sm + !text-sm' },
134
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
135
+ { label: '与主文案间距', cssProp: 'column-gap', token: '--spacing-2', value: '8px' },
136
+ { label: '操作项间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
137
+ { label: '默认字色', cssProp: 'color', token: '--color-brand-700', value: '#18B49D' },
138
+ { label: '悬浮字色', cssProp: 'color', token: '--color-brand-800', value: '#129683', state: 'hover' },
139
+ { label: '按下字色', cssProp: 'color', token: '--color-brand-900', value: '#0D786A', state: 'active' },
140
+ ],
141
+ 引用组件: [
142
+ { label: '关闭按钮', cssProp: '—', value: 'Button ghost-black sm iconOnly(无色底、前景色图标 lucide X)' },
143
+ { label: '文字操作', cssProp: '—', value: 'Button text-brand sm(1~2 项)' },
144
+ { label: '状态图标', cssProp: '—', value: 'Icon md(info-circle / check-circle / alert-triangle / alert-circle)' },
145
+ ],
146
+ };
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Tooltip — 文字提示气泡(Tailwind 内联)
3
+ *
4
+ * 默认深色变体(grey-800 背景 + 白字);hover/focus 触发;
5
+ * tone="light" 仅供 TagInput +N 展示更多标签等特定内容型场景使用。
6
+ * 通过 createPortal 渲染到 document.body 以避开父容器 overflow 裁切,
7
+ * 定位为 fixed 并随滚动/resize 同步。
8
+ *
9
+ * 默认开启 autoFlip:当用户指定方向上空间不足时,自动翻转到对侧主轴
10
+ * (top↔bottom、left↔right),副轴对齐(start/center/end)保持。
11
+ *
12
+ * 文本容器固定规格:圆角 8px、内距上下 8px / 左右 12px、字号 14px、行高 20px。
13
+ *
14
+ * Props:
15
+ * content — 气泡内容(string 或 ReactNode)
16
+ * tone — 'dark' | 'light',默认 'dark'
17
+ * placement — 12 选其一:top/top-start/top-end/bottom/bottom-start/bottom-end/
18
+ * left/left-start/left-end/right/right-start/right-end,默认 'top'
19
+ * arrow — 是否显示箭头,默认 true
20
+ * autoFlip — 视口边缘是否自动翻转方向,默认 true
21
+ * defaultOpen — 是否默认常驻显示(用于预览/调试),默认 false
22
+ * mouseEnterDelay— 进入延迟(ms),默认 100
23
+ * mouseLeaveDelay— 离开延迟(ms),默认 100
24
+ * className — 附加到气泡的类名
25
+ * triggerClassName — 触发器包裹层类名,默认 'inline-flex'
26
+ * children — 触发器(任意 React 子节点,会被包裹到触发器容器中)
27
+ */
28
+
29
+ import { useState, useRef, useLayoutEffect, useEffect, useId, useCallback } from 'react';
30
+ import { createPortal } from 'react-dom';
31
+
32
+ /* ── 常量配置 ── */
33
+ const GAP = 8; // 触发器与气泡的间距
34
+ const ARROW_W = 12; // 箭头底边长
35
+ const ARROW_H = 6; // 箭头高度(凸出气泡外的距离)
36
+ const SAFE_MARGIN = 4; // 视口边缘安全距离
37
+ const ARROW_EDGE_MIN = 14; // 箭头中心距气泡边的最小距离(避开圆角)
38
+
39
+ /* ── 气泡基础样式(含固定内距/字号) ── */
40
+ const BUBBLE = [
41
+ 'pointer-events-none',
42
+ 'rounded-md',
43
+ 'whitespace-pre-line break-words',
44
+ 'max-w-[280px]',
45
+ 'font-normal',
46
+ 'px-3 py-2 text-sm leading-5',
47
+ ].join(' ');
48
+
49
+ const TONE_CLASS = {
50
+ dark: 'bg-grey-800 text-white shadow-md',
51
+ light: 'border border-border bg-white text-foreground shadow-md',
52
+ };
53
+
54
+ const ARROW_FILL_CLASS = {
55
+ dark: 'fill-grey-800',
56
+ light: 'fill-white',
57
+ };
58
+
59
+ /* ── 主轴翻转映射 ── */
60
+ const FLIP_MAIN = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
61
+
62
+ /* ── 拆解 placement → [main, side] ── */
63
+ function parsePlacement(p) {
64
+ const [main, side] = p.split('-');
65
+ return [main, side]; // side 可能是 undefined(即 center)
66
+ }
67
+
68
+ /* ── 计算气泡 left/top(视口坐标系) ── */
69
+ function computePosition(trigger, tw, th, placement) {
70
+ const [main, side] = parsePlacement(placement);
71
+ let top = 0;
72
+ let left = 0;
73
+
74
+ if (main === 'top') top = trigger.top - th - GAP;
75
+ else if (main === 'bottom') top = trigger.bottom + GAP;
76
+ else if (main === 'left') left = trigger.left - tw - GAP;
77
+ else if (main === 'right') left = trigger.right + GAP;
78
+
79
+ if (main === 'top' || main === 'bottom') {
80
+ if (side === 'start') left = trigger.left;
81
+ else if (side === 'end') left = trigger.right - tw;
82
+ else left = trigger.left + (trigger.width - tw) / 2;
83
+ } else {
84
+ if (side === 'start') top = trigger.top;
85
+ else if (side === 'end') top = trigger.bottom - th;
86
+ else top = trigger.top + (trigger.height - th) / 2;
87
+ }
88
+
89
+ return { top, left };
90
+ }
91
+
92
+ /* ── 视口边缘检测,空间不足则翻转主轴 ── */
93
+ function flipPlacement(placement, trigger, tw, th, vw, vh) {
94
+ const [main, side] = parsePlacement(placement);
95
+ let fits = true;
96
+
97
+ if (main === 'top') fits = trigger.top - th - GAP >= SAFE_MARGIN;
98
+ else if (main === 'bottom') fits = trigger.bottom + th + GAP <= vh - SAFE_MARGIN;
99
+ else if (main === 'left') fits = trigger.left - tw - GAP >= SAFE_MARGIN;
100
+ else if (main === 'right') fits = trigger.right + tw + GAP <= vw - SAFE_MARGIN;
101
+
102
+ if (fits) return placement;
103
+ return side ? `${FLIP_MAIN[main]}-${side}` : FLIP_MAIN[main];
104
+ }
105
+
106
+ /* ── 计算箭头在气泡边上的偏移(从气泡左/上算起) ── */
107
+ function computeArrowOffset(actualPlacement, trigger, pos, tw, th) {
108
+ const [main] = parsePlacement(actualPlacement);
109
+ if (main === 'top' || main === 'bottom') {
110
+ const triggerCenterX = trigger.left + trigger.width / 2;
111
+ const raw = triggerCenterX - pos.left;
112
+ return Math.max(ARROW_EDGE_MIN, Math.min(tw - ARROW_EDGE_MIN, raw));
113
+ }
114
+ const triggerCenterY = trigger.top + trigger.height / 2;
115
+ const raw = triggerCenterY - pos.top;
116
+ return Math.max(ARROW_EDGE_MIN, Math.min(th - ARROW_EDGE_MIN, raw));
117
+ }
118
+
119
+ /* ── 箭头 SVG(按主轴方向画对应朝向的三角) ── */
120
+ function ArrowSvg({ main, tone = 'dark' }) {
121
+ const fillClass = ARROW_FILL_CLASS[tone] || ARROW_FILL_CLASS.dark;
122
+ // 横向:底边 ARROW_W、高 ARROW_H
123
+ if (main === 'top') {
124
+ // tooltip 在触发器上方 → 箭头朝下
125
+ return (
126
+ <svg width={ARROW_W} height={ARROW_H} viewBox={`0 0 ${ARROW_W} ${ARROW_H}`} className={['block', fillClass].join(' ')} aria-hidden>
127
+ <path d={`M0 0 L${ARROW_W} 0 L${ARROW_W / 2} ${ARROW_H} Z`} />
128
+ </svg>
129
+ );
130
+ }
131
+ if (main === 'bottom') {
132
+ return (
133
+ <svg width={ARROW_W} height={ARROW_H} viewBox={`0 0 ${ARROW_W} ${ARROW_H}`} className={['block', fillClass].join(' ')} aria-hidden>
134
+ <path d={`M0 ${ARROW_H} L${ARROW_W} ${ARROW_H} L${ARROW_W / 2} 0 Z`} />
135
+ </svg>
136
+ );
137
+ }
138
+ // 纵向:宽 ARROW_H、高 ARROW_W
139
+ if (main === 'left') {
140
+ return (
141
+ <svg width={ARROW_H} height={ARROW_W} viewBox={`0 0 ${ARROW_H} ${ARROW_W}`} className={['block', fillClass].join(' ')} aria-hidden>
142
+ <path d={`M0 0 L0 ${ARROW_W} L${ARROW_H} ${ARROW_W / 2} Z`} />
143
+ </svg>
144
+ );
145
+ }
146
+ return (
147
+ <svg width={ARROW_H} height={ARROW_W} viewBox={`0 0 ${ARROW_H} ${ARROW_W}`} className={['block', fillClass].join(' ')} aria-hidden>
148
+ <path d={`M${ARROW_H} 0 L${ARROW_H} ${ARROW_W} L0 ${ARROW_W / 2} Z`} />
149
+ </svg>
150
+ );
151
+ }
152
+
153
+ export default function Tooltip({
154
+ content,
155
+ tone = 'dark',
156
+ placement = 'top',
157
+ arrow = true,
158
+ autoFlip = true,
159
+ defaultOpen = false,
160
+ mouseEnterDelay = 100,
161
+ mouseLeaveDelay = 100,
162
+ className = '',
163
+ triggerClassName = 'inline-flex',
164
+ children,
165
+ }) {
166
+ const triggerRef = useRef(null);
167
+ const bubbleRef = useRef(null);
168
+ const enterTimer = useRef(null);
169
+ const leaveTimer = useRef(null);
170
+
171
+ const [open, setOpen] = useState(defaultOpen);
172
+ const [pos, setPos] = useState({ top: 0, left: 0, actualPlacement: placement, arrowOffset: 0 });
173
+
174
+ const updatePosition = useCallback(() => {
175
+ if (!triggerRef.current || !bubbleRef.current) return;
176
+ const trigger = triggerRef.current.getBoundingClientRect();
177
+ const bubble = bubbleRef.current.getBoundingClientRect();
178
+ const tw = bubble.width;
179
+ const th = bubble.height;
180
+ const vw = window.innerWidth;
181
+ const vh = window.innerHeight;
182
+
183
+ const actualPlacement = autoFlip
184
+ ? flipPlacement(placement, trigger, tw, th, vw, vh)
185
+ : placement;
186
+ const p = computePosition(trigger, tw, th, actualPlacement);
187
+ const arrowOffset = computeArrowOffset(actualPlacement, trigger, p, tw, th);
188
+ setPos({ top: p.top, left: p.left, actualPlacement, arrowOffset });
189
+ }, [placement, autoFlip]);
190
+
191
+ useLayoutEffect(() => {
192
+ if (open) updatePosition();
193
+ }, [open, content, updatePosition]);
194
+
195
+ useEffect(() => {
196
+ if (!open) return;
197
+ const onScroll = () => updatePosition();
198
+ const onResize = () => updatePosition();
199
+ window.addEventListener('scroll', onScroll, true);
200
+ window.addEventListener('resize', onResize);
201
+ return () => {
202
+ window.removeEventListener('scroll', onScroll, true);
203
+ window.removeEventListener('resize', onResize);
204
+ };
205
+ }, [open, updatePosition]);
206
+
207
+ const clearTimers = () => {
208
+ if (enterTimer.current) clearTimeout(enterTimer.current);
209
+ if (leaveTimer.current) clearTimeout(leaveTimer.current);
210
+ enterTimer.current = null;
211
+ leaveTimer.current = null;
212
+ };
213
+
214
+ const handleEnter = () => {
215
+ clearTimers();
216
+ enterTimer.current = setTimeout(() => setOpen(true), mouseEnterDelay);
217
+ };
218
+
219
+ const handleLeave = () => {
220
+ clearTimers();
221
+ leaveTimer.current = setTimeout(() => setOpen(false), mouseLeaveDelay);
222
+ };
223
+
224
+ useEffect(() => () => clearTimers(), []);
225
+
226
+ const tooltipId = useId();
227
+ const [main] = parsePlacement(pos.actualPlacement);
228
+ const resolvedTone = TONE_CLASS[tone] ? tone : 'dark';
229
+
230
+ /* ── 箭头容器定位(相对气泡 absolute) ── */
231
+ let arrowStyle = {};
232
+ if (main === 'top') {
233
+ arrowStyle = { position: 'absolute', bottom: -ARROW_H, left: pos.arrowOffset - ARROW_W / 2 };
234
+ } else if (main === 'bottom') {
235
+ arrowStyle = { position: 'absolute', top: -ARROW_H, left: pos.arrowOffset - ARROW_W / 2 };
236
+ } else if (main === 'left') {
237
+ arrowStyle = { position: 'absolute', right: -ARROW_H, top: pos.arrowOffset - ARROW_W / 2 };
238
+ } else {
239
+ arrowStyle = { position: 'absolute', left: -ARROW_H, top: pos.arrowOffset - ARROW_W / 2 };
240
+ }
241
+
242
+ return (
243
+ <>
244
+ <span
245
+ ref={triggerRef}
246
+ className={triggerClassName}
247
+ onMouseEnter={handleEnter}
248
+ onMouseLeave={handleLeave}
249
+ onFocus={handleEnter}
250
+ onBlur={handleLeave}
251
+ aria-describedby={open ? tooltipId : undefined}
252
+ >
253
+ {children}
254
+ </span>
255
+ {open && typeof document !== 'undefined'
256
+ ? createPortal(
257
+ <div
258
+ ref={bubbleRef}
259
+ id={tooltipId}
260
+ role="tooltip"
261
+ className={[`tfds-tooltip`, [BUBBLE, TONE_CLASS[resolvedTone], className].filter(Boolean).join(' ')].filter(Boolean).join(' ')}
262
+ data-tfds-component="Tooltip"
263
+ style={{
264
+ position: 'fixed',
265
+ top: pos.top,
266
+ left: pos.left,
267
+ zIndex: 9999,
268
+ }}
269
+ >
270
+ {content}
271
+ {arrow ? (
272
+ <span style={arrowStyle} aria-hidden>
273
+ <ArrowSvg main={main} tone={resolvedTone} />
274
+ </span>
275
+ ) : null}
276
+ </div>,
277
+ document.body
278
+ )
279
+ : null}
280
+ </>
281
+ );
282
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tooltip — TOKEN_MAP(供平台属性面板展示)
3
+ * 组件实现代码见 Tooltip.jsx
4
+ *
5
+ * 设计说明:
6
+ * - 默认深色变体:背景 grey-800(#2E2F38,中性深灰)
7
+ * - light 变体仅供 TagInput +N 展示更多标签等特定内容型场景使用
8
+ * - 文字白色,圆角 md=8px,阴影 shadow-md
9
+ * - 文本容器固定规格:内距上下 8px / 左右 12px,字号 14px、行高 20px
10
+ * - 12 方位 placement,开启 autoFlip 时空间不足自动翻转主轴
11
+ * - 触发器与气泡间距统一 8px,箭头底边 12px、高度 6px
12
+ *
13
+ * 平台属性面板按 key 顺序渲染:气泡 → 箭头 → 触发交互 → 定位行为。
14
+ */
15
+ export const TOOLTIP_TOKEN_MAP = {
16
+ 气泡: [
17
+ { label: '背景色', cssProp: 'background', token: '--color-grey-800', value: '#2E2F38' },
18
+ { label: '文字颜色', cssProp: 'color', token: '--color-white', value: '#FFFFFF' },
19
+ { label: '白底场景', cssProp: 'tone', value: 'light 仅用于 TagInput +N 更多标签' },
20
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
21
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
22
+ { label: '字重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
23
+ { label: '横向内距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
24
+ { label: '纵向内距', cssProp: 'padding-block', token: '--spacing-2', value: '8px' },
25
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
26
+ { label: '阴影', cssProp: 'box-shadow', token: '--shadow-md', value: '0 4px 6px -1px rgba(0,0,0,0.1)' },
27
+ { label: '最大宽度', cssProp: 'max-width', value: '280px' },
28
+ ],
29
+ 箭头: [
30
+ { label: '颜色', cssProp: 'fill', token: '--color-grey-800', value: '#2E2F38' },
31
+ { label: '底边宽', cssProp: 'width', value: '12px' },
32
+ { label: '高度', cssProp: 'height', value: '6px' },
33
+ { label: '距气泡边最小偏移', cssProp: 'edge-min', value: '14px' },
34
+ ],
35
+ 触发交互: [
36
+ { label: '触发方式', cssProp: 'trigger', value: 'hover / focus' },
37
+ { label: '触发器与气泡间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
38
+ { label: '进入延迟', cssProp: 'enter-delay', value: '100ms' },
39
+ { label: '离开延迟', cssProp: 'leave-delay', value: '100ms' },
40
+ ],
41
+ 定位行为: [
42
+ { label: '渲染目标', cssProp: 'portal', value: 'document.body' },
43
+ { label: '定位方式', cssProp: 'position', value: 'fixed' },
44
+ { label: '层级', cssProp: 'z-index', value: '9999' },
45
+ { label: '边缘安全距离', cssProp: 'safe-margin', value: '4px' },
46
+ { label: '空间不足', cssProp: 'auto-flip', value: '主轴翻转(top↔bottom,left↔right)' },
47
+ ],
48
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TooltipPreview — Tooltip 在平台预览页中的演示包装
3
+ *
4
+ * Tooltip 必须 hover/focus 才会出现,预览页直接渲染会"看不到东西"。
5
+ * 这里固定一个中央触发器(虚线方框 + 文字),让 Tooltip 默认 open,
6
+ * 切换 placement 时即可稳定看到 12 个方位的视觉效果。
7
+ *
8
+ * 模式(preview-only,不进入 Tooltip 组件本身的 props):
9
+ * mode='pinned' — 默认 open,便于截图/对比
10
+ * mode='hover' — 不强制 open,鼠标移到触发器上才出现,演示真实交互
11
+ */
12
+
13
+ import Tooltip from './Tooltip';
14
+
15
+ const TRIGGER_BASE = [
16
+ 'inline-flex items-center justify-center',
17
+ 'h-9 px-4 rounded-md',
18
+ 'text-sm text-foreground-secondary',
19
+ 'bg-elevated border border-dashed border-border-default',
20
+ 'cursor-default select-none',
21
+ ].join(' ');
22
+
23
+ export default function TooltipPreview({
24
+ placement = 'top',
25
+ arrow = true,
26
+ autoFlip = true,
27
+ mode = 'pinned',
28
+ content = '这是一段提示文案',
29
+ triggerLabel = '触发元素',
30
+ }) {
31
+ const isPinned = mode === 'pinned';
32
+
33
+ return (
34
+ <div
35
+ className="flex items-center justify-center w-full"
36
+ style={{ minHeight: '200px' }}
37
+ >
38
+ <Tooltip
39
+ key={`${placement}-${arrow}-${autoFlip}-${mode}`}
40
+ content={content}
41
+ placement={placement}
42
+ arrow={arrow}
43
+ autoFlip={autoFlip}
44
+ defaultOpen={isPinned}
45
+ >
46
+ <span className={TRIGGER_BASE}>{triggerLabel}</span>
47
+ </Tooltip>
48
+ </div>
49
+ );
50
+ }