@tfdesign/b-end 1.0.12 → 1.0.14

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 (34) hide show
  1. package/README.md +23 -25
  2. package/package.json +1 -1
  3. package/skills/tfds/components.index.json +226 -59
  4. package/skills/tfds/components.summary.json +93 -54
  5. package/src/_b_end_runtime/components/Card.jsx +4 -2
  6. package/src/_b_end_runtime/components/Card.tokens.js +2 -0
  7. package/src/_b_end_runtime/components/ChatMessage.jsx +1 -1
  8. package/src/_b_end_runtime/components/ConversationList.jsx +53 -58
  9. package/src/_b_end_runtime/components/Filter.jsx +390 -0
  10. package/src/_b_end_runtime/components/Filter.tokens.js +98 -0
  11. package/src/_b_end_runtime/components/FullScreenPage.jsx +1 -0
  12. package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +13 -15
  13. package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +2 -0
  14. package/src/_b_end_runtime/components/Input.jsx +3 -1
  15. package/src/_b_end_runtime/components/Modal.jsx +11 -3
  16. package/src/_b_end_runtime/components/Sheet.jsx +1 -0
  17. package/src/_b_end_runtime/components/Table.jsx +7 -0
  18. package/src/_b_end_runtime/components/TagBar.jsx +2 -0
  19. package/src/_b_end_runtime/components/Toast.jsx +1 -0
  20. package/src/_b_end_runtime/components/Upload.jsx +1 -0
  21. package/src/_b_end_runtime/components.js +98 -5
  22. package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +34 -22
  23. package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +1 -1
  24. package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +6 -6
  25. package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +12 -13
  26. package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +14 -1
  27. package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +19 -12
  28. package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +16 -1
  29. package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +15 -2
  30. package/src/_b_end_runtime/page-patterns/pageListShared.jsx +54 -36
  31. package/src/_b_end_runtime/patterns.js +10 -6
  32. package/src/_b_end_runtime/preview-registry.jsx +97 -2
  33. package/src/index.d.ts +29 -2
  34. package/src/index.js +2 -1
