@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,75 @@
1
+ /**
2
+ * TagBar — TOKEN_MAP(供平台属性面板展示)
3
+ *
4
+ * 分组顺序:文本 → 容器 → 尺寸 → 引用组件
5
+ * 搜索 / 业务名 / 标签字 / 整栏 / 头部 / 树项 / 图标底 / 折叠钮 / 尺寸 / 引用组件
6
+ */
7
+ export const TAGBAR_TOKEN_MAP = {
8
+ 搜索: [
9
+ { label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
10
+ { label: '描边色', cssProp: 'box-shadow', token: '--color-border-default', value: 'inset 0 0 0 1px #E4E7EC', semanticRef: 'border-default' },
11
+ { label: '图标色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
12
+ { label: '占位色', cssProp: 'color', value: 'rgba(34,39,39,0.6)', state: 'placeholder' },
13
+ { label: '高度', cssProp: 'height', value: '36px' },
14
+ ],
15
+ 业务名: [
16
+ { label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
17
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
18
+ { label: '字重', cssProp: 'font-weight', token: '--font-semibold', value: '600(运行时使用 [font-weight:var(--font-semibold)])' },
19
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
20
+ { label: '容器底色', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default' },
21
+ ],
22
+ 标签字: [
23
+ { label: '一级字号', cssProp: 'font-size', token: '--text-sm', value: '14px', state: 'depth-0/1' },
24
+ { label: '一级行高', cssProp: 'line-height', token: '--leading-5', value: '20px', state: 'depth-0/1' },
25
+ { label: '三级字号', cssProp: 'font-size', token: '--text-xs', value: '12px', state: 'depth-2' },
26
+ { label: '三级行高', cssProp: 'line-height', token: '--leading-4', value: '16px', state: 'depth-2' },
27
+ { label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
28
+ { label: '悬浮底', cssProp: 'background', value: 'rgba(83, 96, 143, 0.04)', state: 'hover' },
29
+ ],
30
+ 整栏: [
31
+ { label: '展开背景', cssProp: 'background', value: 'linear-gradient(270deg, rgb(249,250,251) 37.08%, rgb(255,255,255) 100%)' },
32
+ { label: '收起背景', cssProp: 'background', token: '--color-blueGrey-100', value: '#F9FAFB', state: 'collapsed' },
33
+ { label: '分隔线', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
34
+ ],
35
+ 头部: [
36
+ { label: '纵向间距', cssProp: 'gap', token: '--spacing-3', value: '12px' },
37
+ { label: '左右内距', cssProp: 'padding-inline', token: '--spacing-5', value: '20px' },
38
+ { label: '底部内距', cssProp: 'padding-bottom', token: '--spacing-4', value: '16px' },
39
+ ],
40
+ 树项: [
41
+ { label: '层级线颜色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
42
+ { label: '箭头色', cssProp: 'color', value: 'rgba(34,39,39,0.8)' },
43
+ { label: '二级缩进', cssProp: 'padding-left', value: '24px', state: 'depth-1' },
44
+ { label: '三级缩进', cssProp: 'padding-left', value: '48px', state: 'depth-2' },
45
+ ],
46
+ 图标底: [
47
+ { label: '品牌底', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6' },
48
+ { label: '蓝色底', cssProp: 'background', token: '--color-blue-50', value: '#ECF6FE' },
49
+ { label: '青色底', cssProp: 'background', token: '--color-cyan-50', value: '#EAF9FA' },
50
+ { label: '绿色底', cssProp: 'background', token: '--color-green-50', value: '#F0FDF4' },
51
+ { label: '橙色底', cssProp: 'background', token: '--color-orange-50', value: '#FFF5EE' },
52
+ { label: '黄色底', cssProp: 'background', token: '--color-yellow-50', value: '#FEFDEC' },
53
+ { label: '紫色底', cssProp: 'background', token: '--color-purple-50', value: '#F7EAF7' },
54
+ { label: '粉色底', cssProp: 'background', token: '--color-pink-50', value: '#FDEDF1' },
55
+ { label: '中性底', cssProp: 'background', token: '--color-blueGrey-100', value: '#F9FAFB' },
56
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-avatar', value: '3px' },
57
+ ],
58
+ 折叠钮: [
59
+ { label: '尺寸', cssProp: 'width/height', value: '36px' },
60
+ { label: '图标色', cssProp: 'color', value: 'rgba(34,39,39,0.8)' },
61
+ { label: '悬浮底', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default', state: 'hover' },
62
+ ],
63
+ 尺寸: [
64
+ { label: '展开宽度', cssProp: 'width', value: '240px' },
65
+ { label: '收起宽度', cssProp: 'width', value: '76px' },
66
+ { label: '顶部业务高', cssProp: 'height', value: '36px' },
67
+ { label: '节点高度', cssProp: 'height', value: '32px' },
68
+ { label: '图标槽宽', cssProp: 'width', value: '16px' },
69
+ { label: '图标槽高', cssProp: 'height', value: '16px' },
70
+ ],
71
+ 引用组件: [
72
+ { label: '标签图标', cssProp: '—', value: 'Icon / user-01-stroked / layers-two-01-stroked / heart-circle-stroked / edit-01-stroked / gift-01-stroked / credit-card-01-stroked / flip-forward-stroked / search-lg-stroked / star-01-stroked / shopping-bag-03-stroked / heart-hand-stroked / globe-01-stroked' },
73
+ { label: '交互图标', cssProp: '—', value: 'Icon / chevron-up-stroked / chevron-down-stroked / layout-left-stroked / search-lg-stroked' },
74
+ ],
75
+ };
@@ -0,0 +1,23 @@
1
+ import Tag from './Tag';
2
+
3
+ const DEFAULT_VARIANTS = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple', 'pink', 'teal', 'grey', 'white', 'ai'];
4
+
5
+ export default function TagGridPreview({ variant, size, radius, fontWeight, showIcon, iconName, closable, onClose }) {
6
+ if (variant && variant !== 'brand') {
7
+ return (
8
+ <Tag variant={variant} size={size} radius={radius} fontWeight={fontWeight} showIcon={showIcon} iconName={iconName} closable={closable} onClose={onClose}>
9
+ 标签
10
+ </Tag>
11
+ );
12
+ }
13
+
14
+ return (
15
+ <div className="flex h-full min-h-full flex-col items-center justify-center gap-6">
16
+ {DEFAULT_VARIANTS.map((v) => (
17
+ <Tag key={v} variant={v} size={size} radius={radius} fontWeight={fontWeight} showIcon={showIcon} iconName={iconName} closable={closable} onClose={onClose}>
18
+ 标签
19
+ </Tag>
20
+ ))}
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * TagInput — 标签输入框(Tailwind 内联)
3
+ *
4
+ * 容器视觉对齐 Figma taginput / Select:36px 白底、8px 圆角、1px 默认描边。
5
+ * 已选标签、+N 折叠项与 Tooltip 内标签都必须复用平台现有 Tag 组件;
6
+ * TagInput 只负责容器状态、宽度测量、折叠和删除逻辑。
7
+ *
8
+ * @prop {'default'|'error'} [status='default'] — 状态
9
+ * @prop {boolean} [disabled=false] — 是否禁用
10
+ * @prop {string} [placeholder='请输入'] — 空态占位
11
+ * @prop {Array<string|{value:string,label:string,variant?:string}>} value — 当前标签(受控)
12
+ * @prop {Array<string|{value:string,label:string,variant?:string}>} [defaultValue=null] — 非受控初始标签
13
+ * @prop {(nextTags:Array, removedTag?:object) => void} [onChange=null] — 标签列表变化
14
+ * @prop {(tag:object) => void} [onRemove=null] — 删除标签回调
15
+ * @prop {string|Array<string|object>|object} [aiSuggestion] — 单条 AI 推荐(可为单个标签或标签集合)
16
+ * @prop {Array<string|Array<string|object>|object>} [aiSuggestions] — 多条 AI 推荐
17
+ * @prop {(suggestion:Array) => void} [onAdoptSuggestion=null] — 采纳 AI 推荐回调
18
+ * @prop {() => void} [onRefreshAiSuggestions=null] — 刷新 AI 推荐回调
19
+ * @prop {boolean} [closable=true] — 标签是否可关闭
20
+ * @prop {'brand'|'red'|'orange'|'yellow'|'green'|'cyan'|'blue'|'purple'|'pink'|'teal'|'grey'|'white'} [tagVariant='grey'] — 默认 Tag 颜色
21
+ * @prop {'s'|'m'|'l'} [tagSize='m'] — Tag 尺寸
22
+ * @prop {string} [className=''] — 类名
23
+ */
24
+
25
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
26
+ import Tag from './Tag';
27
+ import { DEFAULT_TAG_RADIUS, DEFAULT_TAG_SIZE, DEFAULT_TAG_VARIANT } from './tagShared';
28
+ import Tooltip from './Tooltip';
29
+ import AiSuggestionPanel, {
30
+ AiRefreshButton,
31
+ AI_REFRESH_VISIBLE_CLASS,
32
+ rotateList,
33
+ } from './AiSuggestionShared';
34
+
35
+ /* ── 根容器基础样式 ── */
36
+ const BASE = [
37
+ 'tfds-tag-input',
38
+ 'group relative inline-flex w-full min-w-0 max-w-full items-center',
39
+ 'border border-solid rounded-md',
40
+ 'transition-colors duration-150',
41
+ 'outline-none [font-family:inherit]',
42
+ ].join(' ');
43
+
44
+ /* ── 状态 → 容器背景 / 边框 ── */
45
+ const STATUS_CLASS = {
46
+ default: [
47
+ 'bg-surface border-border',
48
+ 'hover:border-blueGrey-400',
49
+ 'focus-within:border-primary',
50
+ 'focus-within:hover:border-primary',
51
+ ].join(' '),
52
+ error: [
53
+ 'bg-red-50 border-border',
54
+ 'hover:bg-red-100',
55
+ 'focus-within:border-red-500',
56
+ 'focus-within:hover:border-red-500 focus-within:hover:bg-red-50',
57
+ ].join(' '),
58
+ };
59
+
60
+ /* ── 固定 MD 尺寸 → 高度 / 内边距 / 字号 ── */
61
+ const SIZE_CLASS = 'min-h-9 pl-3 pr-2 py-[6px] text-sm';
62
+
63
+ /* ── 禁用态 ── */
64
+ const DISABLED_CLASS = 'bg-disabled border-border cursor-not-allowed opacity-60';
65
+
66
+ /* ── 内容行 / 输入占位 ── */
67
+ const CONTENT_ROW = 'flex min-w-0 flex-1 items-center gap-1 overflow-hidden';
68
+ const PLACEHOLDER = 'min-w-0 truncate text-sm leading-5 text-foreground-muted';
69
+ const MEASURE_ROW = 'pointer-events-none invisible absolute left-0 top-0 flex items-center gap-1 whitespace-nowrap';
70
+ const WRAPPER_BASE = 'inline-flex max-w-full flex-col items-start gap-1 [font-family:inherit]';
71
+ const SUGGESTION_ACTION_ROW = 'flex w-full justify-end';
72
+
73
+ const TAGINPUT_LOCAL_FALLBACK_SUGGESTIONS = [
74
+ ['改签', '门店变更'],
75
+ ['退款', '售后处理'],
76
+ ['物流异常', '催办'],
77
+ ];
78
+
79
+ function normalizeTag(item, index, tagVariant) {
80
+ if (item && typeof item === 'object') {
81
+ const label = String(item.label ?? item.value ?? '');
82
+ const fallbackValue = `${label || 'tag'}-${index}`;
83
+ return {
84
+ ...item,
85
+ value: String(item.value ?? fallbackValue),
86
+ label,
87
+ variant: item.variant || tagVariant,
88
+ dedupeKey: String(item.value ?? label).trim().toLowerCase(),
89
+ };
90
+ }
91
+ const label = String(item ?? '');
92
+ return {
93
+ value: `${label || 'tag'}-${index}`,
94
+ label,
95
+ variant: tagVariant,
96
+ dedupeKey: label.trim().toLowerCase(),
97
+ };
98
+ }
99
+
100
+ function normalizeList(raw, tagVariant) {
101
+ return (Array.isArray(raw) ? raw : [])
102
+ .map((item, index) => normalizeTag(item, index, tagVariant))
103
+ .filter((tag) => tag.label.length > 0);
104
+ }
105
+
106
+ function getVisibleCount(tagWidths, moreWidth, availableWidth) {
107
+ if (tagWidths.length === 0) return 0;
108
+ const gap = 4;
109
+ let used = 0;
110
+ for (let i = 0; i < tagWidths.length; i += 1) {
111
+ const remaining = tagWidths.length - i - 1;
112
+ const reserveMore = remaining > 0 ? moreWidth + gap : 0;
113
+ const nextUsed = used + tagWidths[i] + (i > 0 ? gap : 0);
114
+ if (nextUsed + reserveMore <= availableWidth) {
115
+ used = nextUsed;
116
+ continue;
117
+ }
118
+ return Math.max(0, i);
119
+ }
120
+ return tagWidths.length;
121
+ }
122
+
123
+ function normalizeTagSuggestion(item, index, tagVariant) {
124
+ let tags = [];
125
+ if (Array.isArray(item)) {
126
+ tags = normalizeList(item, tagVariant);
127
+ } else if (item && typeof item === 'object' && Array.isArray(item.tags)) {
128
+ tags = normalizeList(item.tags, tagVariant);
129
+ } else if (item != null) {
130
+ tags = normalizeList([item], tagVariant);
131
+ }
132
+
133
+ if (tags.length === 0) return null;
134
+ return {
135
+ id: `tag-input-ai-suggestion-${index}`,
136
+ label: tags.map((tag) => tag.label).join(' / '),
137
+ tags,
138
+ };
139
+ }
140
+
141
+ function buildTagSuggestionGroups(aiSuggestion, aiSuggestions, tagVariant) {
142
+ const source = Array.isArray(aiSuggestions) && aiSuggestions.length > 0
143
+ ? aiSuggestions
144
+ : (aiSuggestion != null ? [aiSuggestion] : []);
145
+ const baseGroup = source
146
+ .map((item, index) => normalizeTagSuggestion(item, index, tagVariant))
147
+ .filter(Boolean);
148
+ if (baseGroup.length === 0) return [];
149
+ const groups = [];
150
+ const serialized = new Set();
151
+
152
+ const pushGroup = (group) => {
153
+ if (!Array.isArray(group) || group.length === 0) return;
154
+ const key = JSON.stringify(group.map((item) => item.label));
155
+ if (serialized.has(key)) return;
156
+ serialized.add(key);
157
+ groups.push(group);
158
+ };
159
+
160
+ pushGroup(baseGroup);
161
+
162
+ if (baseGroup.length > 1) {
163
+ for (let offset = 1; offset < baseGroup.length; offset += 1) {
164
+ pushGroup(rotateList(baseGroup, offset));
165
+ }
166
+ }
167
+
168
+ const fallbackGroup = TAGINPUT_LOCAL_FALLBACK_SUGGESTIONS
169
+ .map((item, index) => normalizeTagSuggestion(item, `fallback-${index}`, tagVariant))
170
+ .filter(Boolean);
171
+ pushGroup(fallbackGroup);
172
+
173
+ return groups;
174
+ }
175
+
176
+ function mergeSuggestedTags(currentTags, suggestedTags) {
177
+ const merged = [...currentTags];
178
+ const existingKeys = new Set(
179
+ currentTags.map((tag) => String(tag.dedupeKey || tag.value || tag.label).trim().toLowerCase())
180
+ );
181
+
182
+ suggestedTags.forEach((tag) => {
183
+ const key = String(tag.dedupeKey || tag.value || tag.label).trim().toLowerCase();
184
+ if (!key || existingKeys.has(key)) return;
185
+ existingKeys.add(key);
186
+ merged.push(tag);
187
+ });
188
+
189
+ return merged;
190
+ }
191
+
192
+ export default function TagInput({
193
+ size: _size = 'md',
194
+ status = 'default',
195
+ disabled = false,
196
+ placeholder = '请输入',
197
+ value,
198
+ defaultValue = null,
199
+ onChange = null,
200
+ onRemove = null,
201
+ onAdoptSuggestion = null,
202
+ onRefreshAiSuggestions = null,
203
+ aiSuggestion,
204
+ aiSuggestions,
205
+ closable = true,
206
+ tagVariant = DEFAULT_TAG_VARIANT,
207
+ tagSize = DEFAULT_TAG_SIZE,
208
+ className = '',
209
+ ...rest
210
+ }) {
211
+ const rootRef = useRef(null);
212
+ const measureRef = useRef(null);
213
+ const moreMeasureRef = useRef(null);
214
+ const isControlled = value !== undefined;
215
+ const [innerValue, setInnerValue] = useState(() => normalizeList(defaultValue, tagVariant));
216
+ const [visibleCount, setVisibleCount] = useState(null);
217
+ const [isSuggestionDismissed, setIsSuggestionDismissed] = useState(false);
218
+ const [suggestionIndex, setSuggestionIndex] = useState(0);
219
+
220
+ useEffect(() => {
221
+ if (!isControlled) setInnerValue(normalizeList(defaultValue, tagVariant));
222
+ }, [defaultValue, isControlled, tagVariant]);
223
+
224
+ useEffect(() => {
225
+ setIsSuggestionDismissed(false);
226
+ setSuggestionIndex(0);
227
+ }, [aiSuggestion, aiSuggestions]);
228
+
229
+ const tags = useMemo(
230
+ () => (isControlled ? normalizeList(value, tagVariant) : innerValue),
231
+ [innerValue, isControlled, tagVariant, value],
232
+ );
233
+ const suggestionGroups = useMemo(
234
+ () => buildTagSuggestionGroups(aiSuggestion, aiSuggestions, tagVariant),
235
+ [aiSuggestion, aiSuggestions, tagVariant]
236
+ );
237
+ const suggestionList = suggestionGroups[suggestionIndex] || suggestionGroups[0] || [];
238
+ const canRefreshLocally = suggestionGroups.length > 1;
239
+ const showSuggestion = !disabled && !isSuggestionDismissed && suggestionList.length > 0;
240
+ const showAiRefresh = !disabled && suggestionList.length > 0
241
+ && (canRefreshLocally || typeof onRefreshAiSuggestions === 'function');
242
+
243
+ const measure = useCallback(() => {
244
+ const root = rootRef.current;
245
+ const row = measureRef.current;
246
+ if (!root || !row) return;
247
+ const availableWidth = Math.max(0, root.clientWidth - 20);
248
+ const tagWidths = Array.from(row.querySelectorAll('[data-measure-tag]'))
249
+ .map((node) => Math.ceil(node.getBoundingClientRect().width));
250
+ const moreWidth = Math.ceil(moreMeasureRef.current?.getBoundingClientRect().width || 34);
251
+ setVisibleCount(getVisibleCount(tagWidths, moreWidth, availableWidth));
252
+ }, []);
253
+
254
+ useLayoutEffect(() => {
255
+ measure();
256
+ }, [measure, tags, tagSize, status]);
257
+
258
+ useEffect(() => {
259
+ if (!rootRef.current || typeof ResizeObserver === 'undefined') return undefined;
260
+ const observer = new ResizeObserver(() => measure());
261
+ observer.observe(rootRef.current);
262
+ return () => observer.disconnect();
263
+ }, [measure]);
264
+
265
+ const commit = useCallback((next, removedTag) => {
266
+ if (!isControlled) setInnerValue(next);
267
+ onChange?.(next, removedTag);
268
+ }, [isControlled, onChange]);
269
+
270
+ const handleRemove = useCallback((tag) => {
271
+ if (disabled) return;
272
+ const next = tags.filter((item) => item.value !== tag.value);
273
+ commit(next, tag);
274
+ onRemove?.(tag);
275
+ }, [commit, disabled, onRemove, tags]);
276
+
277
+ const handleAdoptSuggestion = useCallback((suggestion) => {
278
+ if (!suggestion?.tags || suggestion.tags.length === 0 || disabled) return;
279
+ const next = mergeSuggestedTags(tags, suggestion.tags);
280
+ commit(next);
281
+ onAdoptSuggestion?.(suggestion.tags);
282
+ setIsSuggestionDismissed(true);
283
+ }, [commit, disabled, onAdoptSuggestion, tags]);
284
+
285
+ const handleRefreshAiSuggestions = useCallback(() => {
286
+ if (canRefreshLocally) {
287
+ setSuggestionIndex((prev) => (prev + 1) % suggestionGroups.length);
288
+ }
289
+ onRefreshAiSuggestions?.();
290
+ setIsSuggestionDismissed(false);
291
+ }, [canRefreshLocally, onRefreshAiSuggestions, suggestionGroups.length]);
292
+
293
+ const safeVisibleCount = visibleCount == null ? tags.length : Math.min(visibleCount, tags.length);
294
+ const visibleTags = tags.slice(0, safeVisibleCount);
295
+ const hiddenTags = tags.slice(safeVisibleCount);
296
+
297
+ const rootCls = [
298
+ BASE,
299
+ disabled ? DISABLED_CLASS : (STATUS_CLASS[status] || STATUS_CLASS.default),
300
+ SIZE_CLASS,
301
+ className,
302
+ ].filter(Boolean).join(' ');
303
+
304
+ const hiddenContent = hiddenTags.length > 0 ? (
305
+ <div className="flex max-w-[248px] flex-wrap gap-1">
306
+ {hiddenTags.map((tag) => (
307
+ <Tag key={tag.value} variant={tag.variant || tagVariant} size={tagSize} radius={DEFAULT_TAG_RADIUS}>
308
+ {tag.label}
309
+ </Tag>
310
+ ))}
311
+ </div>
312
+ ) : null;
313
+
314
+ const renderTag = (tag, tagProps = {}) => (
315
+ <Tag
316
+ key={tag.value}
317
+ variant={tag.variant || tagVariant}
318
+ size={tagSize}
319
+ radius={DEFAULT_TAG_RADIUS}
320
+ {...tagProps}
321
+ >
322
+ {tag.label}
323
+ </Tag>
324
+ );
325
+
326
+ return (
327
+ <div className={WRAPPER_BASE}>
328
+ <div ref={rootRef} className={rootCls} aria-disabled={disabled || undefined} data-tfds-component="TagInput" {...rest}>
329
+ <div className={CONTENT_ROW}>
330
+ {tags.length === 0 ? (
331
+ <span className={PLACEHOLDER}>{placeholder}</span>
332
+ ) : (
333
+ <>
334
+ {visibleTags.map((tag) => (
335
+ renderTag(tag, {
336
+ closable: closable && !disabled,
337
+ onClose: closable && !disabled ? () => handleRemove(tag) : undefined,
338
+ })
339
+ ))}
340
+ {hiddenTags.length > 0 ? (
341
+ <Tooltip
342
+ content={hiddenContent}
343
+ tone="light"
344
+ placement="top"
345
+ triggerClassName="inline-flex shrink-0"
346
+ className="!max-w-[280px]"
347
+ >
348
+ <Tag variant={tagVariant} size={tagSize} radius={DEFAULT_TAG_RADIUS}>+{hiddenTags.length}</Tag>
349
+ </Tooltip>
350
+ ) : null}
351
+ </>
352
+ )}
353
+ </div>
354
+
355
+ <div ref={measureRef} className={MEASURE_ROW} aria-hidden>
356
+ {tags.map((tag) => (
357
+ <span key={tag.value} data-measure-tag>
358
+ {renderTag(tag, { closable: closable && !disabled })}
359
+ </span>
360
+ ))}
361
+ <span ref={moreMeasureRef}>
362
+ <Tag variant={tagVariant} size={tagSize} radius={DEFAULT_TAG_RADIUS}>+99</Tag>
363
+ </span>
364
+ </div>
365
+ </div>
366
+ {showAiRefresh ? (
367
+ <div className={SUGGESTION_ACTION_ROW}>
368
+ <AiRefreshButton onClick={handleRefreshAiSuggestions} className={AI_REFRESH_VISIBLE_CLASS} />
369
+ </div>
370
+ ) : null}
371
+ {showSuggestion ? (
372
+ <AiSuggestionPanel
373
+ suggestions={suggestionList}
374
+ onSelect={handleAdoptSuggestion}
375
+ getSuggestionKey={(suggestion) => suggestion.id}
376
+ getSuggestionLabel={(suggestion) => suggestion.label}
377
+ getSuggestionAriaLabel={(suggestion, index) => `采纳 AI 推荐标签 ${index + 1}:${suggestion.label}`}
378
+ />
379
+ ) : null}
380
+ </div>
381
+ );
382
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * TagInput — TOKEN_MAP(供平台属性面板展示)
3
+ */
4
+ export const TAGINPUT_TOKEN_MAP = {
5
+ base: [],
6
+ 占位符: [
7
+ { label: '颜色', cssProp: 'color', token: '--text-tertiary', value: '#667085', semanticRef: 'text-tertiary' },
8
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
9
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
10
+ ],
11
+ 折叠项: [
12
+ { label: '组件引用', cssProp: '—', value: 'Tag' },
13
+ { label: '颜色', cssProp: 'variant', value: '继承 tagVariant' },
14
+ { label: '尺寸', cssProp: 'size', value: '继承 tagSize' },
15
+ { label: '圆角', cssProp: 'radius', value: 'md' },
16
+ { label: '内容', cssProp: 'children', value: '+N' },
17
+ ],
18
+ 容器: [
19
+ { label: '默认宽度', cssProp: 'width', token: '--size-input-width', value: '300px' },
20
+ { label: '背景色', cssProp: 'background', token: '--bg-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
21
+ { label: '边框色', cssProp: 'border-color', token: '--border-default', value: 'rgba(45,66,107,0.12)', semanticRef: 'border-default' },
22
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
23
+ { label: '标签间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
24
+ { label: '悬浮边框', cssProp: 'border-color', token: '--color-blueGrey-400', value: '#D0D5DD', state: 'hover' },
25
+ { label: '聚焦边框', cssProp: 'border-color', token: '--status-primary', value: '#56D3BC', semanticRef: 'status-primary', state: 'focus' },
26
+ { label: '错态背景', cssProp: 'background', token: '--color-red-50', value: '#FEF2F1', semanticRef: 'status-danger', state: 'error' },
27
+ { label: '错态聚焦', cssProp: 'border-color', token: '--color-red-500', value: '#F74331', semanticRef: 'status-danger', state: 'error+focus' },
28
+ { label: '禁用背景', cssProp: 'background', token: '--bg-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
29
+ { label: '透明度', cssProp: 'opacity', value: '0.6', state: 'disabled' },
30
+ ],
31
+ sizes: {
32
+ sm: [
33
+ { label: '最小高', cssProp: 'min-height', token: '--size-control-sm', value: '24px' },
34
+ { label: '横内距', cssProp: 'padding-inline', token: '--spacing-2', value: '8px' },
35
+ { label: '纵内距', cssProp: 'padding-block', value: '2px' },
36
+ ],
37
+ md: [
38
+ { label: '最小高', cssProp: 'min-height', token: '--size-control-md', value: '36px' },
39
+ { label: '左内距', cssProp: 'padding-left', token: '--spacing-3', value: '12px' },
40
+ { label: '右内距', cssProp: 'padding-right', token: '--spacing-2', value: '8px' },
41
+ { label: '纵内距', cssProp: 'padding-block', value: '6px' },
42
+ ],
43
+ },
44
+ 引用组件: [
45
+ { label: '可见标签', cssProp: '—', value: 'Tag variant=tagVariant size=tagSize radius=md' },
46
+ { label: '折叠数量', cssProp: '—', value: 'Tag variant=tagVariant size=tagSize radius=md children="+N"' },
47
+ { label: '浮层标签', cssProp: '—', value: 'Tag variant=tagVariant size=tagSize radius=md' },
48
+ { label: '浮窗', cssProp: '—', value: 'Tooltip tone=light placement=top,仅 +N 更多标签场景使用' },
49
+ { label: '浮窗背景', cssProp: 'background', token: '--bg-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
50
+ { label: '浮窗文字', cssProp: 'color', token: '--text-primary', value: '#182230', semanticRef: 'text-primary' },
51
+ ],
52
+ };