@teamix-evo/ui 0.5.0 → 0.6.0

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 (63) hide show
  1. package/package.json +19 -17
  2. package/src/components/alert/index.tsx +1 -1
  3. package/src/components/alert-dialog/index.tsx +17 -24
  4. package/src/components/alert-dialog/meta.md +102 -8
  5. package/src/components/alert-dialog/stories.tsx +117 -7
  6. package/src/components/avatar/index.tsx +1 -1
  7. package/src/components/badge/index.tsx +1 -1
  8. package/src/components/button/index.tsx +3 -31
  9. package/src/components/button/meta.md +24 -13
  10. package/src/components/button/stories.tsx +24 -12
  11. package/src/components/button-group/meta.md +6 -9
  12. package/src/components/button-group/stories.tsx +2 -6
  13. package/src/components/calendar/index.tsx +12 -7
  14. package/src/components/cascader-select/index.tsx +1 -1
  15. package/src/components/checkbox/index.tsx +1 -1
  16. package/src/components/combobox/index.tsx +54 -10
  17. package/src/components/combobox/meta.md +3 -5
  18. package/src/components/combobox/stories.tsx +104 -25
  19. package/src/components/data-table/index.tsx +10 -5
  20. package/src/components/data-table/stories.tsx +4 -1
  21. package/src/components/date-picker/index.tsx +25 -2
  22. package/src/components/field/index.tsx +1 -1
  23. package/src/components/filter-bar/index.tsx +1 -1
  24. package/src/components/float-button/meta.md +3 -15
  25. package/src/components/icon/index.tsx +3 -4
  26. package/src/components/icon/meta.md +1 -2
  27. package/src/components/input/index.tsx +11 -3
  28. package/src/components/input-group/index.tsx +33 -23
  29. package/src/components/input-group/meta.md +15 -0
  30. package/src/components/input-group/stories.tsx +14 -0
  31. package/src/components/input-ip/index.tsx +1 -1
  32. package/src/components/input-number/index.tsx +5 -5
  33. package/src/components/item/meta.md +10 -42
  34. package/src/components/item/stories.tsx +12 -44
  35. package/src/components/radio-group/index.tsx +1 -1
  36. package/src/components/rate/index.tsx +3 -3
  37. package/src/components/select/index.tsx +2 -2
  38. package/src/components/skeleton/index.tsx +1 -1
  39. package/src/components/skeleton/meta.md +6 -6
  40. package/src/components/skeleton/stories.tsx +8 -8
  41. package/src/components/slider/index.tsx +27 -1
  42. package/src/components/sonner/index.tsx +43 -40
  43. package/src/components/sonner/meta.md +84 -68
  44. package/src/components/sonner/stories.tsx +122 -83
  45. package/src/components/spinner/index.tsx +170 -0
  46. package/src/components/spinner/meta.md +27 -1
  47. package/src/components/spinner/stories.tsx +23 -0
  48. package/src/components/steps/index.tsx +5 -1
  49. package/src/components/switch/index.tsx +1 -1
  50. package/src/components/table/index.tsx +6 -2
  51. package/src/components/tag/index.tsx +14 -0
  52. package/src/components/tag/meta.md +1 -0
  53. package/src/components/tag/stories.tsx +13 -0
  54. package/src/components/textarea/index.tsx +1 -1
  55. package/src/components/textarea/stories.tsx +1 -1
  56. package/src/components/time-picker/index.tsx +3 -1
  57. package/src/components/toggle/index.tsx +1 -1
  58. package/src/components/tooltip/index.tsx +5 -1
  59. package/src/components/tooltip/meta.md +13 -28
  60. package/src/components/tooltip/stories.tsx +11 -28
  61. package/src/components/transfer/index.tsx +12 -10
  62. package/src/components/tree-select/index.tsx +1 -1
  63. package/LICENSE +0 -21
@@ -18,7 +18,6 @@
18
18
  | 名称 | 类型 | 默认值 | 必填 | 说明 |
19
19
  | --- | --- | --- | --- | --- |
20
20
  | `variant` | `ButtonVariant` | `default` | – | 视觉风格。 |
