@teamix-evo/ui 0.5.2 → 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 (59) hide show
  1. package/package.json +19 -19
  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/skeleton/index.tsx +1 -1
  37. package/src/components/skeleton/meta.md +6 -6
  38. package/src/components/skeleton/stories.tsx +8 -8
  39. package/src/components/slider/index.tsx +27 -1
  40. package/src/components/sonner/index.tsx +43 -40
  41. package/src/components/sonner/meta.md +84 -68
  42. package/src/components/sonner/stories.tsx +122 -83
  43. package/src/components/spinner/index.tsx +170 -0
  44. package/src/components/spinner/meta.md +27 -1
  45. package/src/components/spinner/stories.tsx +23 -0
  46. package/src/components/steps/index.tsx +5 -1
  47. package/src/components/switch/index.tsx +1 -1
  48. package/src/components/tag/index.tsx +14 -0
  49. package/src/components/tag/meta.md +1 -0
  50. package/src/components/tag/stories.tsx +13 -0
  51. package/src/components/textarea/index.tsx +1 -1
  52. package/src/components/textarea/stories.tsx +1 -1
  53. package/src/components/time-picker/index.tsx +3 -1
  54. package/src/components/toggle/index.tsx +1 -1
  55. package/src/components/tooltip/index.tsx +5 -1
  56. package/src/components/tooltip/meta.md +13 -28
  57. package/src/components/tooltip/stories.tsx +11 -28
  58. package/src/components/tree-select/index.tsx +1 -1
  59. package/LICENSE +0 -21
@@ -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}
@@ -35,16 +35,8 @@
35
35
 
36
36
  ```tsx
37
37
  <DemoContainer>
38
- <FloatButton
39
- icon={<MessageCircleIcon />}
40
- aria-label="客服"
41
- badge="3"
42
- />
43
- <FloatButton
44
- variant="default"
45
- icon={<PlusIcon />}
46
- aria-label="新建"
47
- />
38
+ <FloatButton icon={<MessageCircleIcon />} aria-label="客服" badge="3" />
39
+ <FloatButton variant="default" icon={<PlusIcon />} aria-label="新建" />
48
40
  </DemoContainer>
49
41
  ```
50
42
 
@@ -54,11 +46,7 @@
54
46
 
55
47
  ```tsx
56
48
  <DemoContainer>
57
- <FloatButton
58
- shape="square"
59
- icon={<PlusIcon />}
60
- aria-label="新建"
61
- />
49
+ <FloatButton shape="square" icon={<PlusIcon />} aria-label="新建" />
62
50
  </DemoContainer>
63
51
  ```
64
52
 
@@ -20,8 +20,7 @@
20
20
  * <span className="text-success"><SuccessFilledIcon /></span>
21
21
  * ```
22
22
  *
23
- * 尺寸:默认 `size-5`(20px),通过 `className` 覆盖(如 `size-3.5` / `size-4`)。
24
- * viewBox 采用 `-1 -1 22 22`,使图形在容器内自带 1px 内边距留白。
23
+ * 尺寸:默认 `size-4`(16px),通过 `className` 覆盖(如 `size-3.5` / `size-5`)。
25
24
  */
26
25
  import * as React from 'react';
27
26
 
@@ -37,9 +36,9 @@ function BaseFilledIcon({
37
36
  return (
38
37
  <svg
39
38
  data-slot="icon"
40
- viewBox="-1 -1 22 22"
39
+ viewBox="0 0 20 20"
41
40
  aria-hidden="true"
42
- className={cn('size-5 shrink-0', className)}
41
+ className={cn('size-4 shrink-0', className)}
43
42
  {...props}
44
43
  >
45
44
  {children}
@@ -22,8 +22,7 @@
22
22
  - ```tsx
23
23
  - <span className="text-success"><SuccessFilledIcon /></span>
