@tfdesign/b-end 1.0.16 → 1.0.18
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/package.json +1 -1
- package/skills/tfds/CHECKLIST.md +1 -1
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +123 -4
- package/skills/tfds/LAYOUT_RECIPES.md +5 -2
- package/skills/tfds/LAYOUT_RULES.md +52 -29
- package/skills/tfds/components.index.json +33 -9
- package/skills/tfds/components.summary.json +12 -10
- package/src/_b_end_runtime/components/Filter.jsx +60 -27
- package/src/_b_end_runtime/components/NavBar.jsx +1 -1
- package/src/_b_end_runtime/components/NavBar.tokens.js +1 -0
- package/src/_b_end_runtime/components/Select.jsx +18 -12
- package/src/_b_end_runtime/components/Select.tokens.js +6 -2
- package/src/_b_end_runtime/components/Table.jsx +7 -7
- package/src/_b_end_runtime/components.js +17 -9
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +882 -83
- package/src/_b_end_runtime/preview-registry.jsx +27 -9
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filter — B 端筛选胶囊项
|
|
3
3
|
*
|
|
4
|
-
* 用于筛选栏中的单个触发器/已选筛选条件;传入 options
|
|
4
|
+
* 用于筛选栏中的单个触发器/已选筛选条件;传入 options 时内置下拉面板。
|
|
5
5
|
*
|
|
6
6
|
* @prop {string} [label='筛选项'] — 筛选项名称
|
|
7
7
|
* @prop {string|number|null} [value=null] — 已选值,存在时显示在 label 后
|
|
8
|
-
* @prop {Array<{label:string,value:string|number,disabled?:boolean}>} [options=[]] —
|
|
9
|
-
* @prop {
|
|
10
|
-
* @prop {Array<string|number
|
|
11
|
-
* @prop {
|
|
8
|
+
* @prop {Array<{label:string,value:string|number,disabled?:boolean}>} [options=[]] — 下拉选项
|
|
9
|
+
* @prop {boolean} [multiple=true] — 是否多选;`false` 进入单选模式(点击即提交并关闭面板,行尾用 ✓ 而非 checkbox 方框,触发器仅显示 1 项 label,aria-multiselectable=false)
|
|
10
|
+
* @prop {Array<string|number>|string|number} [selectedValues] — 受控值;多选传数组、单选传单值(也兼容单元素数组)
|
|
11
|
+
* @prop {Array<string|number>|string|number} [defaultValue=[]] — 非受控初始值
|
|
12
|
+
* @prop {function|null} [onChange=null] — 变更回调;多选返回数组,单选返回单值(或 null 已清空)
|
|
12
13
|
* @prop {boolean} [selected=false] — 品牌色选中态
|
|
13
14
|
* @prop {boolean} [filled=false] — 中性填充态,适合 hover/轻强调容器
|
|
14
15
|
* @prop {boolean} [disabled=false] — 禁用态
|
|
15
|
-
* @prop {boolean} [closable=false] —
|
|
16
|
+
* @prop {boolean} [closable=false] — 有展示值时是否显示清除图标;已选值默认显示清除入口
|
|
16
17
|
* @prop {function|null} [onClear=null] — 清除回调
|
|
17
18
|
* @prop {string} [className=''] — 附加类名
|
|
18
19
|
*/
|
|
@@ -79,7 +80,7 @@ const CLEAR_CLASS = [
|
|
|
79
80
|
'rounded-full cursor-pointer',
|
|
80
81
|
'text-foreground-disabled hover:text-foreground-secondary',
|
|
81
82
|
'transition-opacity duration-150',
|
|
82
|
-
'bg-transparent border-none p-0 mr-1',
|
|
83
|
+
'bg-transparent border-none p-0 mr-[var(--spacing-1)]',
|
|
83
84
|
].join(' ');
|
|
84
85
|
const PANEL_CLASS = [
|
|
85
86
|
'rounded-[var(--radius-md)] border border-solid border-border-default',
|
|
@@ -150,8 +151,11 @@ function normalizeOptions(options) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
function normalizeValueList(raw) {
|
|
153
|
-
if (
|
|
154
|
-
|
|
154
|
+
if (raw === undefined || raw === null || raw === '') return [];
|
|
155
|
+
if (Array.isArray(raw)) {
|
|
156
|
+
return raw.filter((item) => item !== undefined && item !== null && item !== '');
|
|
157
|
+
}
|
|
158
|
+
return [raw];
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
function measurePanelPosition(triggerEl) {
|
|
@@ -172,8 +176,9 @@ export default function Filter({
|
|
|
172
176
|
label = '筛选项',
|
|
173
177
|
value = null,
|
|
174
178
|
options = [],
|
|
179
|
+
multiple = true,
|
|
175
180
|
selectedValues,
|
|
176
|
-
defaultValue
|
|
181
|
+
defaultValue,
|
|
177
182
|
onChange = null,
|
|
178
183
|
selected = false,
|
|
179
184
|
filled = false,
|
|
@@ -191,7 +196,12 @@ export default function Filter({
|
|
|
191
196
|
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
|
|
192
197
|
const hasDropdown = normalizedOptions.length > 0;
|
|
193
198
|
const isControlled = selectedValues !== undefined;
|
|
194
|
-
const
|
|
199
|
+
const initialValues = useMemo(
|
|
200
|
+
() => normalizeValueList(defaultValue !== undefined ? defaultValue : []),
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
[],
|
|
203
|
+
);
|
|
204
|
+
const [innerValues, setInnerValues] = useState(() => (multiple ? initialValues : initialValues.slice(0, 1)));
|
|
195
205
|
const [open, setOpen] = useState(false);
|
|
196
206
|
const [panelStyle, setPanelStyle] = useState({
|
|
197
207
|
top: 0,
|
|
@@ -199,7 +209,9 @@ export default function Filter({
|
|
|
199
209
|
width: 180,
|
|
200
210
|
maxHeight: PANEL_MAX_HEIGHT,
|
|
201
211
|
});
|
|
202
|
-
const currentValues = isControlled
|
|
212
|
+
const currentValues = isControlled
|
|
213
|
+
? (multiple ? normalizeValueList(selectedValues) : normalizeValueList(selectedValues).slice(0, 1))
|
|
214
|
+
: innerValues;
|
|
203
215
|
const selectedKeySet = useMemo(
|
|
204
216
|
() => new Set(currentValues.map(getValueKey)),
|
|
205
217
|
[currentValues],
|
|
@@ -211,9 +223,10 @@ export default function Filter({
|
|
|
211
223
|
const derivedValue = useMemo(() => {
|
|
212
224
|
if (value !== null && value !== undefined && value !== '') return value;
|
|
213
225
|
if (selectedOptions.length === 1) return selectedOptions[0].label;
|
|
226
|
+
if (!multiple) return null;
|
|
214
227
|
if (selectedOptions.length > 1) return `已选 ${selectedOptions.length} 项`;
|
|
215
228
|
return null;
|
|
216
|
-
}, [selectedOptions, value]);
|
|
229
|
+
}, [multiple, selectedOptions, value]);
|
|
217
230
|
const hasValue = derivedValue !== null && derivedValue !== undefined && derivedValue !== '';
|
|
218
231
|
const isSelected = selected || selectedOptions.length > 0;
|
|
219
232
|
const tone = disabled
|
|
@@ -230,18 +243,30 @@ export default function Filter({
|
|
|
230
243
|
if (!isControlled) {
|
|
231
244
|
setInnerValues(nextValues);
|
|
232
245
|
}
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
if (multiple) {
|
|
247
|
+
onChange?.(nextValues, event);
|
|
248
|
+
} else {
|
|
249
|
+
onChange?.(nextValues[0] ?? null, event);
|
|
250
|
+
}
|
|
251
|
+
}, [isControlled, multiple, onChange]);
|
|
235
252
|
|
|
236
253
|
const toggleOption = useCallback((option, event) => {
|
|
237
254
|
if (option.disabled) return;
|
|
238
255
|
const optionKey = getValueKey(option.value);
|
|
239
256
|
const exists = selectedKeySet.has(optionKey);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
if (multiple) {
|
|
258
|
+
const nextValues = exists
|
|
259
|
+
? currentValues.filter((item) => getValueKey(item) !== optionKey)
|
|
260
|
+
: [...currentValues, option.value];
|
|
261
|
+
commitValues(nextValues, event);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// 单选:再次点击已选项视为反选;否则替换为该值并关闭面板
|
|
265
|
+
const nextValues = exists ? [] : [option.value];
|
|
243
266
|
commitValues(nextValues, event);
|
|
244
|
-
|
|
267
|
+
setOpen(false);
|
|
268
|
+
triggerRef.current?.focus();
|
|
269
|
+
}, [commitValues, currentValues, multiple, selectedKeySet]);
|
|
245
270
|
|
|
246
271
|
const toggleOpen = useCallback(() => {
|
|
247
272
|
if (disabled || !hasDropdown) return;
|
|
@@ -300,7 +325,7 @@ export default function Filter({
|
|
|
300
325
|
ref={panelRef}
|
|
301
326
|
id={listboxId}
|
|
302
327
|
role="listbox"
|
|
303
|
-
aria-multiselectable=
|
|
328
|
+
aria-multiselectable={multiple ? 'true' : 'false'}
|
|
304
329
|
className={PANEL_CLASS}
|
|
305
330
|
style={{
|
|
306
331
|
position: 'fixed',
|
|
@@ -327,14 +352,21 @@ export default function Filter({
|
|
|
327
352
|
onMouseDown={(event) => event.preventDefault()}
|
|
328
353
|
onClick={(event) => toggleOption(option, event)}
|
|
329
354
|
>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
355
|
+
{multiple ? (
|
|
356
|
+
<span className={[
|
|
357
|
+
CHECKBOX_BOX_BASE,
|
|
358
|
+
checked ? CHECKBOX_BOX_ON : CHECKBOX_BOX_OFF,
|
|
359
|
+
option.disabled ? 'border-border-default bg-disabled text-foreground-disabled' : '',
|
|
360
|
+
].filter(Boolean).join(' ')}>
|
|
361
|
+
{checked ? <CheckIcon /> : null}
|
|
362
|
+
</span>
|
|
363
|
+
) : null}
|
|
337
364
|
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
365
|
+
{!multiple && checked ? (
|
|
366
|
+
<span className="inline-flex size-[var(--spacing-4)] shrink-0 items-center justify-center text-brand-500" aria-hidden="true">
|
|
367
|
+
<CheckIcon />
|
|
368
|
+
</span>
|
|
369
|
+
) : null}
|
|
338
370
|
</div>
|
|
339
371
|
);
|
|
340
372
|
})}
|
|
@@ -358,6 +390,7 @@ export default function Filter({
|
|
|
358
390
|
data-open={open || undefined}
|
|
359
391
|
data-selected={isSelected || undefined}
|
|
360
392
|
data-filled={filled || undefined}
|
|
393
|
+
data-multiple={multiple ? undefined : 'false'}
|
|
361
394
|
aria-expanded={hasDropdown ? open : undefined}
|
|
362
395
|
aria-haspopup={hasDropdown ? 'listbox' : undefined}
|
|
363
396
|
aria-controls={hasDropdown ? listboxId : undefined}
|
|
@@ -95,7 +95,7 @@ const OLA_ROOT = [
|
|
|
95
95
|
].join(' ');
|
|
96
96
|
|
|
97
97
|
const OLA_BRAND = 'flex w-full flex-col items-center gap-1 px-4 py-6';
|
|
98
|
-
const OLA_BRAND_TEXT = 'm-0 text-sm leading-5 text-foreground
|
|
98
|
+
const OLA_BRAND_TEXT = 'm-0 text-sm leading-5 text-foreground';
|
|
99
99
|
const OLA_BRAND_TEXT_STYLE = {
|
|
100
100
|
fontFamily: '"Arial Black", Arial, sans-serif',
|
|
101
101
|
fontWeight: 900,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const NAVBAR_TOKEN_MAP = {
|
|
2
2
|
品牌: [
|
|
3
3
|
{ label: '品牌语义', cssProp: 'content', value: '顶部文案展示真实平台名称;OLA 仅为模板示意,不是固定文案' },
|
|
4
|
+
{ label: '大小写', cssProp: 'text-transform', value: '保留 brandName 原始大小写,不强制 uppercase;允许中文、英文大小写和中英混排' },
|
|
4
5
|
{ label: '平台默认', cssProp: 'variant', value: '默认使用 OLA 类型菜单栏结构;ByteHi 仅限明确“客服工作台”场景' },
|
|
5
6
|
{ label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
6
7
|
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
@@ -139,10 +139,15 @@ const OPTION_ACTIVE = 'bg-brand-50 text-brand-500';
|
|
|
139
139
|
/* ── 选项禁用 ── */
|
|
140
140
|
const OPTION_DISABLED = 'opacity-50 cursor-not-allowed pointer-events-none text-foreground-disabled';
|
|
141
141
|
const TAG_OPTION_BASE = [
|
|
142
|
-
'flex min-h-[36px] cursor-pointer items-center px-
|
|
142
|
+
'mx-1 flex min-h-[36px] cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-2',
|
|
143
|
+
'text-sm leading-[20px] text-foreground',
|
|
143
144
|
'transition-colors duration-100',
|
|
144
145
|
].join(' ');
|
|
145
|
-
const
|
|
146
|
+
const TAG_OPTION_HOVER = 'hover:bg-fill';
|
|
147
|
+
const TAG_OPTION_ACTIVE = 'bg-brand-50 text-brand-500 hover:bg-brand-50';
|
|
148
|
+
const TAG_OPTION_HIGHLIGHT = 'bg-fill';
|
|
149
|
+
const TAG_OPTION_LABEL = 'min-w-0 flex-1 truncate';
|
|
150
|
+
const TAG_OPTION_CHECK = 'inline-flex size-[16px] shrink-0 items-center justify-center text-brand-500';
|
|
146
151
|
|
|
147
152
|
function getValueKey(value) {
|
|
148
153
|
return String(value);
|
|
@@ -629,8 +634,9 @@ export default function Select({
|
|
|
629
634
|
const hi = highlight === i;
|
|
630
635
|
const cls = [
|
|
631
636
|
isTagMode ? TAG_OPTION_BASE : OPTION_BASE,
|
|
632
|
-
!opt.disabled && OPTION_HOVER,
|
|
633
|
-
|
|
637
|
+
!opt.disabled && (isTagMode ? (selectedOpt ? '' : TAG_OPTION_HOVER) : OPTION_HOVER),
|
|
638
|
+
hi && !selectedOpt && !opt.disabled && isTagMode ? TAG_OPTION_HIGHLIGHT : '',
|
|
639
|
+
selectedOpt && !opt.disabled ? (isTagMode ? TAG_OPTION_ACTIVE : OPTION_ACTIVE) : '',
|
|
634
640
|
opt.disabled ? OPTION_DISABLED : '',
|
|
635
641
|
].filter(Boolean).join(' ');
|
|
636
642
|
return (
|
|
@@ -645,14 +651,14 @@ export default function Select({
|
|
|
645
651
|
onClick={() => pick(opt)}
|
|
646
652
|
>
|
|
647
653
|
{isTagMode ? (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
654
|
+
<>
|
|
655
|
+
<span className={TAG_OPTION_LABEL}>{opt.label}</span>
|
|
656
|
+
<span className={`${TAG_OPTION_CHECK} ${selectedOpt ? 'opacity-100' : 'opacity-0'}`} aria-hidden>
|
|
657
|
+
<svg viewBox="0 0 16 16" fill="none" className="size-[16px]">
|
|
658
|
+
<path d="M3.5 8.25L6.5 11.25L12.5 4.75" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
|
659
|
+
</svg>
|
|
660
|
+
</span>
|
|
661
|
+
</>
|
|
656
662
|
) : opt.label}
|
|
657
663
|
</div>
|
|
658
664
|
);
|
|
@@ -25,12 +25,16 @@ export const SELECT_TOKEN_MAP = {
|
|
|
25
25
|
],
|
|
26
26
|
标签选择: [
|
|
27
27
|
{ label: '分类归属', cssProp: 'mode', value: 'tag 属于 Select 的标签选择分类' },
|
|
28
|
-
{ label: '已选标签', cssProp: 'component', value: 'Tag,默认规格与 TagInput 保持一致;可按 option.variant 覆盖颜色' },
|
|
28
|
+
{ label: '已选标签', cssProp: 'component', value: '触发器内回显使用 Tag,默认规格与 TagInput 保持一致;可按 option.variant 覆盖颜色' },
|
|
29
29
|
{ label: '折叠项', cssProp: 'component', value: 'Tag children="+N",沿用 TagInput 默认规格,宽度不足时显示' },
|
|
30
30
|
{ label: '更多浮窗', cssProp: 'component', value: 'Tooltip tone=light,仅展示被折叠的标签' },
|
|
31
31
|
{ label: '标签间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
32
|
+
{ label: '下拉项形态', cssProp: 'component', value: '标准多选列表项:左侧文字,右侧 check;下拉面板内不再渲染 Tag' },
|
|
32
33
|
{ label: '下拉项高度', cssProp: 'min-height', token: '--size-control-md', value: '36px' },
|
|
33
|
-
{ label: '
|
|
34
|
+
{ label: '下拉项圆角', cssProp: 'border-radius', token: '--radius-sm', value: '6px' },
|
|
35
|
+
{ label: '下拉项左右内距', cssProp: 'padding-inline', token: '--spacing-2', value: '8px' },
|
|
36
|
+
{ label: '选中背景', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', semanticRef: 'status-primary.bg', state: 'selected' },
|
|
37
|
+
{ label: '选中文字/图标', cssProp: 'color', token: '--color-brand-500', value: '#56D3BC', semanticRef: 'status-primary', state: 'selected' },
|
|
34
38
|
],
|
|
35
39
|
下拉箭头: [
|
|
36
40
|
{ label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
@@ -120,8 +120,8 @@ const TYPE_MIN_HEIGHT = {
|
|
|
120
120
|
/* ── 容器 / 表格 ──
|
|
121
121
|
* Table 自身不带白色背景、不带四周 padding、不带圆角;
|
|
122
122
|
* 由外部容器(业务白卡 / 预览容器)决定外观与留白。
|
|
123
|
-
*
|
|
124
|
-
*
|
|
123
|
+
* 表头默认使用 Blue Grey 100,sticky 固定列保持不透明,
|
|
124
|
+
* 用于保证横向滚动时表头、固定列和内容层级清晰。
|
|
125
125
|
*/
|
|
126
126
|
const WRAPPER = [
|
|
127
127
|
'tfds-table',
|
|
@@ -140,11 +140,10 @@ const TABLE_CONTENT = 'flex w-full flex-1 min-h-0 flex-col';
|
|
|
140
140
|
const TABLE_BODY = 'flex-1 min-h-0 w-full';
|
|
141
141
|
|
|
142
142
|
/* ── 表头 / 单元格 ──
|
|
143
|
-
* Table
|
|
144
|
-
*
|
|
145
|
-
* 请在外层容器设置背景色(白卡 / 灰底 page 等)。
|
|
143
|
+
* Table 自身不带外层背景;表头统一使用 Blue Grey 100,
|
|
144
|
+
* 表体由外部容器或默认 surface 决定底色。
|
|
146
145
|
*/
|
|
147
|
-
const HEADER_ROW = 'sticky top-0 z-40 grid w-full min-w-0 bg-
|
|
146
|
+
const HEADER_ROW = 'sticky top-0 z-40 grid w-full min-w-0 bg-blueGrey-100';
|
|
148
147
|
const BODY_ROW = 'grid w-full min-w-0';
|
|
149
148
|
|
|
150
149
|
const HEADER_CELL = [
|
|
@@ -226,6 +225,7 @@ const PAGE_ELLIPSIS = 'inline-flex size-8 items-center justify-center text-sm fo
|
|
|
226
225
|
|
|
227
226
|
/* ── 空态 ── */
|
|
228
227
|
const EMPTY_CELL = 'px-4 py-10 text-center text-sm font-normal leading-5 text-foreground-muted';
|
|
228
|
+
const TABLE_HEADER_BACKGROUND = 'var(--color-blueGrey-100, #F9FAFB)';
|
|
229
229
|
const PINNED_SURFACE_BACKGROUND = 'var(--color-surface, #FFFFFF)';
|
|
230
230
|
|
|
231
231
|
/* ── 卡片型表单 ── */
|
|
@@ -371,7 +371,7 @@ function getPinnedCellStyle({
|
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
style.position = 'sticky';
|
|
374
|
-
style.background = PINNED_SURFACE_BACKGROUND;
|
|
374
|
+
style.background = isHeader ? TABLE_HEADER_BACKGROUND : PINNED_SURFACE_BACKGROUND;
|
|
375
375
|
style.backgroundClip = 'padding-box';
|
|
376
376
|
style.zIndex = isHeader ? 50 : 30;
|
|
377
377
|
|
|
@@ -925,6 +925,7 @@ export const COMPONENTS = [
|
|
|
925
925
|
LOCAL_MEMBER_AVATAR_RULE,
|
|
926
926
|
'【平台默认】NavBar 默认一律使用 platform="ola" 的 OLA 类型菜单栏结构。这里的 OLA 指导航结构类型,不代表顶部品牌文案必须固定显示为“OLA”;图中的“OLA”仅为模板示意。',
|
|
927
927
|
'【品牌文案】brandName 是顶部平台名称展示入口,必须根据真实页面类型动态传入对应平台名称,例如“体验服务”“抖音体验与服务”“服务治理”等;不要把“OLA”当成所有页面固定品牌名。只有在缺少真实平台名称的组件预览或模板占位中,才允许继续显示 OLA。',
|
|
928
|
+
'【品牌大小写】顶部品牌文案必须保留业务传入的 brandName 原始大小写,不做 uppercase / lowercase / capitalize 等强制转换;允许中文、英文大小写、中英混排和产品专有写法(如 Ola AI、dataPilot、TFDesign)。',
|
|
928
929
|
'【ByteHi 限定】platform="bytehi" 仅限明确提到“客服工作台”相关页面场景时使用,例如“客服工作台首页”“在线客服工作台”“客服工作台会话页”。普通客服、服务、体验、治理、策略、知识、工具、洞察等页面都不要自动切换 ByteHi,仍默认使用 OLA 类型菜单栏。',
|
|
929
930
|
'【提示词】未显式传 platform 时,默认使用 OLA;只有 prompt / promptText 明确包含“客服工作台”时才默认使用 ByteHi。显式传 platform 时以显式值为准,但 AI 生成页面时不得在非客服工作台场景主动传 platform="bytehi"。',
|
|
930
931
|
'【站点级骨架】`NavBar` 是平台级站点导航锚点,不是普通内容区组件。只要页面使用 `NavBar`,一级骨架默认必须是左侧固定导航 + 右侧弹性主工作区的横向 app shell;`NavBar` 永远默认位于页面最左侧',
|
|
@@ -948,6 +949,7 @@ export const COMPONENTS = [
|
|
|
948
949
|
examples: [
|
|
949
950
|
{ label: '默认导航栏', code: '<NavBar />' },
|
|
950
951
|
{ label: '真实平台名称', code: '<NavBar brandName="体验服务" selectedItemId="home" onSelect={(nextId) => setCurrentNavId(nextId)} />' },
|
|
952
|
+
{ label: '保留平台英文大小写', code: '<NavBar brandName="Ola AI" />' },
|
|
951
953
|
{ label: '业务方切换', code: '<NavBar appBusinesses={[{ id: "douyin-community", label: "抖音社区", iconSrc: "icon-logo-douyin" }, { id: "douyin-local-services", label: "抖音生活服务", iconSrc: "icon-logo-douyin" }, { id: "douyin-ecommerce", label: "抖音电商", iconSrc: "icon-logo-douyin" }]} />' },
|
|
952
954
|
{ label: '显示底部操作', code: '<NavBar showUtilityActions />' },
|
|
953
955
|
{ label: 'ByteHi 平台', code: '<NavBar platform="bytehi" />' },
|
|
@@ -2361,9 +2363,10 @@ export const COMPONENTS = [
|
|
|
2361
2363
|
'【选型·与 Radio】选项 ≤5 且表单内需并排展示全部文案 → Radio;选项多、需占位窄、或选项会动态增长 → Select',
|
|
2362
2364
|
'【选型·与 Checkbox】同一字段要选多个离散值 → CheckboxGroup 或 Select mode=tag;仅单个开/关语义 → Switch 或单个 Checkbox',
|
|
2363
2365
|
'【数据】options 为 { value, label, disabled? };value 与 option.value 用字符串比较',
|
|
2364
|
-
'【TagSelect】mode="tag" 属于 Select
|
|
2366
|
+
'【TagSelect】mode="tag" 属于 Select 的标签选择分类,不新增独立基础组件;触发器内已选项与 +N 折叠项必须复用 Tag 组件,但下拉面板必须使用标准多选列表项(左侧文案 + 右侧 check),不要把下拉项渲染成一列 Tag',
|
|
2365
2367
|
'【TagSelect 规格】Tag 默认颜色与尺寸跟 TagInput 保持一致,不单独暴露颜色或尺寸配置;如需特殊颜色可在单个 option.variant 上覆盖',
|
|
2366
|
-
'【TagSelect 交互】mode="tag" 时 value/defaultValue
|
|
2368
|
+
'【TagSelect 交互】mode="tag" 时 value/defaultValue 为数组;点击下拉选项多选/取消且面板保持打开,触发器内标签可关闭,宽度不足时折叠为 +N 并用白底 Tooltip 展示隐藏标签',
|
|
2369
|
+
'【TagSelect 选中态】多选下拉项用整行热区承载点击,已选项使用品牌浅底 + 品牌色文字 + 右侧 check;hover 仅使用浅灰底,避免与 selected 混淆;禁用项降低透明度且不可点',
|
|
2367
2370
|
'【AI推荐】仅 mode="default" 支持 `aiSuggestion` / `aiSuggestions`;推荐区展示在 Select 触发器下方,视觉样式、关闭逻辑、hover 展开和刷新交互与 Input 的 AI 推荐保持一致',
|
|
2368
2371
|
'【AI推荐默认触发】当需求、字段说明或页面描述中出现“AI推荐”“智能推荐”“推荐回复”“刷新推荐”“智能建议”“推荐文案”“建议填写”“推荐选项”等语义时,Select 默认应展示 AI 推荐区,而不是只渲染普通下拉态。',
|
|
2369
2372
|
'【AI推荐默认形态】命中上述语义后,AI 推荐区固定展示在 Select 组件下方,推荐内容应贴合页面场景,并优先给出适合当前任务的推荐选项或推荐分类。',
|
|
@@ -2386,7 +2389,7 @@ export const COMPONENTS = [
|
|
|
2386
2389
|
],
|
|
2387
2390
|
examples: [
|
|
2388
2391
|
{ label: '基础', code: '<Select options={[{ value: "a", label: "选项 A" }, { value: "b", label: "选项 B" }]} placeholder="请选择" />' },
|
|
2389
|
-
{ label: '标签选择', code: '<Select mode="tag" defaultValue={["
|
|
2392
|
+
{ label: '标签选择', code: '<Select mode="tag" defaultValue={["brand", "risk"]} options={[{ value: "brand", label: "品牌连锁", variant: "grey" }, { value: "risk", label: "风险预警", variant: "grey" }]} />' },
|
|
2390
2393
|
{ label: '默认选中', code: '<Select options={[{ value: "a", label: "选项 A" }]} defaultValue="a" />' },
|
|
2391
2394
|
{ label: '可清除', code: '<Select allowClear options={[{ value: "x", label: "已选" }]} defaultValue="x" />' },
|
|
2392
2395
|
{ label: 'AI推荐-单条', code: '<Select options={[{ value: "zj", label: "浙江省" }, { value: "js", label: "江苏省" }, { value: "sh", label: "上海市" }]} aiSuggestion="江苏省" onRefreshAiSuggestions={() => {}} onAdoptSuggestion={(value, option) => console.log(value, option)} placeholder="请选择" />' },
|
|
@@ -2399,7 +2402,7 @@ export const COMPONENTS = [
|
|
|
2399
2402
|
{ label: '❌ Bad(原生 select,黑色边框)', code: '/* 禁止!原生 select 黑边、字号错位、不可定制 */\n<select className="border rounded px-2"><option>doubao</option><option>gpt-4o</option></select>' },
|
|
2400
2403
|
{ label: '✅ Good(Select + options)', code: '<Select options={[{ value: "doubao", label: "doubao-seed-1.8" }, { value: "gpt-4o", label: "GPT-4o" }]} placeholder="选择模型" />' },
|
|
2401
2404
|
{ label: '❌ Bad(手写 div+ul 模拟下拉)', code: '/* 禁止!手写 div 下拉缺少键盘、Portal、aria,黑边/层级错乱 */\n<div className="border rounded"><ul><li>选项 A</li><li>选项 B</li></ul></div>' },
|
|
2402
|
-
{ label: '✅ Good(多选用 mode="tag")', code: '<Select mode="tag" defaultValue={["
|
|
2405
|
+
{ label: '✅ Good(多选用 mode="tag")', code: '<Select mode="tag" defaultValue={["brand", "risk"]} options={[{ value: "brand", label: "品牌连锁" }, { value: "risk", label: "风险预警" }]} />' },
|
|
2403
2406
|
],
|
|
2404
2407
|
keywords: [
|
|
2405
2408
|
'Select', 'select', '下拉框', '下拉选择', '下拉菜单', '选择器', 'dropdown', 'combobox',
|
|
@@ -3295,8 +3298,9 @@ export const COMPONENTS = [
|
|
|
3295
3298
|
'【选型·vs Button】Filter 只用于筛选条件选择/收起/清除;普通动作(提交、取消、导出、新建)仍使用 Button。',
|
|
3296
3299
|
'【尺寸】固定高度 36px(--size-control-md),左右内距 12px(--spacing-3,对齐 Select md),内容 gap 8px(--spacing-2);不要随意压缩为 24px 或 32px,以保证和 B 端 Input/Select 默认高度对齐。',
|
|
3297
3300
|
'【文字】label 使用 semibold 600,value 使用 normal 400;文案建议 label 2-6 字、value 1-8 字,过长值应在业务层截断或改用 Tooltip。',
|
|
3298
|
-
'
|
|
3299
|
-
'
|
|
3301
|
+
'【下拉单/多选】传入 options 后点击胶囊展开下拉面板;通过 `multiple` 切换:默认 `multiple={true}` 多选(行尾 checkbox 方框,可同时勾多项);`multiple={false}` 单选(行尾 ✓,点击即提交并自动关闭面板,再次点击已选项可反选清空)。两种模式都支持 selectedValues 受控、defaultValue 非受控、onChange 回调;点击外部或 Escape 关闭。',
|
|
3302
|
+
'【受控/回调签名】多选:selectedValues 传数组,onChange 返回数组;单选:selectedValues 可传单值或单元素数组,onChange 返回单值(清空时返回 null)。defaultValue 同样支持单值/数组两种形态。',
|
|
3303
|
+
'【值展示】未显式传 value 时,单选/多选选中 1 项都展示该项 label,多选选中 ≥2 项展示“已选 N 项”,单选始终最多 1 项 label;显式 value 优先用于自定义展示。',
|
|
3300
3304
|
'【状态】selected=true 使用品牌浅底 + 品牌描边 + 品牌文字;filled=true 使用中性填充底;disabled=true 使用禁用文字与禁用底,且不可点击。',
|
|
3301
3305
|
'【图标】无选中值时显示 12px 下拉箭头;closable=true 或已选中且未禁用时显示 16px 清除按钮(视觉与 Select 清除入口一致),点击清空多选值、关闭下拉并触发 onClear,不触发展开。',
|
|
3302
3306
|
'【语义结构】外层使用 role="combobox/button" 的 div 触发器,避免清除 button 嵌套在 button 内;不要改回 button 包 button 的非法 DOM。',
|
|
@@ -3305,9 +3309,12 @@ export const COMPONENTS = [
|
|
|
3305
3309
|
],
|
|
3306
3310
|
examples: [
|
|
3307
3311
|
{ label: '基础筛选项', code: '<Filter label="筛选项" />' },
|
|
3308
|
-
{ label: '
|
|
3309
|
-
{ label: '
|
|
3312
|
+
{ label: '多选下拉(默认)', code: '<Filter label="筛选项" options={[{ label: "选项一", value: "1" }, { label: "选项二", value: "2" }]} />' },
|
|
3313
|
+
{ label: '单选下拉', code: '<Filter label="状态" multiple={false} options={[{ label: "全部", value: "all" }, { label: "进行中", value: "running" }, { label: "已完成", value: "done" }]} />' },
|
|
3314
|
+
{ label: '默认已选(多选)', code: '<Filter label="筛选项" options={options} defaultValue={["1"]} />' },
|
|
3315
|
+
{ label: '默认已选(单选)', code: '<Filter label="状态" multiple={false} options={options} defaultValue="running" />' },
|
|
3310
3316
|
{ label: '受控多选', code: '<Filter label="筛选项" options={options} selectedValues={values} onChange={setValues} />' },
|
|
3317
|
+
{ label: '受控单选', code: '<Filter label="状态" multiple={false} options={options} selectedValues={current} onChange={setCurrent} />' },
|
|
3311
3318
|
{ label: '选中态', code: '<Filter label="筛选项" options={options} defaultValue={["1"]} selected />' },
|
|
3312
3319
|
{ label: '填充态', code: '<Filter label="筛选项" filled />' },
|
|
3313
3320
|
{ label: '可清除', code: '<Filter label="筛选项" options={options} defaultValue={["1"]} filled closable onClear={() => {}} />' },
|
|
@@ -3320,7 +3327,8 @@ export const COMPONENTS = [
|
|
|
3320
3327
|
keywords: [
|
|
3321
3328
|
'Filter', 'filter', '筛选', '筛选项', '筛选组件', '筛选胶囊', '筛选 chip',
|
|
3322
3329
|
'筛选触发器', '已选筛选', '过滤条件', 'filter item', 'filter chip', 'pill filter',
|
|
3323
|
-
'closable', 'clear', '清除筛选', '下拉筛选', '多选筛选', '
|
|
3330
|
+
'closable', 'clear', '清除筛选', '下拉筛选', '多选筛选', '单选筛选', 'single select filter', 'multi select filter',
|
|
3331
|
+
'multiple', 'multiple={false}', 'selected filter',
|
|
3324
3332
|
'span rounded-full border', '手搓筛选', '自制筛选 chip',
|
|
3325
3333
|
],
|
|
3326
3334
|
},
|