@tfdesign/b-end 1.0.5 → 1.0.7
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.
- package/AI_READ_FIRST.md +13 -18
- package/README.md +3 -2
- package/package.json +1 -1
- package/scripts/check-tfds-integration.mjs +8 -3
- package/scripts/postinstall-cursor-skill.mjs +7 -7
- package/scripts/setup.mjs +1 -1
- package/skills/tfds/CHECKLIST.md +9 -4
- package/skills/tfds/COMMON_FAILURES.md +3 -3
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +46 -19
- package/skills/tfds/LAYOUT_RULES.md +1 -1
- package/skills/tfds/PAGE_ARCHETYPES.md +3 -0
- package/skills/tfds/SKILL.md +10 -10
- package/skills/tfds/components.index.json +11 -29
- package/skills/tfds/components.summary.json +4 -4
- package/src/_b_end_runtime/components/TagInput.jsx +34 -172
- package/src/_b_end_runtime/components/TextArea.tokens.js +2 -2
- package/src/_b_end_runtime/components/Toast.jsx +1 -1
- package/src/_b_end_runtime/components/Toast.tokens.js +2 -2
- package/src/_b_end_runtime/components.js +10 -12
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +5 -5
- package/src/_b_end_runtime/preview-registry.jsx +186 -146
- package/src/index.d.ts +0 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"system": "b-end",
|
|
3
3
|
"skill": "tfds",
|
|
4
|
-
"generatedAt": "2026-05-
|
|
4
|
+
"generatedAt": "2026-05-10T14:01:13.258Z",
|
|
5
5
|
"purpose": "轻量组件与页面模板目录。AI 先读本文件做选型;确定命中组件后,再到 components.index.json 按 id 读取 props / rules / examples。",
|
|
6
6
|
"counts": {
|
|
7
7
|
"total": 46,
|
|
@@ -344,7 +344,7 @@
|
|
|
344
344
|
"多面板工作区",
|
|
345
345
|
"panel tabs"
|
|
346
346
|
],
|
|
347
|
-
"ruleCount":
|
|
347
|
+
"ruleCount": 26,
|
|
348
348
|
"exampleCount": 8,
|
|
349
349
|
"hasCode": false,
|
|
350
350
|
"detailRef": "components.index.json#info-display-panel"
|
|
@@ -631,7 +631,7 @@
|
|
|
631
631
|
"top 布局",
|
|
632
632
|
"left 布局"
|
|
633
633
|
],
|
|
634
|
-
"ruleCount":
|
|
634
|
+
"ruleCount": 17,
|
|
635
635
|
"exampleCount": 8,
|
|
636
636
|
"hasCode": false,
|
|
637
637
|
"detailRef": "components.index.json#form"
|
|
@@ -1236,7 +1236,7 @@
|
|
|
1236
1236
|
"border rounded p-2 tag",
|
|
1237
1237
|
"自渲染 Tag 列表"
|
|
1238
1238
|
],
|
|
1239
|
-
"ruleCount":
|
|
1239
|
+
"ruleCount": 9,
|
|
1240
1240
|
"exampleCount": 8,
|
|
1241
1241
|
"hasCode": false,
|
|
1242
1242
|
"detailRef": "components.index.json#tag-input"
|
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
* @prop {Array<string|{value:string,label:string,variant?:string}>} [defaultValue=null] — 非受控初始标签
|
|
13
13
|
* @prop {(nextTags:Array, removedTag?:object) => void} [onChange=null] — 标签列表变化
|
|
14
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
15
|
* @prop {boolean} [closable=true] — 标签是否可关闭
|
|
20
16
|
* @prop {'brand'|'red'|'orange'|'yellow'|'green'|'cyan'|'blue'|'purple'|'pink'|'teal'|'grey'|'white'} [tagVariant='grey'] — 默认 Tag 颜色
|
|
21
17
|
* @prop {'s'|'m'|'l'} [tagSize='m'] — Tag 尺寸
|
|
@@ -26,11 +22,6 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
|
|
26
22
|
import Tag from './Tag';
|
|
27
23
|
import { DEFAULT_TAG_RADIUS, DEFAULT_TAG_SIZE, DEFAULT_TAG_VARIANT } from './tagShared';
|
|
28
24
|
import Tooltip from './Tooltip';
|
|
29
|
-
import AiSuggestionPanel, {
|
|
30
|
-
AiRefreshButton,
|
|
31
|
-
AI_REFRESH_VISIBLE_CLASS,
|
|
32
|
-
rotateList,
|
|
33
|
-
} from './AiSuggestionShared';
|
|
34
25
|
|
|
35
26
|
/* ── 根容器基础样式 ── */
|
|
36
27
|
const BASE = [
|
|
@@ -67,14 +58,6 @@ const DISABLED_CLASS = 'bg-disabled border-border cursor-not-allowed opacity-60'
|
|
|
67
58
|
const CONTENT_ROW = 'flex min-w-0 flex-1 items-center gap-1 overflow-hidden';
|
|
68
59
|
const PLACEHOLDER = 'min-w-0 truncate text-sm leading-5 text-foreground-muted';
|
|
69
60
|
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
61
|
|
|
79
62
|
function normalizeTag(item, index, tagVariant) {
|
|
80
63
|
if (item && typeof item === 'object') {
|
|
@@ -120,75 +103,6 @@ function getVisibleCount(tagWidths, moreWidth, availableWidth) {
|
|
|
120
103
|
return tagWidths.length;
|
|
121
104
|
}
|
|
122
105
|
|
|
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
106
|
export default function TagInput({
|
|
193
107
|
size: _size = 'md',
|
|
194
108
|
status = 'default',
|
|
@@ -198,10 +112,6 @@ export default function TagInput({
|
|
|
198
112
|
defaultValue = null,
|
|
199
113
|
onChange = null,
|
|
200
114
|
onRemove = null,
|
|
201
|
-
onAdoptSuggestion = null,
|
|
202
|
-
onRefreshAiSuggestions = null,
|
|
203
|
-
aiSuggestion,
|
|
204
|
-
aiSuggestions,
|
|
205
115
|
closable = true,
|
|
206
116
|
tagVariant = DEFAULT_TAG_VARIANT,
|
|
207
117
|
tagSize = DEFAULT_TAG_SIZE,
|
|
@@ -214,31 +124,15 @@ export default function TagInput({
|
|
|
214
124
|
const isControlled = value !== undefined;
|
|
215
125
|
const [innerValue, setInnerValue] = useState(() => normalizeList(defaultValue, tagVariant));
|
|
216
126
|
const [visibleCount, setVisibleCount] = useState(null);
|
|
217
|
-
const [isSuggestionDismissed, setIsSuggestionDismissed] = useState(false);
|
|
218
|
-
const [suggestionIndex, setSuggestionIndex] = useState(0);
|
|
219
127
|
|
|
220
128
|
useEffect(() => {
|
|
221
129
|
if (!isControlled) setInnerValue(normalizeList(defaultValue, tagVariant));
|
|
222
130
|
}, [defaultValue, isControlled, tagVariant]);
|
|
223
131
|
|
|
224
|
-
useEffect(() => {
|
|
225
|
-
setIsSuggestionDismissed(false);
|
|
226
|
-
setSuggestionIndex(0);
|
|
227
|
-
}, [aiSuggestion, aiSuggestions]);
|
|
228
|
-
|
|
229
132
|
const tags = useMemo(
|
|
230
133
|
() => (isControlled ? normalizeList(value, tagVariant) : innerValue),
|
|
231
134
|
[innerValue, isControlled, tagVariant, value],
|
|
232
135
|
);
|
|
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
136
|
|
|
243
137
|
const measure = useCallback(() => {
|
|
244
138
|
const root = rootRef.current;
|
|
@@ -274,22 +168,6 @@ export default function TagInput({
|
|
|
274
168
|
onRemove?.(tag);
|
|
275
169
|
}, [commit, disabled, onRemove, tags]);
|
|
276
170
|
|
|
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
171
|
const safeVisibleCount = visibleCount == null ? tags.length : Math.min(visibleCount, tags.length);
|
|
294
172
|
const visibleTags = tags.slice(0, safeVisibleCount);
|
|
295
173
|
const hiddenTags = tags.slice(safeVisibleCount);
|
|
@@ -324,59 +202,43 @@ export default function TagInput({
|
|
|
324
202
|
);
|
|
325
203
|
|
|
326
204
|
return (
|
|
327
|
-
<div className={
|
|
328
|
-
<div
|
|
329
|
-
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
</div>
|
|
205
|
+
<div ref={rootRef} className={rootCls} aria-disabled={disabled || undefined} data-tfds-component="TagInput" {...rest}>
|
|
206
|
+
<div className={CONTENT_ROW}>
|
|
207
|
+
{tags.length === 0 ? (
|
|
208
|
+
<span className={PLACEHOLDER}>{placeholder}</span>
|
|
209
|
+
) : (
|
|
210
|
+
<>
|
|
211
|
+
{visibleTags.map((tag) => (
|
|
212
|
+
renderTag(tag, {
|
|
213
|
+
closable: closable && !disabled,
|
|
214
|
+
onClose: closable && !disabled ? () => handleRemove(tag) : undefined,
|
|
215
|
+
})
|
|
216
|
+
))}
|
|
217
|
+
{hiddenTags.length > 0 ? (
|
|
218
|
+
<Tooltip
|
|
219
|
+
content={hiddenContent}
|
|
220
|
+
tone="light"
|
|
221
|
+
placement="top"
|
|
222
|
+
triggerClassName="inline-flex shrink-0"
|
|
223
|
+
className="!max-w-[280px]"
|
|
224
|
+
>
|
|
225
|
+
<Tag variant={tagVariant} size={tagSize} radius={DEFAULT_TAG_RADIUS}>+{hiddenTags.length}</Tag>
|
|
226
|
+
</Tooltip>
|
|
227
|
+
) : null}
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
354
231
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
</span>
|
|
360
|
-
))}
|
|
361
|
-
<span ref={moreMeasureRef}>
|
|
362
|
-
<Tag variant={tagVariant} size={tagSize} radius={DEFAULT_TAG_RADIUS}>+99</Tag>
|
|
232
|
+
<div ref={measureRef} className={MEASURE_ROW} aria-hidden>
|
|
233
|
+
{tags.map((tag) => (
|
|
234
|
+
<span key={tag.value} data-measure-tag>
|
|
235
|
+
{renderTag(tag, { closable: closable && !disabled })}
|
|
363
236
|
</span>
|
|
364
|
-
|
|
237
|
+
))}
|
|
238
|
+
<span ref={moreMeasureRef}>
|
|
239
|
+
<Tag variant={tagVariant} size={tagSize} radius={DEFAULT_TAG_RADIUS}>+99</Tag>
|
|
240
|
+
</span>
|
|
365
241
|
</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
242
|
</div>
|
|
381
243
|
);
|
|
382
244
|
}
|
|
@@ -54,12 +54,12 @@ export const TEXTAREA_TOKEN_MAP = {
|
|
|
54
54
|
{ label: '分割线', cssProp: 'border-right-color', value: 'none' },
|
|
55
55
|
{ label: '数字色', cssProp: 'color', token: '--color-blueGrey-400', value: '#D0D5DD', semanticRef: 'text-tertiary-weak' },
|
|
56
56
|
{ label: '字体', cssProp: 'font-family', value: 'monospace' },
|
|
57
|
-
{ label: '行高', cssProp: 'line-height', value: '
|
|
57
|
+
{ label: '行高', cssProp: 'line-height', value: '20px' },
|
|
58
58
|
{ label: '内边距', cssProp: 'padding', value: '16px 12px' },
|
|
59
59
|
],
|
|
60
60
|
代码正文: [
|
|
61
61
|
{ label: '字体', cssProp: 'font-family', value: 'monospace' },
|
|
62
|
-
{ label: '行高', cssProp: 'line-height', value: '
|
|
62
|
+
{ label: '行高', cssProp: 'line-height', value: '20px' },
|
|
63
63
|
{ label: '内边距', cssProp: 'padding', value: '16px' },
|
|
64
64
|
],
|
|
65
65
|
};
|
|
@@ -1286,6 +1286,8 @@ export const COMPONENTS = [
|
|
|
1286
1286
|
FONT_WEIGHT_RUNTIME_RULE,
|
|
1287
1287
|
'【组件定位】InfoDisplayPanel 是“信息展示面板框架”,只负责外框、顶部 Tabs、拆分/合并、分栏调宽、内容插槽和基础间距;它不提供业务内容默认占位,不替代 Card、Table、Form、ConversationList 或客服工作台整页框架。',
|
|
1288
1288
|
'【触发引用】当页面需要在同一工作区右侧并列查看“托管助手 / 历史工单 / 工单日志 / 信息工具 / 视频信息 / 用户信息 / 沟通记录”等多个详情面板,并且用户需要把当前关注的 tab 临时拆出来并排对照时,优先使用 InfoDisplayPanel;如果只是单块内容展示或普通 tabs 切换,不应使用该业务框架。',
|
|
1289
|
+
'【客服业务优先规则】只要页面触及客服业务、客服工作台、客服接待、工单处理、售后处理、订单/商品上下文核对、客服资料查看等场景,并需要展示客服工单信息、商品信息、订单信息、用户信息、客服相关信息或沟通记录等辅助信息板块,默认优先使用 InfoDisplayPanel 承载这些信息区;禁止用多个 Card、div 白卡或自制 tabs 临时拼出客服信息侧栏。',
|
|
1290
|
+
'【客服分类 Tabs】客服信息区涉及多个分类时,必须通过 InfoDisplayPanel 顶部 Tabs 切换分类,常见分类包括“工单信息 / 商品信息 / 订单信息 / 用户信息 / 客服信息 / 沟通记录 / 处理日志 / 风险提示”。分类名称、顺序和内容必须来自当前业务上下文;不要把这些示例硬编码成所有页面的固定 tab。',
|
|
1289
1291
|
'【适用场景】客服工作台主白卡内部的信息区、在线 Agent 辅助信息区、工单处理详情侧、多来源上下文对照区、质检 / 审核 / 申诉处理工作区。',
|
|
1290
1292
|
'【不适用场景】整页客服工作台外框用 CustomerServiceWorkspaceFrame;左侧会话 / 工单队列用 ConversationList;单个业务摘要用 Card;字段型详情或可批量操作数据用 Form / Table。',
|
|
1291
1293
|
'【结构】外层为一个白色圆角容器,默认使用 `w-full` 撑满父容器可用宽度;内部按“拆分栏数组 + 主栏”组织。每一栏都必须包含 56px 顶部 Tabs 栏和内容区;顶部横线只保留 Header 内 1 条通栏下描边,左右贴满当前栏容器。Tabs 交互、文字、选中态与 3px Brand 指示线必须复用基础 `Tabs variant="line" size="lg"`,禁止手写 tab button 或手写选中线。栏与栏之间用 1px 垂直分隔线,禁止通过 gap 制造分栏空隙。',
|
|
@@ -1972,8 +1974,10 @@ export const COMPONENTS = [
|
|
|
1972
1974
|
'【组件定位】Form 是字段组合层,只管理 label、提示、错误反馈、top/left 布局和字段编排,不重新定义已有表单控件样式',
|
|
1973
1975
|
'【控件复用】已有基础组件必须复用:input 复用 Input,textarea 复用 TextArea(仅传基组件已有 props,默认 minRows=3、resize 纵向增高与 TextArea 一致;System Prompt / JSON / 代码参数字段必须传 variant="code"),input-number 复用 InputNumber,select 复用 Select,tag-input 复用 TagInput,checkbox 复用 CheckboxGroup,radio 复用 RadioGroup,switch 复用 Switch,date-picker 复用 DatePicker,time-picker 复用 TimePicker,slider 复用 Slider,upload 复用 Upload',
|
|
1974
1976
|
'【布局】Form 在页面主内容区、白色卡片容器与纵向堆叠场景中默认 `w-full min-w-0 self-stretch`;labelPosition="top" 时字段容器与控件区都默认全宽,label 与控件间距 4px;labelPosition="left" 时标签列固定 96px、间距 24px、右侧控件区使用 `flex-1 min-w-0 w-full` 撑满剩余宽度',
|
|
1977
|
+
'【表单宽度铁律】页面主表单、白卡表单、抽屉/弹窗正文表单、侧栏详情表单中,Input / Select / TextArea / InputNumber / DatePicker / TimePicker / TagInput 必须默认撑满字段内容列,且同一区域左右边界对齐;只有筛选栏、工具条、紧凑查询区允许固定宽 160/200/240px,禁止把 300px 示意宽或筛选栏固定宽套到业务表单正文',
|
|
1978
|
+
'【自定义控件宽度】使用 children 或 item.control 传入自定义字段时,外层必须补 `className="w-full min-w-0 max-w-full"`;否则会绕过 Form 内置的 `!w-full` 控件宽度约束,导致 Input、Select、TextArea 等宽度不一致',
|
|
1975
1979
|
'【详情页全集预览】组件详情页的“全集”分类按字段类型从上到下单列排列,每个组件间距 40px,超出预览画布时在画布内滚动查看',
|
|
1976
|
-
'【字段配置】items 数组每项支持 id、label、type、required、labelPosition、middleHelpText、helpText、error、errorText、disabled、value/defaultValue/onChange 等字段;当字段命中 AI 推荐语义时,可继续透传 aiSuggestion、aiSuggestions、onAdoptSuggestion、onRefreshAiSuggestions 到 Input / Select / TextArea
|
|
1980
|
+
'【字段配置】items 数组每项支持 id、label、type、required、labelPosition、middleHelpText、helpText、error、errorText、disabled、value/defaultValue/onChange 等字段;当字段命中 AI 推荐语义时,可继续透传 aiSuggestion、aiSuggestions、onAdoptSuggestion、onRefreshAiSuggestions 到 Input / Select / TextArea',
|
|
1977
1981
|
'【标题附加样式】单字段标题支持 labelOptional、labelRequired、labelAi、labelHelp 四个布尔开关,可与纯文字标题组合显示;渲染顺序固定为 可选 -> 必填 -> AI -> Help',
|
|
1978
1982
|
'【标题字重】字段标题、必填星号与可选文案统一使用 `semibold` token,即 `[font-weight:var(--font-semibold)]`;不要继续使用 `font-bold` 或手写 700 字重',
|
|
1979
1983
|
'【AI 标签】当 labelAi=true 时,必须复用标准 `<Tag variant="ai" radius="full" fontWeight="bold" size="m">AI</Tag>`;禁止在 Form 内手写渐变胶囊、手写圆角块或用普通文字 span 模拟 AI 标签',
|
|
@@ -2215,7 +2219,7 @@ export const COMPONENTS = [
|
|
|
2215
2219
|
_preview: INPUT_NUMBER_PREVIEW,
|
|
2216
2220
|
rules: [
|
|
2217
2221
|
'【⛔ AI 强制使用场景】所有数字类参数字段必须使用 InputNumber,禁止用 `<input type="number">` 或 Input 组件代替。典型场景:Temperature(0~2 精度 0.01)、Top-P(0~1 精度 0.01)、Max Tokens(整数步进)、学习率、权重、阈值等一切数值配置项。原生 input 会产生黑色边框并丢失步进、范围约束、精度控制能力',
|
|
2218
|
-
'【组件定位】InputNumber 是数字输入原子组件,输入框视觉必须复用 Input
|
|
2222
|
+
'【组件定位】InputNumber 是数字输入原子组件,输入框视觉必须复用 Input:表单正文默认随字段列满宽,MD 高 36px、圆角 8px、白底灰边框、聚焦 1px 绿色描边;仅筛选栏/工具条/详情预览示意可由父容器限制为 160/200/240/300px',
|
|
2219
2223
|
'【按钮布局】默认 outer 外置步进按钮:位于输入框右侧,宽 16px、高 36px,与输入框间距 4px,上下两个箭头图标均为 8×8px',
|
|
2220
2224
|
'【内置按钮】innerButtons=true 时步进按钮内嵌在输入框右侧,默认隐藏,仅在容器 hover 或 focus-within 时显示,输入内容需预留右侧空间',
|
|
2221
2225
|
'【禁用态】disabled 时输入框与步进按钮同步进入浅灰底、禁用文字和 0.6 透明度;按钮不可点击',
|
|
@@ -2302,7 +2306,7 @@ export const COMPONENTS = [
|
|
|
2302
2306
|
'【关闭】点击触发器、选项或外部 mousedown(触发器与面板均排除)关闭',
|
|
2303
2307
|
'【键盘】Enter/Space 展开或选中高亮项;↑/↓ 移动高亮;Esc 关闭;Tab 失焦关闭',
|
|
2304
2308
|
'【无障碍】触发器 role="combobox" aria-expanded;列表 role="listbox" 与 role="option"',
|
|
2305
|
-
'【宽度】触发器为 w-full min-w-0 max-w-full
|
|
2309
|
+
'【宽度】触发器为 w-full min-w-0 max-w-full,表单正文默认随字段列满宽;筛选栏、工具条或详情预览示意才允许由父容器限制宽度,禁止在业务表单正文把 Select 固定为 300px 导致与 Input / TextArea 不对齐',
|
|
2306
2310
|
'【右侧图标】下拉箭头、清除按钮与自定义 indicator 均固定为 16×16px',
|
|
2307
2311
|
'【尺寸】仅保留 MD 一种尺寸,触发器高度固定 36px;与 Input 保持一致',
|
|
2308
2312
|
],
|
|
@@ -2792,7 +2796,7 @@ export const COMPONENTS = [
|
|
|
2792
2796
|
'【场景·daterange / datetimerange】表格/列表顶部时间范围筛选、统计报表数据周期选择、合同有效期区间',
|
|
2793
2797
|
'【禁忌】不要用 type="date" 代替 type="datetime"(会丢失时分信息);不要把两个 DatePicker 手拼成范围选择(直接用 type="daterange");不要在极紧凑行内(宽度 < 200px)使用,面板最小宽度 280px',
|
|
2794
2798
|
'【类型说明】date / datetime → 单月面板单值选择;daterange / datetimerange → 双月面板范围选择,范围中间区间浅色连续高亮',
|
|
2795
|
-
'
|
|
2799
|
+
'【宽度】表单正文默认随字段列满宽;筛选栏、工具条或详情预览示意才允许由父容器限制宽度(date / datetime / daterange 常用 300px,datetimerange 常用 400px)。禁止在业务表单正文把 DatePicker 固定为 300px 导致与 Input / Select 不对齐',
|
|
2796
2800
|
'【面板行为】单值点击即选中并关闭;范围需点击两次确定起止后自动关闭',
|
|
2797
2801
|
'【时间栏】datetime / datetimerange 面板底部固定显示 52px 日期/时间信息栏',
|
|
2798
2802
|
'【组合·表单】配 Form 使用时包在 Form.Item(type="date-picker")里;时间范围筛选常配合 Table 顶部工具栏 + "查询"Button 使用',
|
|
@@ -2861,7 +2865,7 @@ export const COMPONENTS = [
|
|
|
2861
2865
|
rules: [
|
|
2862
2866
|
FONT_WEIGHT_RUNTIME_RULE,
|
|
2863
2867
|
'【类型】time 为单时间输入框;timerange 为时间范围输入框',
|
|
2864
|
-
'【触发器】视觉对齐 Figma / DatePicker
|
|
2868
|
+
'【触发器】视觉对齐 Figma / DatePicker:表单正文默认随字段列满宽,详情预览示意可用 300px;36px 高、白底、1px 默认描边、8px 圆角,内容默认左对齐,右侧时钟图标固定为 16×16px',
|
|
2865
2869
|
'【面板】点击 Trigger 展开浮层;面板默认与 Trigger 等宽,最小宽度 150px;单时间面板高 252px,无标题;时间范围面板高 304px,顶部显示开始/结束标题',
|
|
2866
2870
|
'【固定选择区】面板中间固定 36px 高浅绿选择区,时/分列按面板宽度等分并居中对齐;timerange 使用同一条横跨四列的固定选择区,四列都支持真实滚动选择',
|
|
2867
2871
|
'【选择】点击任一时间项会滚动到中间选择区;滚动时/分列时实时同步选择区内最近项,停止后自动吸附',
|
|
@@ -3022,10 +3026,6 @@ export const COMPONENTS = [
|
|
|
3022
3026
|
{ name: 'defaultValue', type: 'array', default: null },
|
|
3023
3027
|
{ name: 'onChange', type: 'function', default: null },
|
|
3024
3028
|
{ name: 'onRemove', type: 'function', default: null },
|
|
3025
|
-
{ name: 'aiSuggestion', type: 'array | string | object', default: null },
|
|
3026
|
-
{ name: 'aiSuggestions', type: 'array', default: null },
|
|
3027
|
-
{ name: 'onAdoptSuggestion', type: 'function', default: null },
|
|
3028
|
-
{ name: 'onRefreshAiSuggestions', type: 'function', default: null },
|
|
3029
3029
|
{ name: 'closable', type: 'boolean', default: true },
|
|
3030
3030
|
{ name: 'tagVariant', type: 'enum', options: ['brand', 'red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple', 'pink', 'teal', 'grey', 'white'], default: 'grey' },
|
|
3031
3031
|
{ name: 'tagSize', type: 'enum', options: ['s', 'm', 'l'], default: 'm' },
|
|
@@ -3051,15 +3051,13 @@ export const COMPONENTS = [
|
|
|
3051
3051
|
},
|
|
3052
3052
|
_preview: TAGINPUT_PREVIEW,
|
|
3053
3053
|
rules: [
|
|
3054
|
-
'【视觉】容器对齐 Figma taginput
|
|
3054
|
+
'【视觉】容器对齐 Figma taginput:表单正文默认随字段列满宽,详情预览示意可用 300px;36px 高、白底、1px 默认描边、8px 圆角,空态显示“请输入”',
|
|
3055
3055
|
'【Tag 复用】已选项、+N 折叠项、Tooltip 内省略项、隐藏测量项都必须使用平台现有 Tag 组件;TagInput 不自绘任何标签视觉',
|
|
3056
3056
|
'【宽度自适应】通过 ResizeObserver 监听容器宽度,按真实 Tag 宽度计算可见数量;宽度变窄自动折叠,变宽自动展开',
|
|
3057
3057
|
'【溢出折叠】可见区域放不下的标签折叠为 +N,+N 固定在末尾并以 Tag 渲染;悬浮 +N 使用白底 tone="light" Tooltip 展示被省略的所有 Tag,该白底样式仅用于 TagInput 更多标签场景',
|
|
3058
3058
|
'【删除】closable=true 时每个可见 Tag 显示关闭按钮,删除后触发 onChange(nextTags, removedTag) 与 onRemove(removedTag)',
|
|
3059
3059
|
'【受控】传 value 时为受控模式,非受控使用 defaultValue;value/defaultValue 支持字符串数组或 { value, label, variant } 对象数组',
|
|
3060
3060
|
'【状态】status="error" 使用浅红底与红色聚焦描边;disabled 叠加禁用背景、透明度并关闭删除操作',
|
|
3061
|
-
'【AI推荐】支持推荐单个标签或推荐标签集合;推荐区展示在 TagInput 下方,默认支持真实点击采纳与刷新推荐,不允许只展示静态建议文案',
|
|
3062
|
-
'【AI推荐采纳】点击推荐后,TagInput 默认按“追加去重”把推荐标签写入当前标签列表,并触发 onAdoptSuggestion(suggestedTags);若传入 onRefreshAiSuggestions,应在本地轮换建议之外继续向外通知',
|
|
3063
3061
|
'【Form】Form 的 type="tag-input" 字段必须完整引用 TagInput,不再用 Select 或原生结构临时代替',
|
|
3064
3062
|
'【尺寸】仅保留 MD 一种尺寸,高度固定 36px;与 Input 保持一致',
|
|
3065
3063
|
],
|
|
@@ -360,7 +360,7 @@ function KnowledgeCard({ item, typeLabel, selected, onClick }) {
|
|
|
360
360
|
>
|
|
361
361
|
<div className="flex items-start justify-between gap-2">
|
|
362
362
|
<p
|
|
363
|
-
className="font-semibold text-blueGrey-900 m-0"
|
|
363
|
+
className="[font-weight:var(--font-semibold)] text-blueGrey-900 m-0"
|
|
364
364
|
style={{
|
|
365
365
|
fontSize: '16px',
|
|
366
366
|
lineHeight: '24px',
|
|
@@ -441,7 +441,7 @@ function DetailPanel({ card, typeLabel, open }) {
|
|
|
441
441
|
<div className="flex items-center justify-between gap-2 shrink-0">
|
|
442
442
|
<div className="flex items-center gap-2 min-w-0">
|
|
443
443
|
<h3
|
|
444
|
-
className="font-semibold text-blueGrey-900 m-0 truncate"
|
|
444
|
+
className="[font-weight:var(--font-semibold)] text-blueGrey-900 m-0 truncate"
|
|
445
445
|
style={{ fontSize: '16px', lineHeight: '24px' }}
|
|
446
446
|
>
|
|
447
447
|
知识详情
|
|
@@ -502,7 +502,7 @@ function DetailPanel({ card, typeLabel, open }) {
|
|
|
502
502
|
<div className="flex flex-col" style={{ gap: '12px' }}>
|
|
503
503
|
<SectionBadge type="question">Questions / 问</SectionBadge>
|
|
504
504
|
<p
|
|
505
|
-
className="font-semibold text-blueGrey-900 m-0"
|
|
505
|
+
className="[font-weight:var(--font-semibold)] text-blueGrey-900 m-0"
|
|
506
506
|
style={{ fontSize: '16px', lineHeight: '26px' }}
|
|
507
507
|
>
|
|
508
508
|
{card.question}
|
|
@@ -556,7 +556,7 @@ function SectionBadge({ type, children }) {
|
|
|
556
556
|
gap: '10px',
|
|
557
557
|
borderRadius: '4px',
|
|
558
558
|
fontSize: '12px',
|
|
559
|
-
fontWeight:
|
|
559
|
+
fontWeight: 'var(--font-semibold)',
|
|
560
560
|
lineHeight: '18px',
|
|
561
561
|
alignSelf: 'flex-start',
|
|
562
562
|
...(isQuestion
|
|
@@ -584,7 +584,7 @@ function ScenePill({ label }) {
|
|
|
584
584
|
>
|
|
585
585
|
<button
|
|
586
586
|
type="button"
|
|
587
|
-
className="inline-flex items-center gap-1 cursor-pointer h-8 px-4 rounded-full bg-transparent border-0 text-sm font-semibold text-blueGrey-900 leading-5"
|
|
587
|
+
className="inline-flex items-center gap-1 cursor-pointer h-8 px-4 rounded-full bg-transparent border-0 text-sm [font-weight:var(--font-semibold)] text-blueGrey-900 leading-5"
|
|
588
588
|
>
|
|
589
589
|
<span>{label}</span>
|
|
590
590
|
<Icon name="chevron-down-stroked" />
|