21
- | `color` | `ButtonColor` | `default` | – | 语义色,与 variant 双 prop 组合。 |
22
21
  | `size` | `ButtonSize` | `default` | – | 尺寸。 |
23
22
  | `shape` | `ButtonShape` | `default` | – | 形状。circle/square 为等宽等高,适合纯图标。 |
24
23
  | `block` | `boolean` | `false` | – | true 时撑满父容器宽度。 |
@@ -91,6 +90,9 @@ Button 只有图标时,应该显示为正方形。
91
90
  <Button size="icon-lg" aria-label="bell">
92
91
  <BellIcon />
93
92
  </Button>
93
+ <Button size="icon" variant="link" aria-label="mail">
94
+ <MailIcon />
95
+ </Button>
94
96
  </div>
95
97
  ```
96
98
 
@@ -112,19 +114,28 @@ Button 只有图标时,应该显示为正方形。
112
114
 
113
115
  ### Warning
114
116
 
115
- 警告按钮用于危险操作的确认入口,可与 variant 组合使用。
117
+ 警告按钮用于危险操作的确认入口,可通过 className 与 variant 组合使用。
116
118
 
117
119
  ```tsx
118
120
  <div className="flex gap-2">
119
- <Button variant="destructive">Destructive</Button>
120
- <Button variant="outline" color="destructive">
121
- Outline
122
- </Button>
123
- <Button variant="ghost" color="destructive">
124
- Ghost
125
- </Button>
126
- <Button variant="link" color="destructive">
127
- Link
121
+ <Button variant="destructive">危险按钮</Button>
122
+ <Button
123
+ variant="outline"
124
+ className="border-destructive text-destructive hover:text-destructive"
125
+ >
126
+ 危险按钮
127
+ </Button>
128
+ <Button
129
+ variant="ghost"
130
+ className="text-destructive hover:text-destructive"
131
+ >
132
+ 危险按钮
133
+ </Button>
134
+ <Button
135
+ variant="link"
136
+ className="text-destructive hover:text-destructive"
137
+ >
138
+ 危险按钮
128
139
  </Button>
129
140
  </div>
130
141
  ```
@@ -136,8 +147,8 @@ Button 只有图标时,应该显示为正方形。
136
147
  ```tsx
137
148
  <div className="flex flex-col gap-4">
138
149
  <ButtonGroup>
139
- <Button>OK</Button>
140
- <Button variant="secondary">Cancel</Button>
150
+ <Button variant="outline">Cancel</Button>
151
+ <Button variant="outline">OK</Button>
141
152
  </ButtonGroup>
142
153
 
143
154
  <ButtonGroup>
@@ -84,6 +84,9 @@ export const IconOnly: Story = {
84
84
  <Button size="icon-lg" aria-label="bell">
85
85
  <BellIcon />
86
86
  </Button>
87
+ <Button size="icon" variant="link" aria-label="mail">
88
+ <MailIcon />
89
+ </Button>
87
90
  </div>
88
91
  ),
89
92
  };
@@ -120,19 +123,28 @@ export const Disabled: Story = {
120
123
  ),
121
124
  };
122
125
 