24
24
  - ```
25
- - 尺寸:默认 `size-5`(20px),通过 `className` 覆盖(如 `size-3.5` / `size-4`)。
26
- - viewBox 采用 `-1 -1 22 22`,使图形在容器内自带 1px 内边距留白。
25
+ - 尺寸:默认 `size-4`(16px),通过 `className` 覆盖(如 `size-3.5` / `size-5`)。
27
26
 
28
27
  ## 示例
29
28
 
@@ -46,11 +46,19 @@ export interface InputProps
46
46
  // ─── Input ──────────────────────────────────────────────────────────────────
47
47
 
48
48
  const Input = React.forwardRef<HTMLInputElement, InputProps>(
49
- ({ className, type, size, ...props }, ref) => {
49
+ ({ className, type, size, role: roleProp, ...props }, ref) => {
50
+ // <input type="search"> 会被浏览器渲染原生 clear / 放大镜 / IE Reveal 按钮,
51
+ // 与 Input 自带的 `clearable` / `suffix={<Search/>}` 视觉冲突。组件层透明屏蔽:
52
+ // 渲染为 type="text" + role="searchbox",保留无障碍语义,免除业务侧 globals.css
53
+ // 补 webkit/ms 伪元素 reset(原 ADR 0023 §4 “手动加 CSS” 路径)。
54
+ const isSearch = type === 'search';
55
+ const safeType = isSearch ? 'text' : type;
56
+ const role = isSearch ? roleProp ?? 'searchbox' : roleProp;
50
57
  return (
51
58
  <input
52
59
  ref={ref}
53
- type={type}
60
+ type={safeType}
61
+ role={role}
54
62
  data-slot="input"
55
63
  className={cn(inputVariants({ size, className }))}
56
64
  {...props}
@@ -32,7 +32,7 @@ const InputGroup = React.forwardRef<
32
32
  data-slot="input-group"
33
33
  role="group"
34
34
  className={cn(
35
- 'group/input-group relative flex h-8 w-full min-w-0 items-center rounded-md border border-input transition-colors outline-none has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
35
+ 'group/input-group relative flex h-8 w-full min-w-0 items-center rounded-md border border-input transition-colors outline-none has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/20 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
36
36
  className,
37
37
  )}
38
38
  {...props}
@@ -136,7 +136,7 @@ function InputGroupInput({ className, ...props }: InputProps) {
136
136
  <Input
137
137
  data-slot="input-group-control"
138
138
  className={cn(
139
- 'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0',
139
+ 'h-full flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0',
140
140
  className,
141
141
  )}
142
142
  {...props}
@@ -278,7 +278,7 @@ function InputGroupShowCount({
278
278
  data-slot="input-group-show-count"
279
279
  data-overflow={overflow || undefined}
280
280
  className={cn(
281
- 'flex items-center text-xs tabular-nums text-muted-foreground select-none',
281
+ 'flex items-center text-xs tabular-nums text-muted-foreground/60 select-none',
282
282
  'data-[overflow]:text-destructive',
283
283
  className,
284
284
  )}
@@ -44,6 +44,21 @@
44
44
  </InputGroup>
45
45
  ```
46
46
 
47
+ ### WithTrailingIcon
48
+
49
+ 后置图标插槽:搜索图标在右侧。
50
+
51
+ ```tsx
52
+ <InputGroup className="w-64">
53
+ <InputGroupInput placeholder="搜索..." />
54
+ <InputGroupAddon align="inline-end">
55
+ <InputGroupText>
56
+ <SearchIcon />
57
+ </InputGroupText>
58
+ </InputGroupAddon>
59
+ </InputGroup>
60
+ ```
61
+
47
62
  ### Email
48
63
 
49
64
  邮箱输入:左侧邮件图标 + 右侧域名后缀文本。
@@ -44,6 +44,20 @@ export const WithLeadingIcon: Story = {
44
44
  ),
45
45
  };
46
46
 
