@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.
Files changed (59) hide show
  1. package/package.json +4 -4
  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 +1 -29
  9. package/src/components/button/meta.md +24 -13
  10. package/src/components/button/stories.tsx +21 -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/stories.tsx +4 -1
  20. package/src/components/date-picker/index.tsx +25 -2
  21. package/src/components/field/index.tsx +1 -1
  22. package/src/components/filter-bar/index.tsx +1 -1
  23. package/src/components/float-button/meta.md +3 -15
  24. package/src/components/icon/index.tsx +3 -4
  25. package/src/components/icon/meta.md +1 -2
  26. package/src/components/input/index.tsx +10 -2
  27. package/src/components/input-group/index.tsx +3 -3
  28. package/src/components/input-group/meta.md +15 -0
  29. package/src/components/input-group/stories.tsx +14 -0
  30. package/src/components/input-ip/index.tsx +1 -1
  31. package/src/components/input-number/index.tsx +5 -5
  32. package/src/components/item/meta.md +11 -11
  33. package/src/components/radio-group/index.tsx +1 -1
  34. package/src/components/rate/index.tsx +3 -3
  35. package/src/components/select/index.tsx +2 -2
  36. package/src/components/sidebar/index.tsx +4 -4
  37. package/src/components/skeleton/index.tsx +1 -1
  38. package/src/components/skeleton/meta.md +6 -6
  39. package/src/components/skeleton/stories.tsx +8 -8
  40. package/src/components/slider/index.tsx +27 -1
  41. package/src/components/sonner/index.tsx +43 -40
  42. package/src/components/sonner/meta.md +84 -68
  43. package/src/components/sonner/stories.tsx +122 -83
  44. package/src/components/spinner/index.tsx +170 -0
  45. package/src/components/spinner/meta.md +27 -1
  46. package/src/components/spinner/stories.tsx +23 -0
  47. package/src/components/steps/index.tsx +5 -1
  48. package/src/components/switch/index.tsx +1 -1
  49. package/src/components/tag/index.tsx +14 -0
  50. package/src/components/tag/meta.md +1 -0
  51. package/src/components/tag/stories.tsx +13 -0
  52. package/src/components/textarea/index.tsx +1 -1
  53. package/src/components/textarea/stories.tsx +1 -1
  54. package/src/components/time-picker/index.tsx +3 -1
  55. package/src/components/toggle/index.tsx +1 -1
  56. package/src/components/tooltip/index.tsx +5 -1
  57. package/src/components/tooltip/meta.md +13 -28
  58. package/src/components/tooltip/stories.tsx +11 -28
  59. package/src/components/tree-select/index.tsx +1 -1
@@ -123,19 +123,28 @@ export const Disabled: Story = {
123
123
  ),
124
124
  };
125
125
 
126
- /** 警告按钮用于危险操作的确认入口,可与 variant 组合使用。 */
126
+ /** 警告按钮用于危险操作的确认入口,可通过 className 与 variant 组合使用。 */
127
127
  export const Warning: Story = {
128
128
  render: () => (
129
129
  <div className="flex gap-2">
130
- <Button variant="destructive">Destructive</Button>
131
- <Button variant="outline" color="destructive">
132
- Outline
133
- </Button>
134
- <Button variant="ghost" color="destructive">
135
- Ghost
136
- </Button>
137
- <Button variant="link" color="destructive">
138
- 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
+ 危险按钮
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>OK</Button>
150
- <Button variant="secondary">Cancel</Button>
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']` | `'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
  };
@@ -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>
@@ -29,7 +29,7 @@ import {
29
29
  } from 'date-fns';
30
30
  import { zhCN } from 'date-fns/locale';
31
31
  import {
32
- Calendar as CalendarIcon,
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={value}
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-1 focus-within:ring-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-1 focus-within:ring-ring',
421
+ 'focus-within:border-ring focus-within:ring-ring/20',
422
422
  className,
423
423
  )}
424
424
  {...props}