@tfdesign/b-end 1.0.11 → 1.0.13
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/README.md +23 -25
- package/package.json +1 -1
- package/skills/tfds/components.index.json +271 -67
- package/skills/tfds/components.summary.json +101 -62
- package/src/_b_end_runtime/components/ChatMessage.jsx +210 -61
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +30 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +14 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +30 -6
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +5 -0
- package/src/_b_end_runtime/components/Filter.jsx +390 -0
- package/src/_b_end_runtime/components/Filter.tokens.js +98 -0
- package/src/_b_end_runtime/components/Input.jsx +3 -1
- package/src/_b_end_runtime/components/Modal.jsx +10 -3
- package/src/_b_end_runtime/components/Radio.jsx +174 -4
- package/src/_b_end_runtime/components/Radio.tokens.js +22 -0
- package/src/_b_end_runtime/components.js +124 -13
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +35 -26
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +14 -1
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +19 -12
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +14 -1
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +15 -2
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +54 -36
- package/src/_b_end_runtime/patterns.js +33 -21
- package/src/_b_end_runtime/preview-registry.jsx +180 -8
- package/src/index.d.ts +30 -1
- package/src/index.js +2 -1
|
@@ -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
|
+
};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* @prop {(suggestion: string) => void} [onAdoptSuggestion] — 采纳 AI 推荐回调
|
|
20
20
|
* @prop {() => void} [onRefreshAiSuggestions] — 刷新 AI 推荐回调
|
|
21
21
|
* @prop {string} [className=''] — 类名
|
|
22
|
+
* @prop {import('react').CSSProperties} [style] — 根容器样式,常用于设置 `--size-input-width`
|
|
22
23
|
*/
|
|
23
24
|
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
24
25
|
import AiSuggestionPanel, { AiRefreshButton, buildSuggestionGroupsFromFlatList } from './AiSuggestionShared';
|
|
@@ -124,6 +125,7 @@ export default function Input({
|
|
|
124
125
|
onAdoptSuggestion,
|
|
125
126
|
onRefreshAiSuggestions,
|
|
126
127
|
className = '',
|
|
128
|
+
style,
|
|
127
129
|
...rest
|
|
128
130
|
}) {
|
|
129
131
|
const inputRef = useRef(null);
|
|
@@ -208,7 +210,7 @@ export default function Input({
|
|
|
208
210
|
].filter(Boolean).join(' ');
|
|
209
211
|
|
|
210
212
|
return (
|
|
211
|
-
<div className={wrapperCls}>
|
|
213
|
+
<div className={wrapperCls} style={style}>
|
|
212
214
|
<div className={fieldCls} data-tfds-component="Input">
|
|
213
215
|
{prefix ? <span className={ADDON_CLASS}>{prefix}</span> : null}
|
|
214
216
|
<input
|
|
@@ -40,6 +40,7 @@ const FOOTER_ROW =
|
|
|
40
40
|
const FOOTER_HINT =
|
|
41
41
|
'm-0 min-w-[7.5rem] flex-1 text-xs font-normal leading-4 text-foreground-secondary';
|
|
42
42
|
const ACTIONS = 'flex shrink-0 items-center justify-end gap-3';
|
|
43
|
+
const BODY_BOTTOM_GAP = 'shrink-0 h-6';
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Modal — 模态对话框面板(仅面板本体,遮罩与定位由外层处理)
|
|
@@ -60,7 +61,7 @@ const ACTIONS = 'flex shrink-0 items-center justify-end gap-3';
|
|
|
60
61
|
* @prop {function} [onClose=null] — 关闭回调,传入才显示关闭按钮
|
|
61
62
|
* @prop {function} [onCancel=null] — 取消回调,传入才显示取消按钮
|
|
62
63
|
* @prop {function} [onConfirm=null] — 确定回调,传入才显示确定按钮
|
|
63
|
-
* @prop {ReactNode} [footer=null] —
|
|
64
|
+
* @prop {ReactNode} [footer=null] — 自定义底栏,传入则替换默认底栏;若底栏整体为空则自动保留 24px 底部留白
|
|
64
65
|
* @prop {string} [bodyClassName=''] — 内容区附加类名
|
|
65
66
|
* @prop {string} [className=''] — 附加类名
|
|
66
67
|
* @prop {object} [style] — 内联样式
|
|
@@ -88,6 +89,10 @@ export default function Modal({
|
|
|
88
89
|
const subtitleId = `${uid}-subtitle`;
|
|
89
90
|
|
|
90
91
|
const sizeClass = layout === 'center' ? SIZE_CLASS[size] : '';
|
|
92
|
+
const hasDefaultFooterContent = Boolean((showFooterHint && footerHint) || onCancel || onConfirm);
|
|
93
|
+
const shouldRenderCustomFooter = footer != null;
|
|
94
|
+
const shouldRenderDefaultFooter = !shouldRenderCustomFooter && hasDefaultFooterContent;
|
|
95
|
+
const shouldRenderBodyBottomGap = !shouldRenderCustomFooter && !hasDefaultFooterContent;
|
|
91
96
|
|
|
92
97
|
return (
|
|
93
98
|
<div
|
|
@@ -125,9 +130,9 @@ export default function Modal({
|
|
|
125
130
|
|
|
126
131
|
<div className={[BODY_CLASS[layout], bodyClassName].filter(Boolean).join(' ')}>{children}</div>
|
|
127
132
|
|
|
128
|
-
{
|
|
133
|
+
{shouldRenderCustomFooter ? (
|
|
129
134
|
<div className={FOOTER}>{footer}</div>
|
|
130
|
-
) : (
|
|
135
|
+
) : shouldRenderDefaultFooter ? (
|
|
131
136
|
<footer className={FOOTER}>
|
|
132
137
|
<div className={FOOTER_ROW}>
|
|
133
138
|
{showFooterHint && footerHint ? (
|
|
@@ -149,6 +154,8 @@ export default function Modal({
|
|
|
149
154
|
</div>
|
|
150
155
|
</div>
|
|
151
156
|
</footer>
|
|
157
|
+
) : (
|
|
158
|
+
<div className={BODY_BOTTOM_GAP} aria-hidden="true" />
|
|
152
159
|
)}
|
|
153
160
|
</div>
|
|
154
161
|
);
|