123
- /** 警告按钮用于危险操作的确认入口,可与 variant 组合使用。 */
126
+ /** 警告按钮用于危险操作的确认入口,可通过 className 与 variant 组合使用。 */
124
127
  export const Warning: Story = {
125
128
  render: () => (
126
129
  <div className="flex gap-2">
127
- <Button variant="destructive">Destructive</Button>
128
- <Button variant="outline" color="destructive">
129
- Outline
130
- </Button>
131
- <Button variant="ghost" color="destructive">
132
- Ghost
133
- </Button>
134
- <Button variant="link" color="destructive">
135
- Link
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
+ 危险按钮
136
148
  </Button>
137
149
  </div>
138
150
  ),
@@ -143,8 +155,8 @@ export const Group: Story = {
143
155
  render: () => (
144
156
  <div className="flex flex-col gap-4">
145
157
  <ButtonGroup>
146
- <Button>OK</Button>
147
- <Button variant="secondary">Cancel</Button>
158
+ <Button variant="outline">Cancel</Button>
159
+ <Button variant="outline">OK</Button>
148
160
  </ButtonGroup>
149
161
 
150
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']` | `'icon'` | – | 触发按钮 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 操作列经典模式:link 风格按钮 + Separator + ButtonGroupOverflow 折叠。
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>二次确认</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-end=true]:bg-primary data-[range-end=true]:text-primary-foreground 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 data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
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-1 focus-visible:ring-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-1 focus-visible:ring-ring 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',
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-1 has-[input:focus]:ring-ring 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",
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
- {showClear ? (
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
- closable
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
- {omitted.length > 0 ? (
500
- <Tag key="__rest__" size="sm" variant="outline" title={omittedTitle}>
501
- {`+${omitted.length}`}
502
- </Tag>
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、左侧勾选位、选中态 text-primary
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 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[state=checked]:text-primary [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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 (element.type === ComboboxItem) {
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
- - 组合结构:Combobox > ComboboxTrigger ( ComboboxValue ) + ComboboxContent > ComboboxInput? + ComboboxList > ( ComboboxEmpty | ComboboxGroup > ComboboxItem | ComboboxSeparator )
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
- /** maxTagCount 折叠:超出阈值的选中项以 +N 摘要展示。 */
101
+ /**
102
+ * 多选模式下通过 `maxTagCount` 控制选择的个数,通过 `maxTagPlaceholder` 控制选择的 hover 样式。
103
+ */
101
104
  export const WithMaxTagCount: Story = {
102
105
  render: () => {
103
- const [value, setValue] = React.useState<string[]>([
104
- 'bj',
105
- 'sh',
106
- 'hz',
107
- 'sz',
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
- <Combobox multiple maxTagCount={2} value={value} onValueChange={setValue}>
111
- <ComboboxTrigger
112
- className="w-72"
113
- onClear={value.length ? () => setValue([]) : undefined}
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
- <ComboboxValue placeholder="最多展示 2 个 Tag,其余折叠 +N" />
116
- </ComboboxTrigger>
117
- <ComboboxContent>
118
- <ComboboxList>
119
- <ComboboxGroup>
120
- {cityOptions.map((o) => (
121
- <ComboboxItem key={o.value} value={o.value}>
122
- {o.label}
123
- </ComboboxItem>
124
- ))}
125
- </ComboboxGroup>
126
- </ComboboxList>
127
- </ComboboxContent>
128
- </Combobox>
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
  };
@@ -442,17 +442,22 @@ function ColumnFilterPopover({
442
442
  return (
443
443
  <Popover open={open} onOpenChange={setOpen}>
444
444
  <PopoverTrigger asChild>
445
- <button
446
- type="button"
445
+ <span
446
+ role="button"
447
447
  aria-label="过滤"
448
+ tabIndex={-1}
448
449
  className={cn(
449
- 'inline-flex size-5 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',
450
+ 'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground',
450
451
  active && 'text-primary',
451
452
  )}
453
+ onPointerDown={(e) => {
454
+ e.preventDefault();
455
+ e.stopPropagation();
456
+ }}
452
457
  onClick={(e) => e.stopPropagation()}
453
458
  >
454
459
  <FilterIcon className="size-3.5" />
455
- </button>
460
+ </span>
456
461
  </PopoverTrigger>
457
462
  <PopoverContent align="start" className="w-44 gap-2 p-2">
458
463
  {mode === 'multiple' ? (
@@ -498,7 +503,7 @@ function ColumnFilterPopover({
498
503
  </RadioGroup>
499
504
  )}
500
505
  <div className="mt-1 flex justify-end gap-2 border-t border-border pt-2">
501
- <Button variant="ghost" size="sm" onClick={handleReset}>
506
+ <Button variant="outline" size="sm" onClick={handleReset}>
502
507
  重置
503
508
  </Button>
504
509
  <Button size="sm" onClick={handleConfirm}>
@@ -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 variant="link" color="destructive">
784
+ <Button
785
+ variant="link"
786
+ className="text-destructive hover:text-destructive"
787
+ >
785
788
  删除
786
789
  </Button>
787
790
  </ButtonGroup>