@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,133 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import InfoDisplayPanel from './InfoDisplayPanel';
3
+
4
+ const MIN_PREVIEW_WIDTH = 200;
5
+ const CANVAS = 'relative flex h-full min-h-0 w-full items-stretch justify-end overflow-visible';
6
+ const FRAME = 'relative flex h-full min-h-0 max-w-full items-stretch justify-end overflow-visible';
7
+ const RESIZE_HANDLE = [
8
+ 'group/info-panel-preview-resize absolute inset-y-0 left-0 z-30 flex w-4 -translate-x-full cursor-col-resize items-stretch justify-end border-0 bg-transparent p-0',
9
+ 'touch-none select-none',
10
+ 'focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-blueGrey-400',
11
+ ].join(' ');
12
+ const RESIZE_HANDLE_LINE = [
13
+ 'h-full w-px rounded-full bg-transparent transition-colors duration-150',
14
+ 'group-hover/info-panel-preview-resize:bg-brand-400 group-focus-visible/info-panel-preview-resize:bg-brand-500 group-active/info-panel-preview-resize:bg-brand-500',
15
+ ].join(' ');
16
+
17
+ const SINGLE_TAB_PANELS = [
18
+ {
19
+ id: 'profile',
20
+ tabs: [{ id: 'profile', label: '用户信息' }],
21
+ },
22
+ ];
23
+
24
+ function clamp(value, min, max) {
25
+ return Math.min(max, Math.max(min, value));
26
+ }
27
+
28
+ export default function InfoDisplayPanelPreview({ contentCount = 'multi' }) {
29
+ const rootRef = useRef(null);
30
+ const [availableWidth, setAvailableWidth] = useState(0);
31
+ const [frameWidth, setFrameWidth] = useState(null);
32
+ const isSingleTab = contentCount === 'single';
33
+
34
+ useEffect(() => {
35
+ const node = rootRef.current;
36
+ if (!node) return undefined;
37
+
38
+ function updateWidth() {
39
+ setAvailableWidth(node.getBoundingClientRect().width);
40
+ }
41
+
42
+ updateWidth();
43
+ const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateWidth) : null;
44
+ observer?.observe(node);
45
+ window.addEventListener('resize', updateWidth);
46
+
47
+ return () => {
48
+ observer?.disconnect();
49
+ window.removeEventListener('resize', updateWidth);
50
+ };
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (!Number.isFinite(availableWidth) || availableWidth <= 0) return;
55
+ setFrameWidth(availableWidth);
56
+ }, [availableWidth]);
57
+
58
+ const maxWidth = Number.isFinite(availableWidth) && availableWidth > 0
59
+ ? availableWidth
60
+ : MIN_PREVIEW_WIDTH;
61
+ const currentWidth = clamp(frameWidth ?? maxWidth, MIN_PREVIEW_WIDTH, maxWidth);
62
+
63
+ function updateFrameWidth(nextWidth) {
64
+ setFrameWidth(clamp(nextWidth, MIN_PREVIEW_WIDTH, maxWidth));
65
+ }
66
+
67
+ function handleResizePointerDown(event) {
68
+ event.preventDefault();
69
+ const startX = event.clientX;
70
+ const startWidth = currentWidth;
71
+ const previousCursor = document.body.style.cursor;
72
+ const previousUserSelect = document.body.style.userSelect;
73
+ document.body.style.cursor = 'col-resize';
74
+ document.body.style.userSelect = 'none';
75
+
76
+ function handlePointerMove(moveEvent) {
77
+ const delta = moveEvent.clientX - startX;
78
+ updateFrameWidth(startWidth - delta);
79
+ }
80
+
81
+ function handlePointerUp() {
82
+ document.body.style.cursor = previousCursor;
83
+ document.body.style.userSelect = previousUserSelect;
84
+ window.removeEventListener('pointermove', handlePointerMove);
85
+ window.removeEventListener('pointerup', handlePointerUp);
86
+ }
87
+
88
+ window.addEventListener('pointermove', handlePointerMove);
89
+ window.addEventListener('pointerup', handlePointerUp);
90
+ }
91
+
92
+ function handleResizeKeyDown(event) {
93
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'Home' && event.key !== 'End') return;
94
+ event.preventDefault();
95
+
96
+ if (event.key === 'Home') {
97
+ updateFrameWidth(maxWidth);
98
+ return;
99
+ }
100
+
101
+ if (event.key === 'End') {
102
+ updateFrameWidth(MIN_PREVIEW_WIDTH);
103
+ return;
104
+ }
105
+
106
+ const delta = event.shiftKey ? 32 : 16;
107
+ updateFrameWidth(currentWidth + (event.key === 'ArrowLeft' ? delta : -delta));
108
+ }
109
+
110
+ return (
111
+ <div ref={rootRef} className={CANVAS}>
112
+ <div className={FRAME} style={{ width: `${currentWidth}px` }}>
113
+ <button
114
+ type="button"
115
+ className={RESIZE_HANDLE}
116
+ aria-label="调整信息展示面板整体宽度"
117
+ aria-orientation="vertical"
118
+ aria-valuemin={MIN_PREVIEW_WIDTH}
119
+ aria-valuemax={Math.round(maxWidth)}
120
+ aria-valuenow={Math.round(currentWidth)}
121
+ onPointerDown={handleResizePointerDown}
122
+ onKeyDown={handleResizeKeyDown}
123
+ >
124
+ <span className={RESIZE_HANDLE_LINE} aria-hidden="true" />
125
+ </button>
126
+ <InfoDisplayPanel
127
+ className="!h-full w-full"
128
+ panels={isSingleTab ? SINGLE_TAB_PANELS : undefined}
129
+ />
130
+ </div>
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Input — 单行文本输入(用户自造字符串)
3
+ *
4
+ * **何时用**:名称、搜索词、备注等不可穷举或需键盘录入的值。
5
+ * **不用**:合法值仅来自固定列表 → `Select` 或 `Radio`;纯数字与步进 → `InputNumber`。
6
+ *
7
+ * @prop {'default'|'error'} [status='default'] — 状态
8
+ * @prop {boolean} [disabled=false] — 是否禁用
9
+ * @prop {import('react').ReactNode} [prefix=null] — 前缀
10
+ * @prop {import('react').ReactNode} [suffix=null] — 后缀
11
+ * @prop {boolean} [allowClear=false] — 可清除
12
+ * @prop {string} [aiSuggestion] — 输入框下方单条 AI 推荐文案
13
+ * @prop {string[]} [aiSuggestions] — 输入框下方多条 AI 推荐文案
14
+ * @prop {string} [placeholder='请输入'] — 占位
15
+ * @prop {string} value — 当前值(受控)
16
+ * @prop {string} [defaultValue] — 非受控初值
17
+ * @prop {(e: import('react').ChangeEvent<HTMLInputElement>) => void} [onChange] — 变更回调
18
+ * @prop {() => void} [onClear] — 清除回调
19
+ * @prop {(suggestion: string) => void} [onAdoptSuggestion] — 采纳 AI 推荐回调
20
+ * @prop {() => void} [onRefreshAiSuggestions] — 刷新 AI 推荐回调
21
+ * @prop {string} [className=''] — 类名
22
+ */
23
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
24
+ import AiSuggestionPanel, { AiRefreshButton, buildSuggestionGroupsFromFlatList } from './AiSuggestionShared';
25
+
26
+ /* ── 根容器基础样式 ── */
27
+ const WRAPPER_BASE = [
28
+ 'inline-flex flex-col items-start w-[var(--size-input-width,100%)] max-w-full gap-1',
29
+ '[font-family:inherit]',
30
+ ].join(' ');
31
+
32
+ /* ── 输入框基础样式 ── */
33
+ const FIELD_BASE = [
34
+ 'tfds-input',
35
+ 'group relative inline-flex items-center w-full',
36
+ 'border [border-style:solid] rounded-md',
37
+ 'transition-all duration-150',
38
+ 'outline-none',
39
+ '[font-family:inherit]',
40
+ ].join(' ');
41
+
42
+ /* ── 状态 → 容器背景 / 边框(聚焦仅 1px 描边,聚焦优先级高于悬浮) ── */
43
+ const STATUS_CLASS = {
44
+ default: [
45
+ 'bg-surface border-border-default',
46
+ 'hover:border-border-strong',
47
+ 'focus-within:border-primary',
48
+ 'focus-within:hover:border-primary',
49
+ ].join(' '),
50
+ error: [
51
+ 'bg-red-50 border-border-default',
52
+ 'hover:bg-red-100',
53
+ 'focus-within:border-red-500',
54
+ 'focus-within:hover:border-red-500 focus-within:hover:bg-red-50',
55
+ ].join(' '),
56
+ };
57
+
58
+ /* ── 固定 MD 尺寸 → 高度 / 内边距 / 字号 / 间距 ── */
59
+ const SIZE_CLASS = 'h-[var(--size-control-md)] pl-3 pr-3 text-sm gap-2';
60
+
61
+ /* ── 禁用态(覆盖 status 的颜色,保留当前边框) ── */
62
+ const DISABLED_CLASS = 'bg-disabled border-border-default cursor-not-allowed opacity-60';
63
+
64
+ /* ── 原生 input 样式(透明无边框,继承容器字号;聚焦时 placeholder 更淡) ── */
65
+ const INPUT_BASE = [
66
+ 'flex-1 min-w-0 bg-transparent border-none outline-none',
67
+ 'text-foreground placeholder:text-foreground-muted',
68
+ 'focus:placeholder:text-foreground-disabled',
69
+ '[font-family:inherit] [font-size:inherit] leading-[var(--leading-5)]',
70
+ 'disabled:text-foreground-disabled disabled:placeholder:text-foreground-disabled',
71
+ 'p-0 m-0',
72
+ ].join(' ');
73
+
74
+ /* ── 前后缀插槽 ── */
75
+ const ADDON_CLASS = 'inline-flex size-[16px] items-center justify-center shrink-0 text-foreground-muted [&>svg]:size-[16px]';
76
+
77
+ /* ── 清除按钮(hover 或聚焦时出现) ── */
78
+ const CLEAR_CLASS = [
79
+ 'inline-flex items-center justify-center shrink-0 size-[var(--spacing-4)]',
80
+ 'rounded-full cursor-pointer',
81
+ 'text-foreground-disabled hover:text-foreground-secondary',
82
+ 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-150',
83
+ 'bg-transparent border-none p-0',
84
+ ].join(' ');
85
+
86
+ const TRAILING_CLASS = 'absolute right-3 top-1/2 inline-flex -translate-y-1/2 items-center gap-2';
87
+
88
+ const INPUT_LOCAL_FALLBACK_SUGGESTION_GROUPS = [
89
+ ['您好,这边已经收到您的反馈,正在为您核实处理。'],
90
+ ['您好,问题已记录,这边会尽快同步处理进展。'],
91
+ ];
92
+
93
+ function dispatchNativeInputValue(input, nextValue) {
94
+ if (!input) return;
95
+ const descriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
96
+ descriptor?.set?.call(input, nextValue);
97
+ input.dispatchEvent(new Event('input', { bubbles: true }));
98
+ }
99
+
100
+ function getInputPaddingClasses(hasSuffix, activeButtonCount) {
101
+ const base = hasSuffix ? 'pr-7' : '';
102
+ if (activeButtonCount === 0) return base;
103
+
104
+ if (activeButtonCount === 1) {
105
+ return `${base} group-hover:pr-8 group-focus-within:pr-8`.trim();
106
+ }
107
+ return `${base} group-hover:pr-14 group-focus-within:pr-14`.trim();
108
+ }
109
+
110
+ export default function Input({
111
+ size: _size = 'md',
112
+ status = 'default',
113
+ disabled = false,
114
+ prefix,
115
+ suffix,
116
+ allowClear = false,
117
+ aiSuggestion,
118
+ aiSuggestions,
119
+ placeholder = '请输入',
120
+ value,
121
+ defaultValue,
122
+ onChange,
123
+ onClear,
124
+ onAdoptSuggestion,
125
+ onRefreshAiSuggestions,
126
+ className = '',
127
+ ...rest
128
+ }) {
129
+ const inputRef = useRef(null);
130
+ const isControlled = value !== undefined;
131
+ const [innerValue, setInnerValue] = useState(defaultValue || '');
132
+ const [isSuggestionDismissed, setIsSuggestionDismissed] = useState(false);
133
+ const [suggestionIndex, setSuggestionIndex] = useState(0);
134
+
135
+ const currentValue = isControlled ? value : innerValue;
136
+ const showClear = allowClear && !disabled && Boolean(currentValue);
137
+ const suggestionGroups = useMemo(() => {
138
+ const source = Array.isArray(aiSuggestions) && aiSuggestions.length > 0
139
+ ? aiSuggestions
140
+ : (typeof aiSuggestion === 'string' && aiSuggestion.trim().length > 0 ? [aiSuggestion] : []);
141
+ return buildSuggestionGroupsFromFlatList(source, INPUT_LOCAL_FALLBACK_SUGGESTION_GROUPS);
142
+ }, [aiSuggestion, aiSuggestions]);
143
+ const activeSuggestions = suggestionGroups[suggestionIndex] || suggestionGroups[0] || [];
144
+ const suggestionList = useMemo(
145
+ () => activeSuggestions.map((item, index) => ({
146
+ id: `input-ai-suggestion-${suggestionIndex}-${index}`,
147
+ label: item,
148
+ value: item,
149
+ })),
150
+ [activeSuggestions, suggestionIndex]
151
+ );
152
+ const showSuggestion = !disabled && !isSuggestionDismissed && suggestionList.length > 0;
153
+ const canRefreshLocally = suggestionGroups.length > 1;
154
+ const showAiRefresh = !disabled && suggestionList.length > 0
155
+ && (canRefreshLocally || typeof onRefreshAiSuggestions === 'function');
156
+ const activeButtonCount = (showClear ? 1 : 0) + (showAiRefresh ? 1 : 0);
157
+
158
+ useEffect(() => {
159
+ setIsSuggestionDismissed(false);
160
+ setSuggestionIndex(0);
161
+ }, [aiSuggestion, aiSuggestions]);
162
+
163
+ const handleChange = useCallback((e) => {
164
+ if (!isControlled) setInnerValue(e.target.value);
165
+ onChange?.(e);
166
+ }, [isControlled, onChange]);
167
+
168
+ const handleClear = useCallback(() => {
169
+ if (!isControlled) {
170
+ setInnerValue('');
171
+ if (inputRef.current) inputRef.current.value = '';
172
+ }
173
+ onClear?.();
174
+ inputRef.current?.focus();
175
+ }, [isControlled, onClear]);
176
+
177
+ const handleAdoptSuggestion = useCallback((suggestion) => {
178
+ if (!suggestion?.value) return;
179
+ dispatchNativeInputValue(inputRef.current, suggestion.value);
180
+ onAdoptSuggestion?.(suggestion.value);
181
+ setIsSuggestionDismissed(true);
182
+ inputRef.current?.focus();
183
+ }, [onAdoptSuggestion]);
184
+
185
+ const handleRefreshAiSuggestions = useCallback(() => {
186
+ if (canRefreshLocally) {
187
+ setSuggestionIndex((prev) => (prev + 1) % suggestionGroups.length);
188
+ }
189
+ onRefreshAiSuggestions?.();
190
+ setIsSuggestionDismissed(false);
191
+ inputRef.current?.focus();
192
+ }, [canRefreshLocally, onRefreshAiSuggestions, suggestionGroups.length]);
193
+
194
+ const wrapperCls = [
195
+ WRAPPER_BASE,
196
+ className,
197
+ ].filter(Boolean).join(' ');
198
+
199
+ const fieldCls = [
200
+ FIELD_BASE,
201
+ disabled ? DISABLED_CLASS : STATUS_CLASS[status],
202
+ SIZE_CLASS,
203
+ ].filter(Boolean).join(' ');
204
+
205
+ const inputCls = [
206
+ INPUT_BASE,
207
+ getInputPaddingClasses(Boolean(suffix), activeButtonCount),
208
+ ].filter(Boolean).join(' ');
209
+
210
+ return (
211
+ <div className={wrapperCls}>
212
+ <div className={fieldCls} data-tfds-component="Input">
213
+ {prefix ? <span className={ADDON_CLASS}>{prefix}</span> : null}
214
+ <input
215
+ ref={inputRef}
216
+ className={inputCls}
217
+ placeholder={placeholder}
218
+ disabled={disabled}
219
+ value={isControlled ? value : undefined}
220
+ defaultValue={isControlled ? undefined : defaultValue}
221
+ onChange={handleChange}
222
+ {...rest}
223
+ />
224
+ {(showClear || showAiRefresh || suffix) ? (
225
+ <span className={TRAILING_CLASS}>
226
+ {showClear ? (
227
+ <button
228
+ type="button"
229
+ className={CLEAR_CLASS}
230
+ onClick={handleClear}
231
+ aria-label="清除"
232
+ tabIndex={-1}
233
+ >
234
+ <svg viewBox="0 0 16 16" fill="none" className="size-[16px]">
235
+ <circle cx="8" cy="8" r="7" fill="currentColor" fillOpacity="0.15" />
236
+ <path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
237
+ </svg>
238
+ </button>
239
+ ) : null}
240
+ {showAiRefresh ? (
241
+ <AiRefreshButton onClick={handleRefreshAiSuggestions} />
242
+ ) : null}
243
+ {suffix ? <span className={ADDON_CLASS}>{suffix}</span> : null}
244
+ </span>
245
+ ) : null}
246
+ </div>
247
+ {showSuggestion ? (
248
+ <AiSuggestionPanel
249
+ suggestions={suggestionList}
250
+ onSelect={handleAdoptSuggestion}
251
+ getSuggestionKey={(suggestion) => suggestion.id}
252
+ getSuggestionLabel={(suggestion) => suggestion.label}
253
+ getSuggestionAriaLabel={(_, index) => `采纳 AI 推荐内容 ${index + 1}`}
254
+ />
255
+ ) : null}
256
+ </div>
257
+ );
258
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Input — TOKEN_MAP(供平台属性面板展示)
3
+ */
4
+ export const INPUT_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
+ { label: '聚焦色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled', state: 'focus' },
13
+ ],
14
+ 禁用文字: [
15
+ { label: '颜色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled' },
16
+ ],
17
+ 前后缀: [
18
+ { label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
19
+ { label: '图标尺寸', cssProp: 'width / height', token: '--spacing-4', value: '16px' },
20
+ ],
21
+ AI推荐列表: [
22
+ { label: '上间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
23
+ { label: '列表圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
24
+ { label: '列表内边距', cssProp: 'padding', token: '--spacing-1', value: '4px' },
25
+ { label: '条目间距', cssProp: 'gap', token: '--spacing-0_5', value: '2px' },
26
+ { 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%)' },
27
+ { label: '条目圆角', cssProp: 'border-radius', token: '--radius-sm', value: '6px' },
28
+ { label: '条目横向内边距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
29
+ { label: '条目纵向内边距', cssProp: 'padding-block', token: '--spacing-1', value: '4px' },
30
+ { 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' },
31
+ { label: '文字颜色', cssProp: 'color', token: '--color-foreground-secondary', value: '#475467', semanticRef: 'text-secondary' },
32
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
33
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
34
+ ],
35
+ 刷新推荐按钮: [
36
+ { label: '图标尺寸', cssProp: 'width / height', token: '--spacing-4', value: '16px' },
37
+ { 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' },
38
+ ],
39
+ 容器: [
40
+ { label: '默认宽度', cssProp: 'width', token: '--size-input-width', value: '300px' },
41
+ { label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
42
+ { label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
43
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
44
+ { label: '悬浮边框', cssProp: 'border-color', token: '--color-blueGrey-400', value: '#D0D5DD', state: 'hover' },
45
+ { label: '聚焦边框', cssProp: 'border-color', token: '--color-primary', value: '#56D3BC', semanticRef: 'status-primary', state: 'focus' },
46
+ { label: '错态背景', cssProp: 'background', token: '--color-red-50', value: '#FEF2F1', semanticRef: 'status-danger', state: 'error' },
47
+ { label: '错态悬浮', cssProp: 'background', token: '--color-red-100', value: '#FCD9D7', state: 'error+hover' },
48
+ { label: '错态聚焦', cssProp: 'border-color', token: '--color-red-500', value: '#F74331', semanticRef: 'status-danger', state: 'error+focus' },
49
+ { label: '禁用背景', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
50
+ { label: '透明度', cssProp: 'opacity', value: '0.6', state: 'disabled' },
51
+ ],
52
+ sizes: {
53
+ sm: [
54
+ { label: '高度', cssProp: 'height', token: '--size-control-sm', value: '24px' },
55
+ { label: '左内边距', cssProp: 'padding-left', token: '--spacing-2', value: '8px' },
56
+ { label: '右内边距', cssProp: 'padding-right', token: '--spacing-2', value: '8px' },
57
+ { label: '字号', cssProp: 'font-size', token: '--text-xs', value: '12px' },
58
+ { label: '元素间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
59
+ ],
60
+ md: [
61
+ { label: '高度', cssProp: 'height', token: '--size-control-md', value: '36px' },
62
+ { label: '左内边距', cssProp: 'padding-left', token: '--spacing-3', value: '12px' },
63
+ { label: '右内边距', cssProp: 'padding-right', token: '--spacing-3', value: '12px' },
64
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
65
+ { label: '元素间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
66
+ ],
67
+ },
68
+ };