@@ -499,6 +499,7 @@ function ConversationCardFooter({ card, onSend }) {
499
499
  icon={<Icon name="send-01-stroked" size={16} aria-hidden="true" />}
500
500
  iconOnly
501
501
  className="!h-6 !w-6 !rounded-md !p-1"
502
+ tooltip="发送回复"
502
503
  onKeyDown={(event) => {
503
504
  event.stopPropagation();
504
505
  }}
@@ -1049,17 +1050,16 @@ function ConversationListDefaultVariant({
1049
1050
  {!isCardVariant && isAvatarOnly ? (
1050
1051
  <>
1051
1052
  <div className={AVATAR_ONLY_HEADER}>
1052
- <Tooltip content="展开会话列表">
1053
- <Button
1054
- type="button"
1055
- variant="ghost-black"
1056
- size="sm"
1057
- icon={<Icon name="layout-right-stroked" size={16} />}
1058
- iconOnly
1059
- onClick={handleExpandFromAvatarOnly}
1060
- aria-label="展开会话列表"
1061
- />
1062
- </Tooltip>
1053
+ <Button
1054
+ type="button"
1055
+ variant="ghost-black"
1056
+ size="sm"
1057
+ icon={<Icon name="layout-right-stroked" size={16} />}
1058
+ iconOnly
1059
+ tooltip="展开会话列表"
1060
+ onClick={handleExpandFromAvatarOnly}
1061
+ aria-label="展开会话列表"
1062
+ />
1063
1063
  </div>
1064
1064
  <div className={AVATAR_ONLY_LIST}>
1065
1065
  {avatarOnlyItems.map((item) => (
@@ -1077,65 +1077,60 @@ function ConversationListDefaultVariant({
1077
1077
  <header className={HEADER}>
1078
1078
  <div className={HEADER_MAIN}>
1079
1079
  {!isCardVariant && collapsible ? (
1080
- <Tooltip content="收起会话列表">
1081
- <Button
1082
- type="button"
1083
- variant="ghost-black"
1084
- size="sm"
1085
- icon={<Icon name="layout-right-stroked" size={16} />}
1086
- iconOnly
1087
- onClick={handleCollapseToAvatarOnly}
1088
- aria-label="收起会话列表"
1089
- />
1090
- </Tooltip>
1080
+ <Button
1081
+ type="button"
1082
+ variant="ghost-black"
1083
+ size="sm"
1084
+ icon={<Icon name="layout-right-stroked" size={16} />}
1085
+ iconOnly
1086
+ tooltip="收起会话列表"
1087
+ onClick={handleCollapseToAvatarOnly}
1088
+ aria-label="收起会话列表"
1089
+ />
1091
1090
  ) : null}
1092
1091
  <h2 className={TITLE}>{title}</h2>
1093
1092
  </div>
1094
1093
  {showActions ? (
1095
1094
  <div className={ACTIONS}>
1096
1095
  {showLayoutToggle ? (
1097
- <Tooltip content={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}>
1098
- <Button
1099
- type="button"
1100
- variant="ghost-black"
1101
- size="sm"
1102
- icon={<Icon name="switch-horizontal-01-stroked" size={16} />}
1103
- iconOnly
1104
- onClick={handleVariantToggle}
1105
- aria-label={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
1106
- />
1107
- </Tooltip>
1108
- ) : null}
1109
- <Tooltip content="搜索会话">
1110
1096
  <Button
1111
1097
  type="button"
1112
1098
  variant="ghost-black"
1113
1099
  size="sm"
1114
- icon={<Icon name="search-lg-stroked" size={16} />}
1100
+ icon={<Icon name="switch-horizontal-01-stroked" size={16} />}
1115
1101
  iconOnly
1116
- aria-label="搜索会话"
1102
+ tooltip={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
1103
+ onClick={handleVariantToggle}
1104
+ aria-label={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
1117
1105
  />
1118
- </Tooltip>
1119
- <Tooltip content="筛选会话">
1120
- <Button
1121
- type="button"
1122
- variant="ghost-black"
1123
- size="sm"
1124
- icon={<Icon name="filter-funnel-01-stroked" size={16} />}
1125
- iconOnly
1126
- aria-label="筛选会话"
1127
- />
1128
- </Tooltip>
1129
- <Tooltip content="查看工单文件">
1130
- <Button
1131
- type="button"
1132
- variant="ghost-black"
1133
- size="sm"
1134
- icon={<Icon name="file-05-stroked" size={16} />}
1135
- iconOnly
1136
- aria-label="查看工单文件"
1137
- />
1138
- </Tooltip>
1106
+ ) : null}
1107
+ <Button
1108
+ type="button"
1109
+ variant="ghost-black"
1110
+ size="sm"
1111
+ icon={<Icon name="search-lg-stroked" size={16} />}
1112
+ iconOnly
1113
+ tooltip="搜索会话"
1114
+ aria-label="搜索会话"
1115
+ />
1116
+ <Button
1117
+ type="button"
1118
+ variant="ghost-black"
1119
+ size="sm"
1120
+ icon={<Icon name="filter-funnel-01-stroked" size={16} />}
1121
+ iconOnly
1122
+ tooltip="筛选会话"
1123
+ aria-label="筛选会话"
1124
+ />
1125
+ <Button
1126
+ type="button"
1127
+ variant="ghost-black"
1128
+ size="sm"
1129
+ icon={<Icon name="file-05-stroked" size={16} />}
1130
+ iconOnly
1131
+ tooltip="查看工单文件"
1132
+ aria-label="查看工单文件"
1133
+ />
1139
1134
  </div>
1140
1135
  ) : null}
1141
1136
  </header>
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Filter — B 端筛选胶囊项
3
+ *
4
+ * 用于筛选栏中的单个触发器/已选筛选条件;传入 options 时内置多选下拉面板。
5
+ *
6
+ * @prop {string} [label='筛选项'] — 筛选项名称
7
+ * @prop {string|number|null} [value=null] — 已选值,存在时显示在 label 后
8
+ * @prop {Array<{label:string,value:string|number,disabled?:boolean}>} [options=[]] — 多选下拉选项
9
+ * @prop {Array<string|number>} [selectedValues] — 受控多选值
10
+ * @prop {Array<string|number>} [defaultValue=[]] — 非受控初始多选值
11
+ * @prop {function|null} [onChange=null] — 多选变更回调
12
+ * @prop {boolean} [selected=false] — 品牌色选中态
13
+ * @prop {boolean} [filled=false] — 中性填充态,适合 hover/轻强调容器
14
+ * @prop {boolean} [disabled=false] — 禁用态
15
+ * @prop {boolean} [closable=false] — 有展示值时是否显示清除图标;已选多选值默认显示清除入口
16
+ * @prop {function|null} [onClear=null] — 清除回调
17
+ * @prop {string} [className=''] — 附加类名
18
+ */
19
+
20
+ import {
21
+ useCallback,
22
+ useEffect,
23
+ useId,
24
+ useMemo,
25
+ useRef,
26
+ useState,
27
+ } from 'react';
28
+ import { createPortal } from 'react-dom';
29
+
30
+ const PANEL_GAP = 4;
31
+ const PANEL_MAX_HEIGHT = 260;
32
+ const PANEL_Z_INDEX = 10000;
33
+
34
+ const BASE = [
35
+ 'tfds-filter',
36
+ 'inline-flex h-[var(--size-control-md)] shrink-0 items-center justify-center gap-[var(--spacing-2)]',
37
+ 'rounded-[var(--radius-full)] border border-solid',
38
+ 'px-[var(--spacing-3)]',
39
+ 'text-[var(--text-sm)] leading-[var(--leading-5)] tracking-[var(--tracking-normal)]',
40
+ '[font-family:inherit]',
41
+ 'select-none whitespace-nowrap outline-none',
42
+ 'transition-all duration-150',
43
+ ].join(' ');
44
+
45
+ const TONE_CLASS = {
46
+ outline: [
47
+ 'bg-surface border-border-default text-foreground',
48
+ 'hover:border-border-strong',
49
+ 'active:bg-fill-active',
50
+ 'data-[open=true]:border-primary data-[open=true]:hover:border-primary',
51
+ ].join(' '),
52
+ fill: [
53
+ 'bg-fill border-border-default text-foreground',
54
+ 'hover:bg-fill-hover hover:border-border-strong',
55
+ 'active:bg-fill-active',
56
+ 'data-[open=true]:border-primary data-[open=true]:hover:border-primary',
57
+ ].join(' '),
58
+ selected: [
59
+ 'bg-brand-50 border-brand-300 text-brand-800',
60
+ 'hover:bg-brand-100 hover:border-brand-400',
61
+ 'active:bg-brand-100',
62
+ 'data-[open=true]:border-primary data-[open=true]:hover:border-primary',
63
+ ].join(' '),
64
+ disabled: [
65
+ 'bg-disabled border-grey-100 text-foreground-disabled',
66
+ 'cursor-not-allowed pointer-events-none',
67
+ ].join(' '),
68
+ selectedDisabled: [
69
+ 'bg-brand-50 border-brand-200 text-brand-300',
70
+ 'cursor-not-allowed pointer-events-none',
71
+ ].join(' '),
72
+ };
73
+
74
+ const LABEL_CLASS = '[font-weight:var(--font-semibold)]';
75
+ const VALUE_CLASS = '[font-weight:var(--font-normal)]';
76
+ const ICON_CLASS = 'inline-flex size-[var(--spacing-3)] shrink-0 items-center justify-center [&>svg]:size-[var(--spacing-3)]';
77
+ const CLEAR_CLASS = [
78
+ 'inline-flex items-center justify-center shrink-0 size-[var(--spacing-4)]',
79
+ 'rounded-full cursor-pointer',
80
+ 'text-foreground-disabled hover:text-foreground-secondary',
81
+ 'transition-opacity duration-150',
82
+ 'bg-transparent border-none p-0 mr-1',
83
+ ].join(' ');
84
+ const PANEL_CLASS = [
85
+ 'rounded-[var(--radius-md)] border border-solid border-border-default',
86
+ 'bg-surface shadow-lg',
87
+ '[padding-block:var(--spacing-1)] overflow-y-auto overflow-x-hidden',
88
+ '[font-family:inherit]',
89
+ ].join(' ');
90
+ const OPTION_CLASS = [
91
+ 'flex min-h-[var(--size-control-md)] cursor-pointer items-center gap-[var(--spacing-2)] px-[var(--spacing-3)] py-[var(--spacing-2)]',
92
+ 'text-[var(--text-sm)] leading-[var(--leading-5)] text-foreground',
93
+ 'transition-colors duration-100',
94
+ 'hover:bg-fill',
95
+ ].join(' ');
96
+ const OPTION_DISABLED_CLASS = 'cursor-not-allowed text-foreground-disabled opacity-50 hover:bg-transparent';
97
+ const CHECKBOX_BOX_BASE = [
98
+ 'flex size-[var(--spacing-4)] shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-solid',
99
+ 'transition-colors duration-150',
100
+ ].join(' ');
101
+ const CHECKBOX_BOX_OFF = 'border-foreground-muted bg-transparent';
102
+ const CHECKBOX_BOX_ON = 'border-brand-500 bg-brand-500 text-white';
103
+
104
+ function ChevronIcon() {
105
+ return (
106
+ <svg viewBox="0 0 12 12" fill="none" aria-hidden="true">
107
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
108
+ </svg>
109
+ );
110
+ }
111
+
112
+ function ClearIcon() {
113
+ return (
114
+ <svg viewBox="0 0 16 16" fill="none" className="size-[var(--spacing-4)]" aria-hidden="true">
115
+ <circle cx="8" cy="8" r="7" fill="currentColor" fillOpacity="0.15" />
116
+ <path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
117
+ </svg>
118
+ );
119
+ }
120
+
121
+ function CheckIcon() {
122
+ return (
123
+ <svg viewBox="0 0 12 12" fill="none" className="size-[var(--spacing-3)]" aria-hidden="true">
124
+ <path d="M2.5 6L5 8.5L9.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
125
+ </svg>
126
+ );
127
+ }
128
+
129
+ function getValueKey(item) {
130
+ return String(item);
131
+ }
132
+
133
+ function normalizeOptions(options) {
134
+ const source = Array.isArray(options) && options.length > 0 ? options : [];
135
+ return source
136
+ .map((item) => {
137
+ if (typeof item === 'string' || typeof item === 'number') {
138
+ return { label: String(item), value: item };
139
+ }
140
+ if (!item || typeof item !== 'object') return null;
141
+ const value = item.value ?? item.label;
142
+ if (value === undefined || value === null || value === '') return null;
143
+ return {
144
+ label: item.label != null ? String(item.label) : String(value),
145
+ value,
146
+ disabled: item.disabled === true,
147
+ };
148
+ })
149
+ .filter(Boolean);
150
+ }
151
+
152
+ function normalizeValueList(raw) {
153
+ if (!Array.isArray(raw)) return [];
154
+ return raw.filter((item) => item !== undefined && item !== null && item !== '');
155
+ }
156
+
157
+ function measurePanelPosition(triggerEl) {
158
+ const rect = triggerEl.getBoundingClientRect();
159
+ const spaceBelow = window.innerHeight - rect.bottom - PANEL_GAP;
160
+ const spaceAbove = rect.top - PANEL_GAP;
161
+ const fitsBelow = spaceBelow >= 160 || spaceBelow >= spaceAbove;
162
+ const maxHeight = Math.max(120, Math.min(PANEL_MAX_HEIGHT, (fitsBelow ? spaceBelow : spaceAbove) - 4));
163
+ return {
164
+ top: fitsBelow ? rect.bottom + PANEL_GAP : rect.top - maxHeight - PANEL_GAP,
165
+ left: rect.left,
166
+ width: Math.max(rect.width, 180),
167
+ maxHeight,
168
+ };
169
+ }
170
+
171
+ export default function Filter({
172
+ label = '筛选项',
173
+ value = null,
174
+ options = [],
175
+ selectedValues,
176
+ defaultValue = [],
177
+ onChange = null,
178
+ selected = false,
179
+ filled = false,
180
+ disabled = false,
181
+ closable = false,
182
+ onClear = null,
183
+ className = '',
184
+ children,
185
+ ...rest
186
+ }) {
187
+ const uid = useId();
188
+ const listboxId = `${uid}-filter-listbox`;
189
+ const triggerRef = useRef(null);
190
+ const panelRef = useRef(null);
191
+ const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
192
+ const hasDropdown = normalizedOptions.length > 0;
193
+ const isControlled = selectedValues !== undefined;
194
+ const [innerValues, setInnerValues] = useState(() => normalizeValueList(defaultValue));
195
+ const [open, setOpen] = useState(false);
196
+ const [panelStyle, setPanelStyle] = useState({
197
+ top: 0,
198
+ left: 0,
199
+ width: 180,
200
+ maxHeight: PANEL_MAX_HEIGHT,
201
+ });
202
+ const currentValues = isControlled ? normalizeValueList(selectedValues) : innerValues;
203
+ const selectedKeySet = useMemo(
204
+ () => new Set(currentValues.map(getValueKey)),
205
+ [currentValues],
206
+ );
207
+ const selectedOptions = useMemo(
208
+ () => normalizedOptions.filter((item) => selectedKeySet.has(getValueKey(item.value))),
209
+ [normalizedOptions, selectedKeySet],
210
+ );
211
+ const derivedValue = useMemo(() => {
212
+ if (value !== null && value !== undefined && value !== '') return value;
213
+ if (selectedOptions.length === 1) return selectedOptions[0].label;
214
+ if (selectedOptions.length > 1) return `已选 ${selectedOptions.length} 项`;
215
+ return null;
216
+ }, [selectedOptions, value]);
217
+ const hasValue = derivedValue !== null && derivedValue !== undefined && derivedValue !== '';
218
+ const isSelected = selected || selectedOptions.length > 0;
219
+ const tone = disabled
220
+ ? (isSelected ? 'selectedDisabled' : 'disabled')
221
+ : (isSelected ? 'selected' : (filled ? 'fill' : 'outline'));
222
+ const showClear = (closable || selectedOptions.length > 0) && !disabled && hasValue;
223
+
224
+ const updatePosition = useCallback(() => {
225
+ if (!triggerRef.current) return;
226
+ setPanelStyle(measurePanelPosition(triggerRef.current));
227
+ }, []);
228
+
229
+ const commitValues = useCallback((nextValues, event) => {
230
+ if (!isControlled) {
231
+ setInnerValues(nextValues);
232
+ }
233
+ onChange?.(nextValues, event);
234
+ }, [isControlled, onChange]);
235
+
236
+ const toggleOption = useCallback((option, event) => {
237
+ if (option.disabled) return;
238
+ const optionKey = getValueKey(option.value);
239
+ const exists = selectedKeySet.has(optionKey);
240
+ const nextValues = exists
241
+ ? currentValues.filter((item) => getValueKey(item) !== optionKey)
242
+ : [...currentValues, option.value];
243
+ commitValues(nextValues, event);
244
+ }, [commitValues, currentValues, selectedKeySet]);
245
+
246
+ const toggleOpen = useCallback(() => {
247
+ if (disabled || !hasDropdown) return;
248
+ setOpen((prev) => {
249
+ const next = !prev;
250
+ if (next) {
251
+ requestAnimationFrame(updatePosition);
252
+ }
253
+ return next;
254
+ });
255
+ }, [disabled, hasDropdown, updatePosition]);
256
+
257
+ const handleClear = (event) => {
258
+ event.stopPropagation();
259
+ if (disabled) return;
260
+ if (hasDropdown) {
261
+ commitValues([], event);
262
+ setOpen(false);
263
+ }
264
+ onClear?.(event);
265
+ triggerRef.current?.focus();
266
+ };
267
+
268
+ const handleKeyDown = (event) => {
269
+ if (disabled || !hasDropdown) return;
270
+ if (event.key === 'Enter' || event.key === ' ') {
271
+ event.preventDefault();
272
+ toggleOpen();
273
+ }
274
+ if (event.key === 'Escape') {
275
+ setOpen(false);
276
+ }
277
+ };
278
+
279
+ useEffect(() => {
280
+ if (!open) return undefined;
281
+ const handlePointerDown = (event) => {
282
+ const target = event.target;
283
+ if (triggerRef.current?.contains(target) || panelRef.current?.contains(target)) return;
284
+ setOpen(false);
285
+ };
286
+ const handleScrollOrResize = () => updatePosition();
287
+ document.addEventListener('mousedown', handlePointerDown);
288
+ window.addEventListener('resize', handleScrollOrResize);
289
+ window.addEventListener('scroll', handleScrollOrResize, true);
290
+ updatePosition();
291
+ return () => {
292
+ document.removeEventListener('mousedown', handlePointerDown);
293
+ window.removeEventListener('resize', handleScrollOrResize);
294
+ window.removeEventListener('scroll', handleScrollOrResize, true);
295
+ };
296
+ }, [open, updatePosition]);
297
+
298
+ const panel = open && !disabled && hasDropdown ? (
299
+ <div
300
+ ref={panelRef}
301
+ id={listboxId}
302
+ role="listbox"
303
+ aria-multiselectable="true"
304
+ className={PANEL_CLASS}
305
+ style={{
306
+ position: 'fixed',
307
+ top: panelStyle.top,
308
+ left: panelStyle.left,
309
+ width: panelStyle.width,
310
+ maxHeight: panelStyle.maxHeight,
311
+ zIndex: PANEL_Z_INDEX,
312
+ }}
313
+ >
314
+ {normalizedOptions.map((option, index) => {
315
+ const checked = selectedKeySet.has(getValueKey(option.value));
316
+ return (
317
+ <div
318
+ key={`${getValueKey(option.value)}-${index}`}
319
+ role="option"
320
+ aria-selected={checked}
321
+ aria-disabled={option.disabled || undefined}
322
+ className={[
323
+ OPTION_CLASS,
324
+ checked ? 'bg-brand-50 text-brand-500 hover:bg-brand-50' : '',
325
+ option.disabled ? OPTION_DISABLED_CLASS : '',
326
+ ].filter(Boolean).join(' ')}
327
+ onMouseDown={(event) => event.preventDefault()}
328
+ onClick={(event) => toggleOption(option, event)}
329
+ >
330
+ <span className={[
331
+ CHECKBOX_BOX_BASE,
332
+ checked ? CHECKBOX_BOX_ON : CHECKBOX_BOX_OFF,
333
+ option.disabled ? 'border-border-default bg-disabled text-foreground-disabled' : '',
334
+ ].filter(Boolean).join(' ')}>
335
+ {checked ? <CheckIcon /> : null}
336
+ </span>
337
+ <span className="min-w-0 flex-1 truncate">{option.label}</span>
338
+ </div>
339
+ );
340
+ })}
341
+ </div>
342
+ ) : null;
343
+
344
+ return (
345
+ <>
346
+ <div
347
+ ref={triggerRef}
348
+ role={hasDropdown ? 'combobox' : 'button'}
349
+ tabIndex={disabled ? -1 : 0}
350
+ className={[
351
+ BASE,
352
+ TONE_CLASS[tone],
353
+ disabled ? '' : 'cursor-pointer',
354
+ className,
355
+ ].filter(Boolean).join(' ')}
356
+ aria-disabled={disabled || undefined}
357
+ data-tfds-component="Filter"
358
+ data-open={open || undefined}
359
+ data-selected={isSelected || undefined}
360
+ data-filled={filled || undefined}
361
+ aria-expanded={hasDropdown ? open : undefined}
362
+ aria-haspopup={hasDropdown ? 'listbox' : undefined}
363
+ aria-controls={hasDropdown ? listboxId : undefined}
364
+ onClick={toggleOpen}
365
+ onKeyDown={handleKeyDown}
366
+ {...rest}
367
+ >
368
+ <span className={LABEL_CLASS}>{label}</span>
369
+ {hasValue ? <span className={VALUE_CLASS}>{derivedValue}</span> : null}
370
+ {children}
371
+ {showClear ? (
372
+ <button
373
+ type="button"
374
+ className={CLEAR_CLASS}
375
+ tabIndex={-1}
376
+ aria-label="清除"
377
+ onClick={handleClear}
378
+ >
379
+ <ClearIcon />
380
+ </button>
381
+ ) : (
382
+ <span className={ICON_CLASS} aria-hidden="true">
383
+ <ChevronIcon />
384
+ </span>
385
+ )}
386
+ </div>
387
+ {panel ? createPortal(panel, document.body) : null}
388
+ </>
389
+ );
390
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Filter — TOKEN_MAP(供平台属性面板展示)
3
+ */
4
+ export const FILTER_TOKEN_MAP = {
5
+ base: [
6
+ { label: '高度', cssProp: 'height', token: '--size-control-md', value: '36px' },
7
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-full', value: '9999px' },
8
+ { label: '左侧内距', cssProp: 'padding-left', token: '--spacing-3', value: '12px(对齐 Select md)' },
9
+ { label: '右侧内距', cssProp: 'padding-right', token: '--spacing-3', value: '12px(对齐 Select md)' },
10
+ { label: '垂直内距', cssProp: 'padding-block', value: '由 --size-control-md 36px 与 --leading-5 20px 自然居中计算,不单独写裸值' },
11
+ { label: '内容间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
12
+ { label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
13
+ { label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
14
+ { label: '描边宽度', cssProp: 'border-width', value: '1px' },
15
+ ],
16
+ 文字: [
17
+ { label: '标签字重', cssProp: 'font-weight', token: '--font-semibold', value: '600' },
18
+ { label: '值字重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
19
+ { label: '常态文字', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
20
+ { label: '禁用文字', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled', state: 'disabled' },
21
+ { label: '选中文字', cssProp: 'color', token: '--color-brand-800', value: '#129683', semanticRef: 'status-primary', state: 'selected' },
22
+ { label: '禁用选中文字', cssProp: 'color', token: '--color-brand-300', value: '#87DEC9', state: 'selected+disabled' },
23
+ ],
24
+ 容器: [
25
+ { label: '常态背景', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
26
+ { label: '常态描边', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
27
+ { label: '悬浮描边', cssProp: 'border-color', token: '--color-border-strong', value: '#D0D5DD', semanticRef: 'border-strong', state: 'hover' },
28
+ { label: '展开描边', cssProp: 'border-color', token: '--color-primary', value: '#56D3BC', semanticRef: 'status-primary', state: 'open' },
29
+ { label: '展开焦点', cssProp: 'box-shadow / ring', value: '无额外 ring,仅 1px 边框变色(对齐 Select)', state: 'open' },
30
+ { label: '填充背景', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default', state: 'filled' },
31
+ { label: '填充悬浮', cssProp: 'background', token: '--color-fill-hover', value: 'rgba(83, 96, 143, 0.12)', semanticRef: 'fill-hover', state: 'filled+hover' },
32
+ { label: '填充按下', cssProp: 'background', token: '--color-fill-active', value: 'rgba(83, 96, 143, 0.15)', semanticRef: 'fill-active', state: 'active' },
33
+ { label: '选中背景', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', semanticRef: 'status-primary.bg', state: 'selected' },
34
+ { label: '选中描边', cssProp: 'border-color', token: '--color-brand-300', value: '#87DEC9', state: 'selected' },
35
+ { label: '选中悬浮背景', cssProp: 'background', token: '--color-brand-100', value: '#CFF3EA', state: 'selected+hover' },
36
+ { label: '禁用背景', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
37
+ { label: '禁用描边', cssProp: 'border-color', token: '--color-grey-100', value: '#E6E7EA', state: 'disabled' },
38
+ { label: '禁用选中描边', cssProp: 'border-color', token: '--color-brand-200', value: '#A8E8D7', state: 'selected+disabled' },
39
+ ],
40
+ 图标: [
41
+ { label: '下拉尺寸', cssProp: 'width / height', token: '--spacing-3', value: '12px' },
42
+ { label: '下拉颜色', cssProp: 'color', value: 'inherit(继承当前文字色)' },
43
+ { label: '下拉图标', cssProp: 'icon', value: 'ChevronIcon,线宽 1.4px' },
44
+ { label: '清除尺寸', cssProp: 'width / height', token: '--spacing-4', value: '16px(对齐 Select)' },
45
+ { label: '清除颜色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled' },
46
+ { label: '清除悬浮颜色', cssProp: 'color', token: '--color-foreground-secondary', value: '#475467', semanticRef: 'text-secondary', state: 'hover' },
47
+ { label: '清除右外距', cssProp: 'margin-right', token: '--spacing-1', value: '4px(对齐 Select)' },
48
+ { label: '清除图标', cssProp: 'icon', value: 'ClearIcon,16px viewBox,圆底透明度 15%(对齐 Select)' },
49
+ { label: '勾选图标尺寸', cssProp: 'width / height', token: '--spacing-3', value: '12px' },
50
+ ],
51
+ 下拉面板: [
52
+ { label: '触发间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
53
+ { label: '最小宽度', cssProp: 'min-width', value: '180px' },
54
+ { label: '最大高度', cssProp: 'max-height', value: '260px' },
55
+ { label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
56
+ { label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
57
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
58
+ { label: '阴影', cssProp: 'box-shadow', value: 'shadow-lg' },
59
+ { label: '纵向内距', cssProp: 'padding-block', token: '--spacing-1', value: '4px' },
60
+ { label: '层级', cssProp: 'z-index', value: '10000' },
61
+ ],
62
+ 多选项: [
63
+ { label: '最小高度', cssProp: 'min-height', token: '--size-control-md', value: '36px' },
64
+ { label: '横向内距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
65
+ { label: '纵向内距', cssProp: 'padding-block', token: '--spacing-2', value: '8px' },
66
+ { label: '内容间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
67
+ { label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
68
+ { label: '选中背景', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', semanticRef: 'status-primary.bg', state: 'selected' },
69
+ { label: '选中文字', cssProp: 'color', token: '--color-brand-500', value: '#56D3BC', semanticRef: 'status-primary', state: 'selected' },
70
+ { label: '悬浮背景', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default', state: 'hover' },
71
+ { label: '禁用文字', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled', state: 'disabled' },
72
+ { label: 'Checkbox 尺寸', cssProp: 'width / height', token: '--spacing-4', value: '16px' },
73
+ { label: 'Checkbox 圆角', cssProp: 'border-radius', token: '--radius-sm', value: '6px' },
74
+ { label: 'Checkbox 选中', cssProp: 'background / border-color', token: '--color-brand-500', value: '#56D3BC' },
75
+ ],
76
+ states: {
77
+ outline: [
78
+ { label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
79
+ { label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
80
+ { label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
81
+ ],
82
+ fill: [
83
+ { label: '背景色', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default' },
84
+ { label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
85
+ { label: '文字色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
86
+ ],
87
+ selected: [
88
+ { label: '背景色', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', semanticRef: 'status-primary.bg' },
89
+ { label: '边框色', cssProp: 'border-color', token: '--color-brand-300', value: '#87DEC9' },
90
+ { label: '文字色', cssProp: 'color', token: '--color-brand-800', value: '#129683', semanticRef: 'status-primary' },
91
+ ],
92
+ disabled: [
93
+ { label: '背景色', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled' },
94
+ { label: '边框色', cssProp: 'border-color', token: '--color-grey-100', value: '#E6E7EA' },
95
+ { label: '文字色', cssProp: 'color', token: '--color-foreground-disabled', value: '#98A2B3', semanticRef: 'text-disabled' },
96
+ ],
97
+ },
98
+ };
@@ -75,6 +75,7 @@ export default function FullScreenPage({
75
75
  icon={<ArrowLeft size={16} strokeWidth={2} />}
76
76
  iconOnly
77
77
  onClick={onBack}
78
+ tooltip="返回"
78
79
  aria-label="返回"
79
80
  className="shrink-0"
80
81
  data-tfds-component="FullScreenPage.Back"