@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,363 @@
1
+ /**
2
+ * TextArea — 多行文本输入(Input 的多行变体)
3
+ *
4
+ * **何时用**:备注、描述、反馈内容等需换行与较长文本录入的场景。
5
+ * **不用**:单行短文本 → `Input`;合法值来自枚举 → `Select` / `Radio`。
6
+ *
7
+ * **规格**:仅 MD 一种尺寸(与 Input `size="md"` 的字号与内边距一致),不再提供 `size` 配置。
8
+ *
9
+ * @prop {'default'|'code'} [variant='default'] — 输入框类型;code 用于代码 / Prompt 输入,左侧显示行号,使用等宽字体
10
+ * @prop {1|2|3|4|5} [minRows=3] — 默认占位行数(最小高度);1 行时外框 36px 与 Input md 对齐(上下各 8px + 20px 行高),仍可按内容增高
11
+ * @prop {'default'|'error'} [status='default'] — 状态
12
+ * @prop {boolean} [disabled=false] — 是否禁用
13
+ * @prop {number} [maxLength] — 可选字数上限;不传则不限制字数。与 showCount 搭配可展示「当前/上限」或仅「当前」
14
+ * @prop {boolean} [showCount=false] — 是否展示字数;无 maxLength 时仅展示已输入字数
15
+ * @prop {boolean} [enforceMaxLength=true] — 有 maxLength 时是否用原生属性阻止继续输入(false 时仅标红计数)
16
+ * @prop {string} [aiSuggestion] — 单条 AI 推荐草稿
17
+ * @prop {string[]} [aiSuggestions] — 多条 AI 推荐草稿
18
+ * @prop {string} [placeholder='请输入'] — 占位
19
+ * @prop {string} value — 当前值(受控)
20
+ * @prop {string} [defaultValue] — 非受控初值
21
+ * @prop {(e: import('react').ChangeEvent<HTMLTextAreaElement>) => void} [onChange] — 变更回调
22
+ * @prop {(e: import('react').UIEvent<HTMLTextAreaElement>) => void} [onScroll] — 滚动回调
23
+ * @prop {(suggestion: string) => void} [onAdoptSuggestion] — 采纳 AI 推荐回调
24
+ * @prop {() => void} [onRefreshAiSuggestions] — 刷新 AI 推荐回调
25
+ * @prop {'vertical'|'none'} [resize='vertical'] — 高度模式:`vertical` 随内容增高;`none` 固定为默认行高、超出内滚
26
+ * @prop {boolean} [fillHeight=false] — 是否填满父容器高度;用于卡片主编辑区,需配合父容器 `flex-1 min-h-0`
27
+ * @prop {React.CSSProperties} [style] — 内联样式
28
+ * @prop {string} [className=''] — 类名
29
+ */
30
+ import { useState, useCallback, useRef, useLayoutEffect, useEffect, useMemo } from 'react';
31
+ import AiSuggestionPanel, {
32
+ AiRefreshButton,
33
+ AI_REFRESH_VISIBLE_CLASS,
34
+ buildSuggestionGroupsFromFlatList,
35
+ } from './AiSuggestionShared';
36
+
37
+ /* ── 尺寸与最小高度(行高 20px;多行竖向 padding 12px;单行外框 36px 对齐 Input md) ── */
38
+ const LINE_HEIGHT_PX = 20;
39
+ const PADDING_Y_PX = 12;
40
+ const SINGLE_ROW_MIN_PX = 36;
41
+ const CODE_VERTICAL_PADDING_PX = 32;
42
+
43
+ function clampMinRows(raw) {
44
+ const n = Number(raw);
45
+ if (!Number.isFinite(n)) return 3;
46
+ return Math.min(5, Math.max(1, Math.round(n)));
47
+ }
48
+
49
+ function getTextAreaMinContentHeightPx(minRows) {
50
+ const rows = clampMinRows(minRows);
51
+ if (rows === 1) return SINGLE_ROW_MIN_PX;
52
+ return PADDING_Y_PX + rows * LINE_HEIGHT_PX;
53
+ }
54
+
55
+ function getTextAreaVerticalPaddingClass(minRows) {
56
+ return clampMinRows(minRows) === 1 ? 'py-2' : 'py-[6px]';
57
+ }
58
+
59
+ /* ── 根容器 ── */
60
+ const WRAPPER_BASE = [
61
+ 'inline-flex flex-col items-start w-[var(--size-input-width,100%)] max-w-full',
62
+ '[font-family:inherit]',
63
+ ].join(' ');
64
+
65
+ const WRAPPER_FILL_HEIGHT = 'flex-1 min-h-0 w-full';
66
+
67
+ /* ── 外层字段框(边框 + 圆角) ── */
68
+ const FIELD_BASE = [
69
+ 'group relative flex w-full min-w-0 flex-col',
70
+ 'border [border-style:solid] rounded-md',
71
+ 'transition-all duration-150',
72
+ 'outline-none',
73
+ '[font-family:inherit]',
74
+ ].join(' ');
75
+
76
+ const FIELD_FILL_HEIGHT = 'flex-1 min-h-0';
77
+
78
+ /* ── 状态 → 容器背景 / 边框(聚焦仅 1px 描边,聚焦优先级高于悬浮) ── */
79
+ const STATUS_CLASS = {
80
+ default: [
81
+ 'bg-surface border-border-default',
82
+ 'hover:border-border-strong',
83
+ 'focus-within:border-primary',
84
+ 'focus-within:hover:border-primary',
85
+ ].join(' '),
86
+ error: [
87
+ 'bg-red-50 border-border-default',
88
+ 'hover:bg-red-100',
89
+ 'focus-within:border-red-500',
90
+ 'focus-within:hover:border-red-500 focus-within:hover:bg-red-50',
91
+ ].join(' '),
92
+ };
93
+
94
+ /* ── 禁用态(覆盖 status 颜色,保留边框) ── */
95
+ const DISABLED_CLASS = 'bg-disabled border-border-default cursor-not-allowed opacity-60';
96
+
97
+ /* ── 原生 textarea 正文区(透明无边框;正文固定走 14px / 20px token) ── */
98
+ const TEXTAREA_BODY_BASE = [
99
+ 'w-full box-border resize-none bg-transparent border-none outline-none',
100
+ 'px-3 [font-size:var(--text-sm)]',
101
+ 'text-foreground placeholder:text-foreground-muted',
102
+ 'focus:placeholder:text-foreground-disabled',
103
+ '[font-family:inherit] [line-height:var(--leading-5)]',
104
+ 'disabled:text-foreground-disabled disabled:placeholder:text-foreground-disabled disabled:cursor-not-allowed',
105
+ 'm-0',
106
+ ].join(' ');
107
+
108
+ const TEXTAREA_BODY_FILL_HEIGHT = 'flex-1 min-h-0 h-full';
109
+
110
+ /* ── resize 模式 → 滚动 / 自动增高 ── */
111
+ const TEXTAREA_OVERFLOW_CLASS = {
112
+ autogrow: 'overflow-hidden',
113
+ fixed: 'overflow-y-auto',
114
+ };
115
+
116
+ /* ── 字数统计行 ── */
117
+ const COUNT_ROW = 'flex w-full shrink-0 items-center justify-end px-3 pb-[6px] pt-0.5';
118
+
119
+ const COUNT_TEXT_CLASS = 'text-xs leading-4 tabular-nums text-foreground-muted';
120
+ const COUNT_TEXT_DISABLED = 'text-xs leading-4 tabular-nums text-foreground-disabled';
121
+ const COUNT_OVERFLOW_CLASS = 'text-xs leading-4 tabular-nums text-red-500';
122
+ const SUGGESTION_ACTION_ROW = 'flex w-full justify-end';
123
+
124
+ const TEXTAREA_LOCAL_FALLBACK_SUGGESTION_GROUPS = [
125
+ ['您好,这边已根据当前对话内容整理出一版备注草稿,您可以继续补充后保存。'],
126
+ ['根据当前上下文,建议先记录用户诉求、异常现象以及下一步跟进动作。'],
127
+ ];
128
+
129
+ function dispatchNativeTextareaValue(textarea, nextValue) {
130
+ if (!textarea) return;
131
+ const descriptor = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');
132
+ descriptor?.set?.call(textarea, nextValue);
133
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
134
+ }
135
+
136
+ export default function TextArea({
137
+ variant = 'default',
138
+ minRows = 3,
139
+ status = 'default',
140
+ disabled = false,
141
+ maxLength,
142
+ showCount = false,
143
+ enforceMaxLength = true,
144
+ aiSuggestion,
145
+ aiSuggestions,
146
+ placeholder = '请输入',
147
+ value,
148
+ defaultValue,
149
+ onChange,
150
+ onScroll,
151
+ onAdoptSuggestion,
152
+ onRefreshAiSuggestions,
153
+ resize = 'vertical',
154
+ fillHeight = false,
155
+ style,
156
+ className = '',
157
+ ...rest
158
+ }) {
159
+ const isControlled = value !== undefined;
160
+ const [innerValue, setInnerValue] = useState(defaultValue ?? '');
161
+ const textareaRef = useRef(null);
162
+ const gutterInnerRef = useRef(null);
163
+ const [isSuggestionDismissed, setIsSuggestionDismissed] = useState(false);
164
+ const [suggestionIndex, setSuggestionIndex] = useState(0);
165
+ const autogrow = resize === 'vertical';
166
+ const minRowsClamped = clampMinRows(minRows);
167
+ const resolvedVariant = variant === 'code' ? 'code' : 'default';
168
+ const isCodeVariant = resolvedVariant === 'code';
169
+
170
+ const currentValue = isControlled ? (value ?? '') : innerValue;
171
+ const length = typeof currentValue === 'string' ? currentValue.length : 0;
172
+ const hasMax = typeof maxLength === 'number' && maxLength >= 0;
173
+ const showCounter = Boolean(showCount);
174
+ const isOverLimit = hasMax && length > maxLength;
175
+ const nativeMaxLength = hasMax && enforceMaxLength ? maxLength : undefined;
176
+ const shouldFillHeight = Boolean(fillHeight);
177
+ const suggestionGroups = useMemo(() => {
178
+ const source = Array.isArray(aiSuggestions) && aiSuggestions.length > 0
179
+ ? aiSuggestions
180
+ : (typeof aiSuggestion === 'string' && aiSuggestion.trim().length > 0 ? [aiSuggestion] : []);
181
+ return buildSuggestionGroupsFromFlatList(source, TEXTAREA_LOCAL_FALLBACK_SUGGESTION_GROUPS);
182
+ }, [aiSuggestion, aiSuggestions]);
183
+ const activeSuggestions = suggestionGroups[suggestionIndex] || suggestionGroups[0] || [];
184
+ const suggestionList = useMemo(
185
+ () => activeSuggestions.map((item, index) => ({
186
+ id: `textarea-ai-suggestion-${suggestionIndex}-${index}`,
187
+ label: item,
188
+ value: item,
189
+ })),
190
+ [activeSuggestions, suggestionIndex]
191
+ );
192
+ const canRefreshLocally = suggestionGroups.length > 1;
193
+ const showSuggestion = !disabled && !isSuggestionDismissed && suggestionList.length > 0;
194
+ const showAiRefresh = !disabled && suggestionList.length > 0
195
+ && (canRefreshLocally || typeof onRefreshAiSuggestions === 'function');
196
+
197
+ const minContentPx = isCodeVariant
198
+ ? CODE_VERTICAL_PADDING_PX + minRowsClamped * LINE_HEIGHT_PX
199
+ : getTextAreaMinContentHeightPx(minRowsClamped);
200
+
201
+ const handleChange = useCallback((e) => {
202
+ if (!isControlled) setInnerValue(e.target.value);
203
+ onChange?.(e);
204
+ }, [isControlled, onChange]);
205
+
206
+ const handleScroll = useCallback((e) => {
207
+ if (gutterInnerRef.current) {
208
+ gutterInnerRef.current.style.transform = `translateY(-${e.currentTarget.scrollTop}px)`;
209
+ }
210
+ onScroll?.(e);
211
+ }, [onScroll]);
212
+
213
+ useEffect(() => {
214
+ setIsSuggestionDismissed(false);
215
+ setSuggestionIndex(0);
216
+ }, [aiSuggestion, aiSuggestions]);
217
+
218
+ const handleAdoptSuggestion = useCallback((suggestion) => {
219
+ if (!suggestion?.value) return;
220
+ dispatchNativeTextareaValue(textareaRef.current, suggestion.value);
221
+ onAdoptSuggestion?.(suggestion.value);
222
+ setIsSuggestionDismissed(true);
223
+ textareaRef.current?.focus();
224
+ }, [onAdoptSuggestion]);
225
+
226
+ const handleRefreshAiSuggestions = useCallback(() => {
227
+ if (canRefreshLocally) {
228
+ setSuggestionIndex((prev) => (prev + 1) % suggestionGroups.length);
229
+ }
230
+ onRefreshAiSuggestions?.();
231
+ setIsSuggestionDismissed(false);
232
+ textareaRef.current?.focus();
233
+ }, [canRefreshLocally, onRefreshAiSuggestions, suggestionGroups.length]);
234
+
235
+ useLayoutEffect(() => {
236
+ const el = textareaRef.current;
237
+ if (!el) return;
238
+ if (shouldFillHeight) {
239
+ el.style.minHeight = '0px';
240
+ el.style.height = '100%';
241
+ el.style.maxHeight = 'none';
242
+ return;
243
+ }
244
+ if (!autogrow) {
245
+ el.style.minHeight = `${minContentPx}px`;
246
+ el.style.height = `${minContentPx}px`;
247
+ el.style.maxHeight = `${minContentPx}px`;
248
+ return;
249
+ }
250
+ el.style.minHeight = `${minContentPx}px`;
251
+ el.style.maxHeight = 'none';
252
+ const emptySingle =
253
+ minRowsClamped === 1 && String(currentValue).length === 0;
254
+ if (emptySingle) {
255
+ el.style.height = `${minContentPx}px`;
256
+ return;
257
+ }
258
+ el.style.height = 'auto';
259
+ el.style.height = `${Math.max(el.scrollHeight, minContentPx)}px`;
260
+ }, [autogrow, minContentPx, currentValue, showCounter, hasMax, minRowsClamped, shouldFillHeight]);
261
+
262
+ const wrapperCls = [
263
+ WRAPPER_BASE,
264
+ shouldFillHeight ? WRAPPER_FILL_HEIGHT : '',
265
+ className,
266
+ ].filter(Boolean).join(' ');
267
+ const fieldCls = [
268
+ FIELD_BASE,
269
+ shouldFillHeight ? FIELD_FILL_HEIGHT : '',
270
+ disabled ? DISABLED_CLASS : STATUS_CLASS[status],
271
+ ].filter(Boolean).join(' ');
272
+
273
+ const bodyCls = [
274
+ TEXTAREA_BODY_BASE,
275
+ isCodeVariant ? '' : getTextAreaVerticalPaddingClass(minRowsClamped),
276
+ isCodeVariant ? 'px-4 py-4 font-mono' : '',
277
+ shouldFillHeight ? TEXTAREA_BODY_FILL_HEIGHT : '',
278
+ TEXTAREA_OVERFLOW_CLASS[shouldFillHeight ? 'fixed' : (autogrow ? 'autogrow' : 'fixed')],
279
+ ].join(' ');
280
+
281
+ const codeLines = String(currentValue || '').split('\n');
282
+ const lineCount = Math.max(minRowsClamped, codeLines.length || 1);
283
+ const lineNumbers = Array.from({ length: lineCount }, (_, index) => index + 1);
284
+ const codeRowCls = [
285
+ 'flex w-full min-w-0',
286
+ shouldFillHeight ? 'flex-1 min-h-0' : '',
287
+ ].filter(Boolean).join(' ');
288
+ const codeGutterCls = [
289
+ 'relative shrink-0 overflow-hidden bg-transparent px-3 py-4 text-right font-mono [font-size:var(--text-sm)] [line-height:var(--leading-5)]',
290
+ 'tabular-nums text-blueGrey-400 select-none',
291
+ ].join(' ');
292
+ const textareaStyle = style;
293
+
294
+ const countCls = disabled ? COUNT_TEXT_DISABLED : (isOverLimit ? COUNT_OVERFLOW_CLASS : COUNT_TEXT_CLASS);
295
+
296
+ return (
297
+ <div className={[`tfds-textarea`, wrapperCls].filter(Boolean).join(' ')} data-tfds-component="TextArea">
298
+ <div className={fieldCls}>
299
+ {isCodeVariant ? (
300
+ <div className={codeRowCls}>
301
+ <div className={codeGutterCls} aria-hidden="true">
302
+ <div ref={gutterInnerRef}>
303
+ {lineNumbers.map((line) => (
304
+ <div key={line}>{line}</div>
305
+ ))}
306
+ </div>
307
+ </div>
308
+ <textarea
309
+ ref={textareaRef}
310
+ className={bodyCls}
311
+ placeholder={placeholder}
312
+ disabled={disabled}
313
+ value={isControlled ? value : undefined}
314
+ defaultValue={isControlled ? undefined : defaultValue}
315
+ onChange={handleChange}
316
+ onScroll={handleScroll}
317
+ maxLength={nativeMaxLength}
318
+ spellCheck={false}
319
+ {...rest}
320
+ style={textareaStyle}
321
+ />
322
+ </div>
323
+ ) : (
324
+ <textarea
325
+ ref={textareaRef}
326
+ className={bodyCls}
327
+ placeholder={placeholder}
328
+ disabled={disabled}
329
+ value={isControlled ? value : undefined}
330
+ defaultValue={isControlled ? undefined : defaultValue}
331
+ onChange={handleChange}
332
+ onScroll={handleScroll}
333
+ maxLength={nativeMaxLength}
334
+ {...rest}
335
+ style={textareaStyle}
336
+ />
337
+ )}
338
+ {showCounter ? (
339
+ <div className={COUNT_ROW} aria-live="polite">
340
+ <span className={countCls}>
341
+ {length}
342
+ {hasMax ? `/${maxLength}` : null}
343
+ </span>
344
+ </div>
345
+ ) : null}
346
+ </div>
347
+ {showAiRefresh ? (
348
+ <div className={SUGGESTION_ACTION_ROW}>
349
+ <AiRefreshButton onClick={handleRefreshAiSuggestions} className={AI_REFRESH_VISIBLE_CLASS} />
350
+ </div>
351
+ ) : null}
352
+ {showSuggestion ? (
353
+ <AiSuggestionPanel
354
+ suggestions={suggestionList}
355
+ onSelect={handleAdoptSuggestion}
356
+ getSuggestionKey={(suggestion) => suggestion.id}
357
+ getSuggestionLabel={(suggestion) => suggestion.label}
358
+ getSuggestionAriaLabel={(_, index) => `采纳 AI 推荐草稿 ${index + 1}`}
359
+ />
360
+ ) : null}
361
+ </div>
362
+ );
363
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * TextArea — TOKEN_MAP(供平台属性面板展示)
3
+ * 规格固定为 MD:14px 字、横向内边距 12px;纵向 minRows=1 时为 8px×2,≥2 行时为 6px×2。
4
+ */
5
+ export const TEXTAREA_TOKEN_MAP = {
6
+ base: [
7
+ { label: '默认类型', cssProp: 'variant', value: 'default / code(代码或 Prompt 输入,带行号)' },
8
+ ],
9
+ 输入文字: [
10
+ { label: '颜色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
11
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
12
+ ],
13
+ 占位符: [
14
+ { label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
15
+ { label: '聚焦色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled', state: 'focus' },
16
+ ],
17
+ 禁用文字: [
18
+ { label: '颜色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled' },
19
+ ],
20
+ 字数统计: [
21
+ { label: '字号', cssProp: 'font-size', token: '--text-xs', value: '12px' },
22
+ { label: '行高', cssProp: 'line-height', token: '--leading-4', value: '16px' },
23
+ { label: '默认色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
24
+ { label: '超长色', cssProp: 'color', token: '--color-red-500', value: '#F74331', semanticRef: 'status-danger', state: 'overflow' },
25
+ { label: '禁用色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled', state: 'disabled' },
26
+ { label: '下内边距', cssProp: 'padding-bottom', value: '6px' },
27
+ { label: '上内边距', cssProp: 'padding-top', value: '2px' },
28
+ ],
29
+ 容器: [
30
+ { label: '默认宽度', cssProp: 'width', token: '--size-input-width', value: '300px' },
31
+ { label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
32
+ { label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
33
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
34
+ { label: '悬浮边框', cssProp: 'border-color', token: '--color-blueGrey-400', value: '#D0D5DD', state: 'hover' },
35
+ { label: '聚焦边框', cssProp: 'border-color', token: '--color-primary', value: '#56D3BC', semanticRef: 'status-primary', state: 'focus' },
36
+ { label: '错态背景', cssProp: 'background', token: '--color-red-50', value: '#FEF2F1', semanticRef: 'status-danger', state: 'error' },
37
+ { label: '错态悬浮', cssProp: 'background', token: '--color-red-100', value: '#FCD9D7', state: 'error+hover' },
38
+ { label: '错态聚焦', cssProp: 'border-color', token: '--color-red-500', value: '#F74331', semanticRef: 'status-danger', state: 'error+focus' },
39
+ { label: '禁用背景', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
40
+ { label: '透明度', cssProp: 'opacity', value: '0.6', state: 'disabled' },
41
+ ],
42
+ 默认高度: [
43
+ { label: '规则', cssProp: 'min-height', value: '≥2 行:12px + minRows×20px;minRows=1:36px(8+20+8,与 Input md 同高)' },
44
+ { label: '行数档位', cssProp: 'minRows', value: '1 / 2 / 3 / 4 / 5(默认 3)' },
45
+ { label: '撑满模式', cssProp: 'height', value: 'fillHeight=true 时填满父容器剩余高度,内容在 TextArea 内部滚动' },
46
+ ],
47
+ 正文区: [
48
+ { label: '横向内边距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
49
+ { label: '纵向内边距', cssProp: 'padding-block', value: 'minRows=1 时 8px×2;≥2 行时 6px×2' },
50
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
51
+ ],
52
+ 代码行号: [
53
+ { label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
54
+ { label: '分割线', cssProp: 'border-right-color', value: 'none' },
55
+ { label: '数字色', cssProp: 'color', token: '--color-blueGrey-400', value: '#D0D5DD', semanticRef: 'text-tertiary-weak' },
56
+ { label: '字体', cssProp: 'font-family', value: 'monospace' },
57
+ { label: '行高', cssProp: 'line-height', value: '24px' },
58
+ { label: '内边距', cssProp: 'padding', value: '16px 12px' },
59
+ ],
60
+ 代码正文: [
61
+ { label: '字体', cssProp: 'font-family', value: 'monospace' },
62
+ { label: '行高', cssProp: 'line-height', value: '24px' },
63
+ { label: '内边距', cssProp: 'padding', value: '16px' },
64
+ ],
65
+ };