@teamix-evo/ui 0.5.2 → 0.6.1
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 +4 -4
- package/src/components/alert/index.tsx +1 -1
- package/src/components/alert-dialog/index.tsx +17 -24
- package/src/components/alert-dialog/meta.md +102 -8
- package/src/components/alert-dialog/stories.tsx +117 -7
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/badge/index.tsx +1 -1
- package/src/components/button/index.tsx +1 -29
- package/src/components/button/meta.md +24 -13
- package/src/components/button/stories.tsx +21 -12
- package/src/components/button-group/meta.md +6 -9
- package/src/components/button-group/stories.tsx +2 -6
- package/src/components/calendar/index.tsx +12 -7
- package/src/components/cascader-select/index.tsx +1 -1
- package/src/components/checkbox/index.tsx +1 -1
- package/src/components/combobox/index.tsx +54 -10
- package/src/components/combobox/meta.md +3 -5
- package/src/components/combobox/stories.tsx +104 -25
- package/src/components/data-table/stories.tsx +4 -1
- package/src/components/date-picker/index.tsx +25 -2
- package/src/components/field/index.tsx +1 -1
- package/src/components/filter-bar/index.tsx +1 -1
- package/src/components/float-button/meta.md +3 -15
- package/src/components/icon/index.tsx +3 -4
- package/src/components/icon/meta.md +1 -2
- package/src/components/input/index.tsx +10 -2
- package/src/components/input-group/index.tsx +3 -3
- package/src/components/input-group/meta.md +15 -0
- package/src/components/input-group/stories.tsx +14 -0
- package/src/components/input-ip/index.tsx +1 -1
- package/src/components/input-number/index.tsx +5 -5
- package/src/components/item/meta.md +11 -11
- package/src/components/radio-group/index.tsx +1 -1
- package/src/components/rate/index.tsx +3 -3
- package/src/components/select/index.tsx +2 -2
- package/src/components/sidebar/index.tsx +4 -4
- package/src/components/skeleton/index.tsx +1 -1
- package/src/components/skeleton/meta.md +6 -6
- package/src/components/skeleton/stories.tsx +8 -8
- package/src/components/slider/index.tsx +27 -1
- package/src/components/sonner/index.tsx +43 -40
- package/src/components/sonner/meta.md +84 -68
- package/src/components/sonner/stories.tsx +122 -83
- package/src/components/spinner/index.tsx +170 -0
- package/src/components/spinner/meta.md +27 -1
- package/src/components/spinner/stories.tsx +23 -0
- package/src/components/steps/index.tsx +5 -1
- package/src/components/switch/index.tsx +1 -1
- package/src/components/tag/index.tsx +14 -0
- package/src/components/tag/meta.md +1 -0
- package/src/components/tag/stories.tsx +13 -0
- package/src/components/textarea/index.tsx +1 -1
- package/src/components/textarea/stories.tsx +1 -1
- package/src/components/time-picker/index.tsx +3 -1
- package/src/components/toggle/index.tsx +1 -1
- package/src/components/tooltip/index.tsx +5 -1
- package/src/components/tooltip/meta.md +13 -28
- package/src/components/tooltip/stories.tsx +11 -28
- package/src/components/tree-select/index.tsx +1 -1
|
@@ -123,19 +123,28 @@ export const Disabled: Story = {
|
|
|
123
123
|
),
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
/**
|
|
126
|
+
/** 警告按钮用于危险操作的确认入口,可通过 className 与 variant 组合使用。 */
|
|
127
127
|
export const Warning: Story = {
|
|
128
128
|
render: () => (
|
|
129
129
|
<div className="flex gap-2">
|
|
130
|
-
<Button variant="destructive"
|
|
131
|
-
<Button
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
</Button>
|
|
137
|
-
<Button
|
|
138
|
-
|
|
130
|
+
<Button variant="destructive">危险按钮</Button>
|
|
131
|
+
<Button
|
|
132
|
+
variant="outline"
|
|
133
|
+
className="border-destructive text-destructive hover:text-destructive"
|
|
134
|
+
>
|
|
135
|
+
危险按钮
|
|
136
|
+
</Button>
|
|
137
|
+
<Button
|
|
138
|
+
variant="ghost"
|
|
139
|
+
className="text-destructive hover:text-destructive"
|
|
140
|
+
>
|
|
141
|
+
危险按钮
|
|
142
|
+
</Button>
|
|
143
|
+
<Button
|
|
144
|
+
variant="link"
|
|
145
|
+
className="text-destructive hover:text-destructive"
|
|
146
|
+
>
|
|
147
|
+
危险按钮
|
|
139
148
|
</Button>
|
|
140
149
|
</div>
|
|
141
150
|
),
|
|
@@ -146,8 +155,8 @@ export const Group: Story = {
|
|
|
146
155
|
render: () => (
|
|
147
156
|
<div className="flex flex-col gap-4">
|
|
148
157
|
<ButtonGroup>
|
|
149
|
-
<Button>
|
|
150
|
-
<Button variant="
|
|
158
|
+
<Button variant="outline">Cancel</Button>
|
|
159
|
+
<Button variant="outline">OK</Button>
|
|
151
160
|
</ButtonGroup>
|
|
152
161
|
|
|
153
162
|
<ButtonGroup>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| `asChild` | `boolean` | – | – | 使用 Slot 模式渲染子元素 |
|
|
27
27
|
| `triggerLabel` | `string` | `'更多操作'` | – | 触发按钮的可访问名(aria-label)。 |
|
|
28
28
|
| `variant` | `ButtonProps['variant']` | `'ghost'` | – | 触发按钮 variant,与 ButtonGroup 中相邻按钮风格保持一致。 |
|
|
29
|
-
| `size` | `ButtonProps['size']` |
|
|
29
|
+
| `size` | `ButtonProps['size']` | – | – | 触发按钮 size。不传时根据 variant 自选: - `variant='link'` → `'default'`(命中 link×default 的 h-auto px-0,与相邻 link 文字按钮等高) - 其余 → `'icon'`(保持 32×32 方形热区) |
|
|
30
30
|
| `icon` | `React.ReactNode` | – | – | 自定义触发器图标,传入即替换默认 ⋯。 |
|
|
31
31
|
| `align` | `'start' \| 'center' \| 'end'` | `'end'` | – | 下拉菜单对齐方式。 |
|
|
32
32
|
| `sideOffset` | `number` | – | – | 下拉菜单与触发器的距离(像素)。 |
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
```tsx
|
|
43
43
|
<ButtonGroup>
|
|
44
44
|
<Button variant="outline">取消</Button>
|
|
45
|
-
<Button>确定</Button>
|
|
45
|
+
<Button variant="outline">确定</Button>
|
|
46
46
|
</ButtonGroup>
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -132,10 +132,7 @@
|
|
|
132
132
|
|
|
133
133
|
```tsx
|
|
134
134
|
<ButtonGroup>
|
|
135
|
-
<Button>
|
|
136
|
-
<PlusIcon data-icon="inline-start" />
|
|
137
|
-
新建实例
|
|
138
|
-
</Button>
|
|
135
|
+
<Button>新建实例</Button>
|
|
139
136
|
<DropdownMenu modal={false}>
|
|
140
137
|
<DropdownMenuTrigger asChild>
|
|
141
138
|
<Button size="icon" aria-label="更多选项">
|
|
@@ -169,10 +166,10 @@
|
|
|
169
166
|
|
|
170
167
|
### ActionColumn
|
|
171
168
|
|
|
172
|
-
操作列 — DataTable 操作列经典模式:
|
|
169
|
+
操作列 — DataTable 操作列经典模式:h-4 锁高 + gap-3 间距 + ButtonGroupSeparator 分隔 + ButtonGroupOverflow 折叠。
|
|
173
170
|
|
|
174
171
|
```tsx
|
|
175
|
-
<ButtonGroup>
|
|
172
|
+
<ButtonGroup className="h-4 gap-3">
|
|
176
173
|
<Button variant="link">管理</Button>
|
|
177
174
|
<ButtonGroupSeparator />
|
|
178
175
|
<Button variant="link" disabled>
|
|
@@ -184,7 +181,7 @@
|
|
|
184
181
|
<ButtonGroupOverflow variant="link">
|
|
185
182
|
<DropdownMenuItem disabled>释放</DropdownMenuItem>
|
|
186
183
|
<DropdownMenuItem>暂停</DropdownMenuItem>
|
|
187
|
-
<DropdownMenuItem
|
|
184
|
+
<DropdownMenuItem variant="destructive">删除</DropdownMenuItem>
|
|
188
185
|
</ButtonGroupOverflow>
|
|
189
186
|
</ButtonGroup>
|
|
190
187
|
```
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
AlignCenterIcon,
|
|
8
8
|
AlignRightIcon,
|
|
9
9
|
ChevronDownIcon,
|
|
10
|
-
PlusIcon,
|
|
11
10
|
CopyIcon,
|
|
12
11
|
TrashIcon,
|
|
13
12
|
PencilIcon,
|
|
@@ -40,7 +39,7 @@ export const Horizontal: Story = {
|
|
|
40
39
|
render: () => (
|
|
41
40
|
<ButtonGroup>
|
|
42
41
|
<Button variant="outline">取消</Button>
|
|
43
|
-
<Button>确定</Button>
|
|
42
|
+
<Button variant="outline">确定</Button>
|
|
44
43
|
</ButtonGroup>
|
|
45
44
|
),
|
|
46
45
|
};
|
|
@@ -125,10 +124,7 @@ export const MenuButton: Story = {
|
|
|
125
124
|
export const SplitButton: Story = {
|
|
126
125
|
render: () => (
|
|
127
126
|
<ButtonGroup>
|
|
128
|
-
<Button>
|
|
129
|
-
<PlusIcon data-icon="inline-start" />
|
|
130
|
-
新建实例
|
|
131
|
-
</Button>
|
|
127
|
+
<Button>新建实例</Button>
|
|
132
128
|
<DropdownMenu modal={false}>
|
|
133
129
|
<DropdownMenuTrigger asChild>
|
|
134
130
|
<Button size="icon" aria-label="更多选项">
|
|
@@ -212,25 +212,30 @@ function CalendarDayButton({
|
|
|
212
212
|
if (modifiers.focused) ref.current?.focus();
|
|
213
213
|
}, [modifiers.focused]);
|
|
214
214
|
|
|
215
|
+
const isSelectedSingle =
|
|
216
|
+
modifiers.selected &&
|
|
217
|
+
!modifiers.range_start &&
|
|
218
|
+
!modifiers.range_end &&
|
|
219
|
+
!modifiers.range_middle;
|
|
220
|
+
const isActive =
|
|
221
|
+
isSelectedSingle || modifiers.range_start || modifiers.range_end;
|
|
222
|
+
|
|
215
223
|
return (
|
|
216
224
|
<Button
|
|
217
225
|
ref={ref}
|
|
218
226
|
variant="ghost"
|
|
219
227
|
size="icon"
|
|
220
228
|
data-day={day.date.toLocaleDateString(locale?.code)}
|
|
221
|
-
data-selected-single={
|
|
222
|
-
modifiers.selected &&
|
|
223
|
-
!modifiers.range_start &&
|
|
224
|
-
!modifiers.range_end &&
|
|
225
|
-
!modifiers.range_middle
|
|
226
|
-
}
|
|
229
|
+
data-selected-single={isSelectedSingle}
|
|
227
230
|
data-range-start={modifiers.range_start}
|
|
228
231
|
data-range-end={modifiers.range_end}
|
|
229
232
|
data-range-middle={modifiers.range_middle}
|
|
230
233
|
className={cn(
|
|
231
|
-
'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 text-xs leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-1 group-data-[focused=true]/day:ring-ring data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-
|
|
234
|
+
'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 text-xs leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-1 group-data-[focused=true]/day:ring-ring data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
|
|
232
235
|
defaultClassNames.day,
|
|
233
236
|
className,
|
|
237
|
+
isActive &&
|
|
238
|
+
'!bg-primary !text-primary-foreground hover:!bg-primary/90 hover:!text-primary-foreground',
|
|
234
239
|
)}
|
|
235
240
|
{...props}
|
|
236
241
|
/>
|
|
@@ -247,7 +247,7 @@ function CascaderSelect({
|
|
|
247
247
|
onClick={() => setOpen(!open)}
|
|
248
248
|
className={cn(
|
|
249
249
|
'flex w-fit min-w-36 cursor-pointer items-center justify-between gap-1.5 rounded-md border border-input bg-transparent px-2.5 text-xs transition-colors outline-none select-none',
|
|
250
|
-
'focus-visible:border-ring focus-visible:ring-
|
|
250
|
+
'focus-visible:border-ring focus-visible:ring-ring/20',
|
|
251
251
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
252
252
|
size === 'default' && 'h-8',
|
|
253
253
|
size === 'sm' && 'h-7',
|
|
@@ -84,7 +84,7 @@ function Checkbox({
|
|
|
84
84
|
disabled={resolvedDisabled}
|
|
85
85
|
value={value}
|
|
86
86
|
className={cn(
|
|
87
|
-
'peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-(--radius-checkbox) border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-
|
|
87
|
+
'peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-(--radius-checkbox) border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground',
|
|
88
88
|
className,
|
|
89
89
|
)}
|
|
90
90
|
{...props}
|
|
@@ -52,6 +52,10 @@ interface ComboboxContextValue {
|
|
|
52
52
|
setSearch: (search: string) => void;
|
|
53
53
|
/** 多选 Tag 显示上限,超出折叠为 +N。 */
|
|
54
54
|
maxTagCount?: number;
|
|
55
|
+
/** 折叠摘要自定义内容。 */
|
|
56
|
+
maxTagPlaceholder?:
|
|
57
|
+
| React.ReactNode
|
|
58
|
+
| ((omittedValues: string[], total: number) => React.ReactNode);
|
|
55
59
|
/** 禁用状态。 */
|
|
56
60
|
disabled: boolean;
|
|
57
61
|
/** 当前面板是否打开。 */
|
|
@@ -119,6 +123,13 @@ interface ComboboxBaseProps {
|
|
|
119
123
|
* 仅 `multiple=true` 时生效。
|
|
120
124
|
*/
|
|
121
125
|
maxTagCount?: number;
|
|
126
|
+
/**
|
|
127
|
+
* 折叠摘要的自定义内容。不传则默认显示 `+N`。
|
|
128
|
+
* 传字符串直接显示;传函数则接收 `(omittedValues: string[], total: number)` 返回 ReactNode。
|
|
129
|
+
*/
|
|
130
|
+
maxTagPlaceholder?:
|
|
131
|
+
| React.ReactNode
|
|
132
|
+
| ((omittedValues: string[], total: number) => React.ReactNode);
|
|
122
133
|
/** 是否启用 cmdk 内置过滤逻辑;关闭后由外部接管搜索结果。 @default true */
|
|
123
134
|
shouldFilter?: boolean;
|
|
124
135
|
/** 自定义匹配评分函数:返回 0 表示不匹配,>0 越大越靠前。 */
|
|
@@ -144,6 +155,7 @@ function Combobox(props: ComboboxProps) {
|
|
|
144
155
|
onOpenChange,
|
|
145
156
|
disabled = false,
|
|
146
157
|
maxTagCount,
|
|
158
|
+
maxTagPlaceholder,
|
|
147
159
|
shouldFilter = true,
|
|
148
160
|
filter,
|
|
149
161
|
loop,
|
|
@@ -242,6 +254,7 @@ function Combobox(props: ComboboxProps) {
|
|
|
242
254
|
clearValue,
|
|
243
255
|
itemLabels,
|
|
244
256
|
maxTagCount,
|
|
257
|
+
maxTagPlaceholder,
|
|
245
258
|
disabled,
|
|
246
259
|
open,
|
|
247
260
|
setOpen,
|
|
@@ -256,6 +269,7 @@ function Combobox(props: ComboboxProps) {
|
|
|
256
269
|
clearValue,
|
|
257
270
|
itemLabels,
|
|
258
271
|
maxTagCount,
|
|
272
|
+
maxTagPlaceholder,
|
|
259
273
|
disabled,
|
|
260
274
|
open,
|
|
261
275
|
setOpen,
|
|
@@ -295,12 +309,18 @@ export interface ComboboxTriggerProps
|
|
|
295
309
|
* 受控模式下由调用方传入 `() => setValue([])`;不传则不展示清除按钮。
|
|
296
310
|
*/
|
|
297
311
|
onClear?: () => void;
|
|
312
|
+
/**
|
|
313
|
+
* 右侧自定义插槽,替换默认的 chevron / clear 区域。
|
|
314
|
+
* 用于搜索图标、分隔线 + 按钮等场景。
|
|
315
|
+
*/
|
|
316
|
+
suffix?: React.ReactNode;
|
|
298
317
|
}
|
|
299
318
|
|
|
300
319
|
function ComboboxTrigger({
|
|
301
320
|
className,
|
|
302
321
|
size = 'default',
|
|
303
322
|
onClear,
|
|
323
|
+
suffix,
|
|
304
324
|
children,
|
|
305
325
|
...props
|
|
306
326
|
}: ComboboxTriggerProps) {
|
|
@@ -352,7 +372,7 @@ function ComboboxTrigger({
|
|
|
352
372
|
}}
|
|
353
373
|
className={cn(
|
|
354
374
|
// 视觉与 SelectTrigger 对齐;focus-within 产生 ring;min-h 容纳多行 Tag
|
|
355
|
-
"flex w-fit cursor-text items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-1 pr-2 pl-2.5 text-xs transition-colors outline-none has-[input:focus]:border-ring has-[input:focus]:ring-
|
|
375
|
+
"flex w-fit cursor-text items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-1 pr-2 pl-2.5 text-xs transition-colors outline-none has-[input:focus]:border-ring has-[input:focus]:ring-ring/20 data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[size=default]:min-h-8 data-[size=sm]:min-h-7 data-[size=lg]:min-h-9 data-[size=lg]:text-sm aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
356
376
|
className,
|
|
357
377
|
)}
|
|
358
378
|
{...props}
|
|
@@ -392,7 +412,9 @@ function ComboboxTrigger({
|
|
|
392
412
|
className="min-w-10 flex-1 bg-transparent text-xs outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
|
|
393
413
|
/>
|
|
394
414
|
</div>
|
|
395
|
-
{
|
|
415
|
+
{suffix !== undefined ? (
|
|
416
|
+
suffix
|
|
417
|
+
) : showClear ? (
|
|
396
418
|
<span
|
|
397
419
|
role="button"
|
|
398
420
|
aria-label="清除"
|
|
@@ -472,13 +494,25 @@ function ComboboxValue({
|
|
|
472
494
|
})
|
|
473
495
|
.join(', ');
|
|
474
496
|
|
|
497
|
+
// 计算折叠摘要内容
|
|
498
|
+
const summaryContent = (() => {
|
|
499
|
+
if (omitted.length === 0) return null;
|
|
500
|
+
const placeholder = ctx.maxTagPlaceholder;
|
|
501
|
+
if (placeholder === undefined) return `+${omitted.length}`;
|
|
502
|
+
if (typeof placeholder === 'function')
|
|
503
|
+
return placeholder(omitted, values.length);
|
|
504
|
+
return placeholder;
|
|
505
|
+
})();
|
|
506
|
+
|
|
475
507
|
return (
|
|
476
508
|
<>
|
|
477
509
|
{visible.map((v) => (
|
|
478
510
|
<Tag
|
|
479
511
|
key={v}
|
|
480
512
|
size="sm"
|
|
481
|
-
|
|
513
|
+
variant="solid"
|
|
514
|
+
color="primary"
|
|
515
|
+
closable={{ hoverReveal: true }}
|
|
482
516
|
disabled={ctx.disabled}
|
|
483
517
|
onClose={(e) => {
|
|
484
518
|
e.preventDefault();
|
|
@@ -496,10 +530,15 @@ function ComboboxValue({
|
|
|
496
530
|
{ctx.itemLabels.get(v) ?? v}
|
|
497
531
|
</Tag>
|
|
498
532
|
))}
|
|
499
|
-
{
|
|
500
|
-
<
|
|
501
|
-
|
|
502
|
-
|
|
533
|
+
{summaryContent !== null ? (
|
|
534
|
+
<span
|
|
535
|
+
key="__rest__"
|
|
536
|
+
data-slot="combobox-summary"
|
|
537
|
+
className="inline-flex shrink-0 items-center text-xs text-muted-foreground"
|
|
538
|
+
title={omittedTitle}
|
|
539
|
+
>
|
|
540
|
+
{summaryContent}
|
|
541
|
+
</span>
|
|
503
542
|
) : null}
|
|
504
543
|
</>
|
|
505
544
|
);
|
|
@@ -630,8 +669,8 @@ function ComboboxItem({
|
|
|
630
669
|
ctx.toggleValue(value);
|
|
631
670
|
}}
|
|
632
671
|
className={cn(
|
|
633
|
-
// 与 SelectItem 视觉对齐:h-8
|
|
634
|
-
"group/combobox-item relative flex h-8 w-full cursor-pointer items-center gap-1.5 rounded-md py-1 pr-2 pl-7 text-xs outline-hidden select-none
|
|
672
|
+
// 与 SelectItem 视觉对齐:h-8、左侧勾选位;hover 灰底,选中蓝底(含 hover 保持)
|
|
673
|
+
"group/combobox-item relative flex h-8 w-full cursor-pointer items-center gap-1.5 rounded-md py-1 pr-2 pl-7 text-xs outline-hidden select-none hover:bg-muted data-[state=checked]:bg-primary/10 data-[state=checked]:hover:bg-primary/10 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
635
674
|
className,
|
|
636
675
|
)}
|
|
637
676
|
{...props}
|
|
@@ -661,7 +700,10 @@ function collectComboboxLabels(
|
|
|
661
700
|
value?: string;
|
|
662
701
|
children?: React.ReactNode;
|
|
663
702
|
}>;
|
|
664
|
-
if (
|
|
703
|
+
if (
|
|
704
|
+
element.type === ComboboxItem ||
|
|
705
|
+
(element.type as { displayName?: string }).displayName === 'ComboboxItem'
|
|
706
|
+
) {
|
|
665
707
|
const v = element.props.value;
|
|
666
708
|
if (typeof v === 'string') {
|
|
667
709
|
map.set(v, element.props.children);
|
|
@@ -674,6 +716,8 @@ function collectComboboxLabels(
|
|
|
674
716
|
});
|
|
675
717
|
}
|
|
676
718
|
|
|
719
|
+
ComboboxItem.displayName = 'ComboboxItem';
|
|
720
|
+
|
|
677
721
|
export {
|
|
678
722
|
Combobox,
|
|
679
723
|
ComboboxContent,
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
- 选项数量较多需要边输入边过滤时
|
|
14
14
|
- 需要多选并以可关闭标签形式展示已选时
|
|
15
15
|
- Select 无法满足搜索 / 多选 / 远程数据等场景时
|
|
16
|
-
-
|
|
16
|
+
- 触发区内置 search input,输入即过滤选项;多选模式下已选 Tag 与 input 共占触发区。
|
|
17
|
+
- 组合结构:Combobox > ComboboxTrigger ( ComboboxValue ) + ComboboxContent > ComboboxList > ( ComboboxEmpty | ComboboxGroup > ComboboxItem | ComboboxSeparator )
|
|
17
18
|
|
|
18
19
|
## Props
|
|
19
20
|
|
|
@@ -21,11 +22,8 @@
|
|
|
21
22
|
| --- | --- | --- | --- | --- |
|
|
22
23
|
| `size` | `'sm' \| 'default' \| 'lg'` | – | – | 触发器尺寸,对齐 SelectTrigger 三档:sm 28px / default 32px / lg 36px。 |
|
|
23
24
|
| `onClear` | `() => void` | – | – | 清除回调:传入后右侧 chevron 切换为 × 按钮,点击调用此回调清空选中。 受控模式下由调用方传入 `() => setValue([])`;不传则不展示清除按钮。 |
|
|
25
|
+
| `suffix` | `React.ReactNode` | – | – | 右侧自定义插槽,替换默认的 chevron / clear 区域。 用于搜索图标、分隔线 + 按钮等场景。 |
|
|
24
26
|
| `placeholder` | `React.ReactNode` | – | – | 未选时占位文案。 |
|
|
25
|
-
| `shouldFilter` | `boolean` | `true` | – | 是否启用 cmdk 内置过滤逻辑;关闭后由外部接管搜索结果。 |
|
|
26
|
-
| `filter` | `(value: string, search: string, keywords?: string[]) => number` | – | – | 自定义匹配评分函数:返回 0 表示不匹配,>0 越大越靠前。 |
|
|
27
|
-
| `loop` | `boolean` | `false` | – | 到达列表末尾时是否循环回到起点。 |
|
|
28
|
-
| `label` | `string` | – | – | 命令面板的可访问性标签。 |
|
|
29
27
|
| `value` | `string` | – | ✓ | 选项值,必填,用于受控状态匹配。 |
|
|
30
28
|
| `disabled` | `boolean` | – | – | 是否禁用。 |
|
|
31
29
|
| `keywords` | `string[]` | – | – | cmdk 搜索关键字(除 children 文本外补充匹配项)。 |
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ComboboxTrigger,
|
|
12
12
|
ComboboxValue,
|
|
13
13
|
} from './index';
|
|
14
|
+
import { Tag } from '@/components/tag';
|
|
14
15
|
|
|
15
16
|
const meta: Meta<typeof Combobox> = {
|
|
16
17
|
title: '数据录入 · Data Entry/Combobox 组合选择器',
|
|
@@ -97,35 +98,113 @@ export const Multiple: Story = {
|
|
|
97
98
|
},
|
|
98
99
|
};
|
|
99
100
|
|
|
100
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* 多选模式下通过 `maxTagCount` 控制选择的个数,通过 `maxTagPlaceholder` 控制选择的 hover 样式。
|
|
103
|
+
*/
|
|
101
104
|
export const WithMaxTagCount: Story = {
|
|
102
105
|
render: () => {
|
|
103
|
-
const [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
]);
|
|
106
|
+
const allValues = ['bj', 'sh', 'hz', 'sz', 'cd'] as const;
|
|
107
|
+
const [v1, setV1] = React.useState<string[]>([...allValues]);
|
|
108
|
+
const [v2, setV2] = React.useState<string[]>([...allValues]);
|
|
109
|
+
const [v3, setV3] = React.useState<string[]>([...allValues]);
|
|
110
|
+
const [v4, setV4] = React.useState<string[]>([...allValues]);
|
|
109
111
|
return (
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
<div className="flex flex-wrap items-start gap-3">
|
|
113
|
+
{/* 默认 +N */}
|
|
114
|
+
<Combobox multiple maxTagCount={2} value={v1} onValueChange={setV1}>
|
|
115
|
+
<ComboboxTrigger className="w-60">
|
|
116
|
+
<ComboboxValue placeholder="选择城市" />
|
|
117
|
+
</ComboboxTrigger>
|
|
118
|
+
<ComboboxContent>
|
|
119
|
+
<ComboboxList>
|
|
120
|
+
<ComboboxGroup>
|
|
121
|
+
{cityOptions.map((o) => (
|
|
122
|
+
<ComboboxItem key={o.value} value={o.value}>
|
|
123
|
+
{o.label}
|
|
124
|
+
</ComboboxItem>
|
|
125
|
+
))}
|
|
126
|
+
</ComboboxGroup>
|
|
127
|
+
</ComboboxList>
|
|
128
|
+
</ComboboxContent>
|
|
129
|
+
</Combobox>
|
|
130
|
+
{/* 自定义摘要 wrapped in Tag */}
|
|
131
|
+
<Combobox
|
|
132
|
+
multiple
|
|
133
|
+
maxTagCount={2}
|
|
134
|
+
maxTagPlaceholder={(_omitted, total) => (
|
|
135
|
+
<Tag size="sm" variant="solid" color="primary">
|
|
136
|
+
已选择 {total}/{cityOptions.length} 项
|
|
137
|
+
</Tag>
|
|
138
|
+
)}
|
|
139
|
+
value={v2}
|
|
140
|
+
onValueChange={setV2}
|
|
114
141
|
>
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{o.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
<ComboboxTrigger className="w-60">
|
|
143
|
+
<ComboboxValue placeholder="选择城市" />
|
|
144
|
+
</ComboboxTrigger>
|
|
145
|
+
<ComboboxContent>
|
|
146
|
+
<ComboboxList>
|
|
147
|
+
<ComboboxGroup>
|
|
148
|
+
{cityOptions.map((o) => (
|
|
149
|
+
<ComboboxItem key={o.value} value={o.value}>
|
|
150
|
+
{o.label}
|
|
151
|
+
</ComboboxItem>
|
|
152
|
+
))}
|
|
153
|
+
</ComboboxGroup>
|
|
154
|
+
</ComboboxList>
|
|
155
|
+
</ComboboxContent>
|
|
156
|
+
</Combobox>
|
|
157
|
+
{/* 简洁 x/total 纯文本 */}
|
|
158
|
+
<Combobox
|
|
159
|
+
multiple
|
|
160
|
+
maxTagCount={2}
|
|
161
|
+
maxTagPlaceholder={(_omitted, total) =>
|
|
162
|
+
`${total}/${cityOptions.length}`
|
|
163
|
+
}
|
|
164
|
+
value={v3}
|
|
165
|
+
onValueChange={setV3}
|
|
166
|
+
>
|
|
167
|
+
<ComboboxTrigger className="w-60">
|
|
168
|
+
<ComboboxValue placeholder="选择城市" />
|
|
169
|
+
</ComboboxTrigger>
|
|
170
|
+
<ComboboxContent>
|
|
171
|
+
<ComboboxList>
|
|
172
|
+
<ComboboxGroup>
|
|
173
|
+
{cityOptions.map((o) => (
|
|
174
|
+
<ComboboxItem key={o.value} value={o.value}>
|
|
175
|
+
{o.label}
|
|
176
|
+
</ComboboxItem>
|
|
177
|
+
))}
|
|
178
|
+
</ComboboxGroup>
|
|
179
|
+
</ComboboxList>
|
|
180
|
+
</ComboboxContent>
|
|
181
|
+
</Combobox>
|
|
182
|
+
{/* maxTagCount=1 + 纯文本摘要 */}
|
|
183
|
+
<Combobox
|
|
184
|
+
multiple
|
|
185
|
+
maxTagCount={1}
|
|
186
|
+
maxTagPlaceholder={(_omitted, total) =>
|
|
187
|
+
`已选择 ${total}/${cityOptions.length} 项`
|
|
188
|
+
}
|
|
189
|
+
value={v4}
|
|
190
|
+
onValueChange={setV4}
|
|
191
|
+
>
|
|
192
|
+
<ComboboxTrigger className="w-60">
|
|
193
|
+
<ComboboxValue placeholder="选择城市" />
|
|
194
|
+
</ComboboxTrigger>
|
|
195
|
+
<ComboboxContent>
|
|
196
|
+
<ComboboxList>
|
|
197
|
+
<ComboboxGroup>
|
|
198
|
+
{cityOptions.map((o) => (
|
|
199
|
+
<ComboboxItem key={o.value} value={o.value}>
|
|
200
|
+
{o.label}
|
|
201
|
+
</ComboboxItem>
|
|
202
|
+
))}
|
|
203
|
+
</ComboboxGroup>
|
|
204
|
+
</ComboboxList>
|
|
205
|
+
</ComboboxContent>
|
|
206
|
+
</Combobox>
|
|
207
|
+
</div>
|
|
129
208
|
);
|
|
130
209
|
},
|
|
131
210
|
};
|
|
@@ -781,7 +781,10 @@ export const CustomCell: Story = {
|
|
|
781
781
|
<ButtonGroup className="h-4 gap-3">
|
|
782
782
|
<Button variant="link">编辑</Button>
|
|
783
783
|
<ButtonGroupSeparator />
|
|
784
|
-
<Button
|
|
784
|
+
<Button
|
|
785
|
+
variant="link"
|
|
786
|
+
className="text-destructive hover:text-destructive"
|
|
787
|
+
>
|
|
785
788
|
删除
|
|
786
789
|
</Button>
|
|
787
790
|
</ButtonGroup>
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from 'date-fns';
|
|
30
30
|
import { zhCN } from 'date-fns/locale';
|
|
31
31
|
import {
|
|
32
|
-
|
|
32
|
+
CalendarDays as CalendarIcon,
|
|
33
33
|
ChevronLeft,
|
|
34
34
|
ChevronRight,
|
|
35
35
|
ChevronsLeft,
|
|
@@ -578,6 +578,9 @@ function DatePicker({
|
|
|
578
578
|
dateToParts(value),
|
|
579
579
|
);
|
|
580
580
|
|
|
581
|
+
// Track pointer-down inside popover to suppress blur side-effects
|
|
582
|
+
const pointerInPanelRef = React.useRef(false);
|
|
583
|
+
|
|
581
584
|
// input text
|
|
582
585
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
583
586
|
const anchorRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -611,6 +614,7 @@ function DatePicker({
|
|
|
611
614
|
};
|
|
612
615
|
|
|
613
616
|
const handleInputBlur = () => {
|
|
617
|
+
if (pointerInPanelRef.current) return;
|
|
614
618
|
const parsed = tryParseDate(inputText, displayFormat);
|
|
615
619
|
if (parsed) commitValue(parsed);
|
|
616
620
|
else if (inputText.trim() === '') commitValue(undefined);
|
|
@@ -724,6 +728,7 @@ function DatePicker({
|
|
|
724
728
|
<CalendarIcon
|
|
725
729
|
data-slot="date-picker-icon"
|
|
726
730
|
aria-hidden
|
|
731
|
+
strokeWidth={1.5}
|
|
727
732
|
className="size-4 shrink-0 text-muted-foreground"
|
|
728
733
|
/>
|
|
729
734
|
)}
|
|
@@ -732,6 +737,12 @@ function DatePicker({
|
|
|
732
737
|
<PopoverContent
|
|
733
738
|
data-slot="date-picker-content"
|
|
734
739
|
className="w-auto rounded-md p-0"
|
|
740
|
+
onPointerDown={() => {
|
|
741
|
+
pointerInPanelRef.current = true;
|
|
742
|
+
}}
|
|
743
|
+
onPointerUp={() => {
|
|
744
|
+
pointerInPanelRef.current = false;
|
|
745
|
+
}}
|
|
735
746
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
736
747
|
onInteractOutside={(e) => {
|
|
737
748
|
const target = e.detail.originalEvent.target as Node | null;
|
|
@@ -806,7 +817,7 @@ function DatePicker({
|
|
|
806
817
|
) : (
|
|
807
818
|
<Calendar
|
|
808
819
|
mode="single"
|
|
809
|
-
selected={
|
|
820
|
+
selected={draftDate}
|
|
810
821
|
onSelect={handleSelectDate}
|
|
811
822
|
disabled={disabledDates}
|
|
812
823
|
defaultMonth={value}
|
|
@@ -972,6 +983,9 @@ function DateRangePicker({
|
|
|
972
983
|
const [hoveredDate, setHoveredDate] = React.useState<Date | undefined>();
|
|
973
984
|
const [activeField, setActiveField] = React.useState<'from' | 'to'>('from');
|
|
974
985
|
|
|
986
|
+
// Track pointer-down inside popover to suppress blur side-effects
|
|
987
|
+
const pointerInPanelRef2 = React.useRef(false);
|
|
988
|
+
|
|
975
989
|
const fromInputRef = React.useRef<HTMLInputElement>(null);
|
|
976
990
|
const toInputRef = React.useRef<HTMLInputElement>(null);
|
|
977
991
|
const anchorRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -1115,6 +1129,7 @@ function DateRangePicker({
|
|
|
1115
1129
|
};
|
|
1116
1130
|
|
|
1117
1131
|
const handleFromBlur = () => {
|
|
1132
|
+
if (pointerInPanelRef2.current) return;
|
|
1118
1133
|
const t = fromText.trim();
|
|
1119
1134
|
if (!t) {
|
|
1120
1135
|
// 仅当之前已有起始值时,才视为用户主动清空。
|
|
@@ -1145,6 +1160,7 @@ function DateRangePicker({
|
|
|
1145
1160
|
};
|
|
1146
1161
|
|
|
1147
1162
|
const handleToBlur = () => {
|
|
1163
|
+
if (pointerInPanelRef2.current) return;
|
|
1148
1164
|
const t = toText.trim();
|
|
1149
1165
|
if (!t) {
|
|
1150
1166
|
// 同 handleFromBlur:仅当之前已有结束值时才主动清空,避免吞掉日历首次点击
|
|
@@ -1375,6 +1391,7 @@ function DateRangePicker({
|
|
|
1375
1391
|
<CalendarIcon
|
|
1376
1392
|
data-slot="date-range-picker-icon"
|
|
1377
1393
|
aria-hidden
|
|
1394
|
+
strokeWidth={1.5}
|
|
1378
1395
|
className="size-4 shrink-0 text-muted-foreground"
|
|
1379
1396
|
/>
|
|
1380
1397
|
)}
|
|
@@ -1383,6 +1400,12 @@ function DateRangePicker({
|
|
|
1383
1400
|
<PopoverContent
|
|
1384
1401
|
data-slot="date-range-picker-content"
|
|
1385
1402
|
className={cn('w-auto rounded-md p-0', RANGE_PREVIEW_BASE_CLASS)}
|
|
1403
|
+
onPointerDown={() => {
|
|
1404
|
+
pointerInPanelRef2.current = true;
|
|
1405
|
+
}}
|
|
1406
|
+
onPointerUp={() => {
|
|
1407
|
+
pointerInPanelRef2.current = false;
|
|
1408
|
+
}}
|
|
1386
1409
|
onPointerLeave={() => setHoveredDate(undefined)}
|
|
1387
1410
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
1388
1411
|
onInteractOutside={(e) => {
|
|
@@ -209,7 +209,7 @@ function FieldInsetBox({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
209
209
|
data-slot="field-inset-box"
|
|
210
210
|
className={cn(
|
|
211
211
|
'flex h-8 w-full items-center rounded-md border border-input bg-card',
|
|
212
|
-
'focus-within:border-ring focus-within:ring-
|
|
212
|
+
'focus-within:border-ring focus-within:ring-ring/20',
|
|
213
213
|
'group-data-[invalid=true]/field:!border-destructive group-data-[invalid=true]/field:!shadow-none',
|
|
214
214
|
'group-data-[invalid=true]/field:focus-within:!border-destructive group-data-[invalid=true]/field:focus-within:ring-destructive/20',
|
|
215
215
|
INSET_INNER_RESET,
|
|
@@ -418,7 +418,7 @@ function FilterBarSearch({
|
|
|
418
418
|
data-slot="filter-bar-search"
|
|
419
419
|
className={cn(
|
|
420
420
|
'flex h-8 items-center overflow-hidden rounded-md border border-input bg-card',
|
|
421
|
-
'focus-within:border-ring focus-within:ring-
|
|
421
|
+
'focus-within:border-ring focus-within:ring-ring/20',
|
|
422
422
|
className,
|
|
423
423
|
)}
|
|
424
424
|
{...props}
|