47
+ /** 后置图标插槽:搜索图标在右侧。 */
48
+ export const WithTrailingIcon: Story = {
49
+ render: () => (
50
+ <InputGroup className="w-64">
51
+ <InputGroupInput placeholder="搜索..." />
52
+ <InputGroupAddon align="inline-end">
53
+ <InputGroupText>
54
+ <SearchIcon />
55
+ </InputGroupText>
56
+ </InputGroupAddon>
57
+ </InputGroup>
58
+ ),
59
+ };
60
+
47
61
  /**
48
62
  * 搜索场景:左侧 SearchIcon + 右侧 InputGroupClear 受控清除按钮。
49
63
  * 仅当 value 非空时显示清除按钮。
@@ -27,7 +27,7 @@ const inputIpVariants = cva(
27
27
  // `w-fit` 显式锁定“宽度 = 内容”,避免被 flex/grid 父容器 cross-axis stretch 拉满。
28
28
  // 消费方需要铺满父容器时,可通过 className="w-full" 覆盖。
29
29
  'inline-flex w-fit items-center rounded-md border border-input bg-transparent transition-colors',
30
- 'focus-within:border-ring focus-within:ring-1 focus-within:ring-ring',
30
+ 'focus-within:border-ring focus-within:ring-ring/20',
31
31
  'aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-1 aria-[invalid=true]:ring-destructive/20',
32
32
  ].join(' '),
33
33
  {
@@ -21,8 +21,8 @@ import { cn } from '@/lib/utils';
21
21
 
22
22
  const inputNumberVariants = cva(
23
23
  [
24
- 'inline-flex w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-transparent transition-colors',
25
- 'focus-within:border-ring focus-within:ring-1 focus-within:ring-ring',
24
+ 'group/input-number inline-flex w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-transparent transition-colors',
25
+ 'focus-within:border-ring focus-within:ring-ring/20',
26
26
  'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:bg-input/50 has-[input:disabled]:opacity-50',
27
27
  'has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-1 has-[input[aria-invalid=true]]:ring-destructive/20',
28
28
  ].join(' '),
@@ -253,7 +253,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
253
253
  };
254
254
 
255
255
  const stepBtnClass =
256
- 'flex cursor-pointer items-center justify-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-muted-foreground';
256
+ 'flex cursor-pointer items-center justify-center text-muted-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent';
257
257
 
258
258
  const upDisabled =
259
259
  disabled ||
@@ -325,7 +325,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
325
325
  {controls && variant === 'default' ? (
326
326
  <div
327
327
  data-slot="input-number-handlers"
328
- className="flex flex-col border-l border-input"
328
+ className="flex flex-col divide-y divide-input border-l border-input opacity-0 transition-opacity group-hover/input-number:opacity-100 group-focus-within/input-number:opacity-100"
329
329
  >
330
330
  <button
331
331
  type="button"
@@ -345,7 +345,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
345
345
  onClick={() => stepBy(-1)}
346
346
  aria-label="减少"
347
347
  data-slot="input-number-decrement"
348
- className={cn(stepBtnClass, 'h-1/2 border-t border-input px-1.5')}
348
+ className={cn(stepBtnClass, 'h-1/2 px-1.5')}
349
349
  >
350
350
  <ChevronDown className="size-3.5" />
351
351
  </button>
@@ -16,20 +16,20 @@
16
16
  - 图文卡片(horizontal 布局)
17
17
  - 通知/消息列表
18
18
  - 带操作的资源列表
19
- - 组合结构:ItemGroup [bordered] [divider] > ItemGroupHeader + Item\* > ItemMedia + ItemContent (> ItemTitle + ItemDescription) + ItemExtra + ItemActions [separated] > ItemGroupFooter
19
+ - 组合结构:ItemGroup [bordered] [divider] > ItemGroupHeader + Item* > ItemMedia + ItemContent (> ItemTitle + ItemDescription) + ItemExtra + ItemActions [separated] > ItemGroupFooter
20
20
 
21
21
  ## Props
22
22
 
23
- | 名称 | 类型 | 默认值 | 必填 | 说明 |
24
- | ----------- | ------------------------------------------- | ------------ | ---- | ---------------------------- |
25
- | `bordered` | `"true" \| "false"` | – | – | – |
26
- | `divider` | `"true" \| "false"` | – | – | – |
27
- | `variant` | `"default" \| "outline" \| "muted"` | `"default"` | – | – |
28
- | `size` | `"default" \| "sm" \| "xs"` | `"default"` | – | – |
29
- | `layout` | `"vertical" \| "horizontal"` | `"vertical"` | – | – |
30
- | `asChild` | `boolean` | – | – | 使用 Slot 模式渲染子元素 |
31
- | `variant` | `"default" \| "icon" \| "image" \| "cover"` | `"default"` | – | – |
32
- | `separated` | `boolean` | – | – | 子元素之间自动插入竖向分隔线 |
23
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
24
+ | --- | --- | --- | --- | --- |
25
+ | `bordered` | `"true" \| "false"` | – | – | – |
26
+ | `divider` | `"true" \| "false"` | – | – | – |
27
+ | `variant` | `"default" \| "outline" \| "muted"` | `"default"` | – | – |
28
+ | `size` | `"default" \| "sm" \| "xs"` | `"default"` | – | – |
29
+ | `layout` | `"vertical" \| "horizontal"` | `"vertical"` | – | – |
30
+ | `asChild` | `boolean` | – | – | 使用 Slot 模式渲染子元素 |
31
+ | `variant` | `"default" \| "icon" \| "image" \| "cover"` | `"default"` | – | – |
32
+ | `separated` | `boolean` | – | – | 子元素之间自动插入竖向分隔线 |
33
33
 
34
34
  ## 示例
35
35
 
@@ -60,7 +60,7 @@ function RadioGroupItem({ className, ...props }: RadioGroupItemProps) {
60
60
  <RadioGroupPrimitive.Item
61
61
  data-slot="radio-group-item"
62
62
  className={cn(
63
- 'peer relative flex aspect-square size-4 shrink-0 cursor-pointer items-center justify-center rounded-full border border-input shadow-xs transition-[color,box-shadow] outline-none 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]:text-primary',
63
+ 'peer relative flex aspect-square size-4 shrink-0 cursor-pointer items-center justify-center rounded-full border border-input shadow-xs transition-[color,box-shadow] outline-none 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]:text-primary',
64
64
  className,
65
65
  )}
66
66
  {...props}
@@ -232,7 +232,7 @@ function Rate({
232
232
  rateItemVariants({ size }),
233
233
  'inline-flex items-center justify-center',
234
234
  state === 'empty' && 'text-border',
235
- state !== 'empty' && 'text-warning',
235
+ state !== 'empty' && 'text-primary',
236
236
  )}
237
237
  >
238
238
  {state === 'half' ? (
@@ -241,7 +241,7 @@ function Rate({
241
241
  {character}
242
242
  </span>
243
243
  <span
244
- className="absolute inset-0 inline-flex items-center justify-center text-warning"
244
+ className="absolute inset-0 inline-flex items-center justify-center text-primary"
245
245
  style={{ clipPath: 'inset(0 50% 0 0)' }}
246
246
  >
247
247
  {character}
@@ -261,7 +261,7 @@ function Rate({
261
261
  {Icon}
262
262
  {state !== 'empty' && (
263
263
  <span
264
- className={cn('absolute inset-0 inline-block text-warning')}
264
+ className={cn('absolute inset-0 inline-block text-primary')}
265
265
  style={
266
266
  state === 'half'
267
267
  ? { clipPath: 'inset(0 50% 0 0)' }
@@ -112,7 +112,7 @@ function SelectTrigger({
112
112
  data-size={size}
113
113
  data-loading={loading ? 'true' : undefined}
114
114
  className={cn(
115
- "flex w-fit cursor-pointer items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs whitespace-nowrap transition-colors outline-none select-none 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-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=lg]:h-9 data-[size=lg]:text-sm *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
115
+ "flex w-fit cursor-pointer items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs whitespace-nowrap transition-colors outline-none select-none focus:border-ring focus:ring-ring/20 data-[state=open]:border-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=lg]:h-9 data-[size=lg]:text-sm *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
116
116
  className,
117
117
  )}
118
118
  {...props}
@@ -198,7 +198,7 @@ function SelectContent({
198
198
  className={cn(
199
199
  // 面板高度上限 `max-h-80`(20rem ≈ 10 个选项),避免选项多时面板过长;
200
200
  // 配合 `overflow-y-auto` 使用浏览器原生滚动条,不再渲染 ScrollUp/Down 按钮。
201
- 'relative z-50 max-h-80 min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
201
+ 'relative z-50 max-h-80 min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
202
202
  position === 'popper' &&
203
203
  'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
204
204
  className,
@@ -204,7 +204,7 @@ function SkeletonImage({
204
204
  className={cn(skeletonImageVariants({ size, block }), className)}
205
205
  {...props}
206
206
  >
207
- {icon ?? <ImageIcon aria-hidden="true" />}
207
+ {icon ?? <ImageIcon strokeWidth={1.5} aria-hidden="true" />}
208
208
  </Skeleton>
209
209
  );
210
210
  }
@@ -52,19 +52,19 @@ SkeletonInput / SkeletonTitle / SkeletonParagraph(独立子家族,可单独
52
52
  ```tsx
53
53
  <div className="flex flex-col gap-3">
54
54
  <div className="flex items-center gap-3">
55
- <span className="w-24 text-muted-foreground">text</span>
55
+ <span className="w-24 text-xs text-muted-foreground">text</span>
56
56
  <Skeleton variant="text" className="max-w-64" />
57
57
  </div>
58
58
  <div className="flex items-center gap-3">
59
- <span className="w-24 text-muted-foreground">rectangular</span>
59
+ <span className="w-24 text-xs text-muted-foreground">rectangular</span>
60
60
  <Skeleton variant="rectangular" className="h-4 w-64" />
61
61
  </div>
62
62
  <div className="flex items-center gap-3">
63
- <span className="w-24 text-muted-foreground">rounded</span>
63
+ <span className="w-24 text-xs text-muted-foreground">rounded</span>
64
64
  <Skeleton variant="rounded" className="h-4 w-64" />
65
65
  </div>
66
66
  <div className="flex items-center gap-3">
67
- <span className="w-24 text-muted-foreground">circular</span>
67
+ <span className="w-24 text-xs text-muted-foreground">circular</span>
68
68
  <Skeleton variant="circular" className="size-10" />
69
69
  </div>
70
70
  </div>
@@ -77,11 +77,11 @@ pulse / none 两种动画对照。
77
77
  ```tsx
78
78
  <div className="flex flex-col gap-3">
79
79
  <div className="flex items-center gap-3">
80
- <span className="w-16 text-muted-foreground">pulse</span>
80
+ <span className="w-16 text-xs text-muted-foreground">pulse</span>
81
81
  <Skeleton animation="pulse" className="h-4 w-64" />
82
82
  </div>
83
83
  <div className="flex items-center gap-3">
84
- <span className="w-16 text-muted-foreground">none</span>
84
+ <span className="w-16 text-xs text-muted-foreground">none</span>
85
85
  <Skeleton animation="none" className="h-4 w-64" />
86
86
  </div>
87
87
  </div>
@@ -31,19 +31,19 @@ export const Variants: Story = {
31
31
  render: () => (
32
32
  <div className="flex flex-col gap-3">
33
33
  <div className="flex items-center gap-3">
34
- <span className="w-24 text-muted-foreground">text</span>
34
+ <span className="w-24 text-xs text-muted-foreground">text</span>
35
35
  <Skeleton variant="text" className="max-w-64" />
36
36
  </div>
37
37
  <div className="flex items-center gap-3">
38
- <span className="w-24 text-muted-foreground">rectangular</span>
38
+ <span className="w-24 text-xs text-muted-foreground">rectangular</span>
39
39
  <Skeleton variant="rectangular" className="h-4 w-64" />
40
40
  </div>
41
41
  <div className="flex items-center gap-3">
42
- <span className="w-24 text-muted-foreground">rounded</span>
42
+ <span className="w-24 text-xs text-muted-foreground">rounded</span>
43
43
  <Skeleton variant="rounded" className="h-4 w-64" />
44
44
  </div>
45
45
  <div className="flex items-center gap-3">
46
- <span className="w-24 text-muted-foreground">circular</span>
46
+ <span className="w-24 text-xs text-muted-foreground">circular</span>
47
47
  <Skeleton variant="circular" className="size-10" />
48
48
  </div>
49
49
  </div>
@@ -55,11 +55,11 @@ export const Animations: Story = {
55
55
  render: () => (
56
56
  <div className="flex flex-col gap-3">
57
57
  <div className="flex items-center gap-3">
58
- <span className="w-16 text-muted-foreground">pulse</span>
58
+ <span className="w-16 text-xs text-muted-foreground">pulse</span>
59
59
  <Skeleton animation="pulse" className="h-4 w-64" />
60
60
  </div>
61
61
  <div className="flex items-center gap-3">
62
- <span className="w-16 text-muted-foreground">none</span>
62
+ <span className="w-16 text-xs text-muted-foreground">none</span>
63
63
  <Skeleton animation="none" className="h-4 w-64" />
64
64
  </div>
65
65
  </div>
@@ -84,8 +84,8 @@ export const Loading: Story = {
84
84
  return (
85
85
  <div className="flex flex-col gap-3">
86
86
  <Button onClick={() => setLoading((v) => !v)}>切换</Button>
87
- <Skeleton loading={loading} className="h-4 w-64">
88
- <span className="text-foreground">真实内容已加载</span>
87
+ <Skeleton loading={loading} className="h-8 w-64">
88
+ <span className="text-xs text-foreground">真实内容已加载</span>
89
89
  </Skeleton>
90
90
  </div>
91
91
  );
@@ -78,8 +78,9 @@ function Slider({
78
78
  showTooltip = 'auto',
79
79
  tooltipFormatter,
80
80
  reverse = false,
81
+ onValueChange,
81
82
  ...props
82
- }: SliderProps) {
83
+ }: SliderProps & { onValueChange?: (value: number[]) => void }) {
83
84
  const _values = React.useMemo(
84
85
  () =>
85
86
  Array.isArray(value)
@@ -90,6 +91,29 @@ function Slider({
90
91
  [value, defaultValue, min, max],
91
92
  );
92
93
 
94
+ // Track current slider values for mark in-range detection
95
+ const [currentValues, setCurrentValues] = React.useState(_values);
96
+ React.useEffect(() => {
97
+ if (value) setCurrentValues(Array.isArray(value) ? value : [value]);
98
+ }, [value]);
99
+
100
+ const handleValueChange = React.useCallback(
101
+ (vals: number[]) => {
102
+ setCurrentValues(vals);
103
+ onValueChange?.(vals);
104
+ },
105
+ [onValueChange],
106
+ );
107
+
108
+ const isInRange = (markValue: number) => {
109
+ const first = currentValues[0] ?? min;
110
+ if (currentValues.length === 1) {
111
+ return reverse ? markValue >= first : markValue <= first;
112
+ }
113
+ const last = currentValues[currentValues.length - 1] ?? max;
114
+ return markValue >= first && markValue <= last;
115
+ };
116
+
93
117
  const parsedMarks = React.useMemo(
94
118
  () => parseMarks(marks, min, max),
95
119
  [marks, min, max],
@@ -178,6 +202,7 @@ function Slider({
178
202
  min={min}
179
203
  max={max}
180
204
  orientation={orientation}
205
+ onValueChange={handleValueChange}
181
206
  className={cn(
182
207
  'group/slider relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
183
208
  className,
@@ -196,6 +221,7 @@ function Slider({
196
221
  <span
197
222
  key={`mark-${m.value}`}
198
223
  data-slot="slider-mark"
224
+ data-in-range={isInRange(m.value) || undefined}
199
225
  aria-hidden="true"
200
226
  style={markDotStyle(m.value)}
201
227
  className="pointer-events-none absolute size-1 rounded-full bg-border"