@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
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import * as React from 'react';
3
- import { CircleHelpIcon } from 'lucide-react';
3
+ import { HelpFilledIcon } from '@/components/icon';
4
4
  import { Button } from '@/components/button';
5
5
  import { HelpToastIcon, Toaster, toast } from './index';
6
6
 
@@ -13,9 +13,8 @@ const meta: Meta<typeof Toaster> = {
13
13
  },
14
14
  decorators: [
15
15
  (Story) => (
16
- <div>
16
+ <div style={{ minHeight: 280 }}>
17
17
  <Story />
18
- <Toaster />
19
18
  </div>
20
19
  ),
21
20
  ],
@@ -26,6 +25,25 @@ type Story = StoryObj<typeof Toaster>;
26
25
 
27
26
  /** 五档语义状态 — 等价 cloud-design `Message.success / .error / .warning / .notice / .help / .loading`。 */
28
27
  export const Default: Story = {
28
+ parameters: {
29
+ docs: {
30
+ source: {
31
+ code: `import { Toaster, toast } from '@/components/sonner';
32
+
33
+ // 全局挂载一次 Toaster 容器
34
+ <Toaster />
35
+
36
+ // 通过 toast() 函数式触发
37
+ toast('默认通知');
38
+ toast.success('保存成功');
39
+ toast.info('已收到 3 条新消息');
40
+ toast.warning('请检查网络连接');
41
+ toast.error('操作失败,请重试');
42
+ toast.loading('正在加载...');
43
+ toast('需要帮助?', { icon: HelpToastIcon });`,
44
+ },
45
+ },
46
+ },
29
47
  render: () => (
30
48
  <div className="flex flex-wrap gap-2">
31
49
  <Button onClick={() => toast('默认通知')}>默认</Button>
@@ -44,6 +62,7 @@ export const Default: Story = {
44
62
  help
45
63
  </Button>
46
64
  <Button onClick={() => toast.loading('正在加载...')}>loading</Button>
65
+ <Toaster />
47
66
  </div>
48
67
  ),
49
68
  };
@@ -51,70 +70,82 @@ export const Default: Story = {
51
70
  /** 标题 + 描述 — 对应 cloud-design `Notification.open({ title, content })`。 */
52
71
  export const WithTitleAndDescription: Story = {
53
72
  render: () => (
54
- <Button
55
- onClick={() =>
56
- toast('文件已保存', {
57
- description: '你的修改已自动同步到云端,可在历史版本中找回。',
58
- })
59
- }
60
- >
61
- 触发
62
- </Button>
73
+ <>
74
+ <Button
75
+ onClick={() =>
76
+ toast('文件已保存', {
77
+ description: '你的修改已自动同步到云端,可在历史版本中找回。',
78
+ })
79
+ }
80
+ >
81
+ 触发
82
+ </Button>
83
+ <Toaster />
84
+ </>
63
85
  ),
64
86
  };
65
87
 
66
88
  /** 操作按钮 — 业内主流 `action` props,单击执行下一步行动并关闭。 */
67
89
  export const WithAction: Story = {
68
90
  render: () => (
69
- <Button
70
- onClick={() =>
71
- toast('已删除项目「示例」', {
72
- description: '删除后 7 天内可在回收站恢复。',
73
- action: {
74
- label: '撤销',
75
- onClick: () => toast.success('已撤销删除'),
76
- },
77
- })
78
- }
79
- >
80
- 触发
81
- </Button>
91
+ <>
92
+ <Button
93
+ onClick={() =>
94
+ toast('已删除项目「示例」', {
95
+ description: '删除后 7 天内可在回收站恢复。',
96
+ action: {
97
+ label: '撤销',
98
+ onClick: () => toast.success('已撤销删除'),
99
+ },
100
+ })
101
+ }
102
+ >
103
+ 触发
104
+ </Button>
105
+ <Toaster />
106
+ </>
82
107
  ),
83
108
  };
84
109
 
85
110
  /** 取消按钮 — 与 action 并存的次级行动点,可阻断默认行为。 */
86
111
  export const WithCancel: Story = {
87
112
  render: () => (
88
- <Button
89
- onClick={() =>
90
- toast('确认离开当前页面?', {
91
- description: '未保存的修改将丢失。',
92
- action: { label: '继续', onClick: () => toast.info('已继续') },
93
- cancel: { label: '取消', onClick: () => toast('已取消') },
94
- })
95
- }
96
- >
97
- 触发
98
- </Button>
113
+ <>
114
+ <Button
115
+ onClick={() =>
116
+ toast('确认离开当前页面?', {
117
+ description: '未保存的修改将丢失。',
118
+ action: { label: '继续', onClick: () => toast.info('已继续') },
119
+ cancel: { label: '取消', onClick: () => toast('已取消') },
120
+ })
121
+ }
122
+ >
123
+ 触发
124
+ </Button>
125
+ <Toaster />
126
+ </>
99
127
  ),
100
128
  };
101
129
 
102
130
  /** 按 id 链式更新 — 等价 cloud-design `Message.loading + Message.success({ key })`。 */
103
131
  export const LoadingThenResolve: Story = {
104
132
  render: () => (
105
- <Button
106
- onClick={() => {
107
- const id = toast.loading('正在上传...');
108
- setTimeout(() => {
109
- toast.success('上传完成', {
110
- id,
111
- description: '文件已发布到生产环境。',
112
- });
113
- }, 1500);
114
- }}
115
- >
116
- 触发
117
- </Button>
133
+ <>
134
+ <Button
135
+ onClick={() => {
136
+ const id = toast.loading('正在上传...');
137
+ setTimeout(() => {
138
+ toast.success('上传完成', {
139
+ id,
140
+ description: '文件已发布到生产环境。',
141
+ });
142
+ }, 1500);
143
+ }}
144
+ >
145
+ 触发
146
+ </Button>
147
+ <Toaster />
148
+ </>
118
149
  ),
119
150
  };
120
151
 
@@ -130,17 +161,20 @@ export const PromiseToast: Story = {
130
161
  });
131
162
 
132
163
  return (
133
- <Button
134
- onClick={() =>
135
- toast.promise(mockRequest(), {
136
- loading: '正在提交订单...',
137
- success: (data) => `${data.name} 已提交`,
138
- error: (err: Error) => `提交失败:${err.message}`,
139
- })
140
- }
141
- >
142
- 触发
143
- </Button>
164
+ <>
165
+ <Button
166
+ onClick={() =>
167
+ toast.promise(mockRequest(), {
168
+ loading: '正在提交订单...',
169
+ success: (data) => `${data.name} 已提交`,
170
+ error: (err: Error) => `提交失败:${err.message}`,
171
+ })
172
+ }
173
+ >
174
+ 触发
175
+ </Button>
176
+ <Toaster />
177
+ </>
144
178
  );
145
179
  },
146
180
  };
@@ -165,6 +199,7 @@ export const Duration: Story = {
165
199
  >
166
200
  持久 (Infinity)
167
201
  </Button>
202
+ <Toaster />
168
203
  </div>
169
204
  ),
170
205
  };
@@ -215,30 +250,33 @@ export const Placement: Story = {
215
250
  /** 自定义渲染 — `toast.custom` 完全接管节点,用于嵌入富内容卡片。 */
216
251
  export const Custom: Story = {
217
252
  render: () => (
218
- <Button
219
- onClick={() =>
220
- toast.custom((id) => (
221
- <div className="flex w-80 items-start gap-3 rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-lg">
222
- <CircleHelpIcon className="mt-0.5 size-4 shrink-0 text-help" />
223
- <div className="flex-1">
224
- <div className="text-sm font-medium">自定义卡片</div>
225
- <div className="mt-1 text-xs text-muted-foreground">
226
- 你可以在此渲染任意 JSX:表单、富文本、进度条等。
253
+ <>
254
+ <Button
255
+ onClick={() =>
256
+ toast.custom((id) => (
257
+ <div className="flex w-80 items-start gap-3 rounded-md border border-border bg-popover p-3 text-popover-foreground">
258
+ <HelpFilledIcon className="mt-0.5 size-4 shrink-0 text-help" />
259
+ <div className="flex-1">
260
+ <div className="text-sm font-medium">自定义卡片</div>
261
+ <div className="mt-1 text-xs text-muted-foreground">
262
+ 你可以在此渲染任意 JSX:表单、富文本、进度条等。
263
+ </div>
227
264
  </div>
265
+ <button
266
+ type="button"
267
+ className="cursor-pointer text-xs text-muted-foreground hover:text-foreground"
268
+ onClick={() => toast.dismiss(id)}
269
+ >
270
+ 关闭
271
+ </button>
228
272
  </div>
229
- <button
230
- type="button"
231
- className="cursor-pointer text-xs text-muted-foreground hover:text-foreground"
232
- onClick={() => toast.dismiss(id)}
233
- >
234
- 关闭
235
- </button>
236
- </div>
237
- ))
238
- }
239
- >
240
- 触发
241
- </Button>
273
+ ))
274
+ }
275
+ >
276
+ 触发
277
+ </Button>
278
+ <Toaster />
279
+ </>
242
280
  ),
243
281
  };
244
282
 
@@ -251,7 +289,7 @@ export const DismissProgrammatic: Story = {
251
289
  <Button
252
290
  onClick={() => {
253
291
  idRef.current = toast('一条可被定向关闭的通知', {
254
- description: '保留这条 id,再点"关闭它"按钮。',
292
+ description: '保留这条 id,再点“关闭它”按钮。',
255
293
  duration: Infinity,
256
294
  });
257
295
  }}
@@ -279,6 +317,7 @@ export const DismissProgrammatic: Story = {
279
317
  <Button variant="destructive" onClick={() => toast.dismiss()}>
280
318
  全部关闭
281
319
  </Button>
320
+ <Toaster />
282
321
  </div>
283
322
  );
284
323
  },
@@ -19,6 +19,174 @@ import { Loader2Icon } from 'lucide-react';
19
19
 
20
20
  import { cn } from '@/lib/utils';
21
21
 
22
+ /* ────────────────────────────────────────────────────────────────────────── */
23
+ /* SpinnerDots keyframes(singleton 注入) */
24
+ /* ────────────────────────────────────────────────────────────────────────── */
25
+
26
+ const SPINNER_DOTS_STYLE_ID = '__spinner-dots-keyframes__';
27
+
28
+ /**
29
+ * 确保 keyframes 在 DOM 中只注入一次。
30
+ * 双层动画架构(对齐 cloud-design fusion-reactor):
31
+ * - 外层容器:阶梯式旋转(快速转 90° → 停住),实现弧线位移
32
+ * - 每个点:径向脉动(边缘 → 中心 → 边缘),实现汇聚与发散
33
+ */
34
+ function ensureDotsKeyframes() {
35
+ if (typeof document === 'undefined') return;
36
+ if (document.getElementById(SPINNER_DOTS_STYLE_ID)) return;
37
+ const style = document.createElement('style');
38
+ style.id = SPINNER_DOTS_STYLE_ID;
39
+ style.textContent = `
40
+ @keyframes __sdots-orbit__ {
41
+ 0% { transform: rotate(0deg); }
42
+ 5% { transform: rotate(90deg); }
43
+ 25% { transform: rotate(90deg); }
44
+ 30% { transform: rotate(180deg); }
45
+ 50% { transform: rotate(180deg); }
46
+ 55% { transform: rotate(270deg); }
47
+ 75% { transform: rotate(270deg); }
48
+ 80% { transform: rotate(360deg); }
49
+ 100% { transform: rotate(360deg); }
50
+ }
51
+ @keyframes __sdot-x__ {
52
+ 25% { left: 0; }
53
+ 45%, 50% { left: calc(50% - 4px); }
54
+ 90% { left: 0; }
55
+ }
56
+ @keyframes __sdot-y__ {
57
+ 25% { top: 0; }
58
+ 45%, 50% { top: calc(50% - 4px); }
59
+ 90% { top: 0; }
60
+ }
61
+ @keyframes __sdot-xr__ {
62
+ 25% { right: 0; }
63
+ 45%, 50% { right: calc(50% - 4px); }
64
+ 90% { right: 0; }
65
+ }
66
+ @keyframes __sdot-yr__ {
67
+ 25% { bottom: 0; }
68
+ 45%, 50% { bottom: calc(50% - 4px); }
69
+ 90% { bottom: 0; }
70
+ }
71
+ `;
72
+ document.head.appendChild(style);
73
+ }
74
+
75
+ /* ────────────────────────────────────────────────────────────────────────── */
76
+ /* SpinnerDots(四点轨道位移原子) */
77
+ /* ────────────────────────────────────────────────────────────────────────── */
78
+
79
+ /** 容器尺寸(点固定 8×8px) */
80
+ const DOTS_SIZES = {
81
+ sm: { container: 'size-6' },
82
+ md: { container: 'size-7' },
83
+ lg: { container: 'size-8' },
84
+ } as const;
85
+
86
+ export interface SpinnerDotsProps
87
+ extends Omit<React.ComponentProps<'span'>, 'children'> {
88
+ /** 三档尺寸,控制点的扩散半径 */
89
+ size?: 'sm' | 'md' | 'lg';
90
+ /** sr-only 读屏文本 */
91
+ label?: string;
92
+ }
93
+
94
+ /**
95
+ * 四点轨道位移旋转指示器(对齐 cloud-design fusion-reactor 动效)。
96
+ * 4 个主题色(primary)圆点 8×8px,透明度 1 / 0.8 / 0.6 / 0.2,分居左上右下。
97
+ * 双层动画:外层容器阶梯旋转(弧线位移)+ 每个点径向脉动(汇聚/发散)。
98
+ */
99
+ function SpinnerDots({
100
+ size = 'md',
101
+ label = '加载中',
102
+ className,
103
+ ...props
104
+ }: SpinnerDotsProps) {
105
+ React.useEffect(() => {
106
+ ensureDotsKeyframes();
107
+ }, []);
108
+
109
+ const sizeToken = DOTS_SIZES[size ?? 'md'];
110
+
111
+ const dotBase: React.CSSProperties = {
112
+ position: 'absolute',
113
+ width: '8px',
114
+ height: '8px',
115
+ borderRadius: '50%',
116
+ background: 'var(--color-primary)',
117
+ margin: 'auto',
118
+ animationDuration: '1.4s',
119
+ animationTimingFunction: 'ease-in-out',
120
+ animationIterationCount: 'infinite',
121
+ };
122
+
123
+ return (
124
+ <span
125
+ data-slot="spinner-dots-root"
126
+ role="status"
127
+ aria-live="polite"
128
+ className={cn(
129
+ 'inline-flex shrink-0 items-center justify-center',
130
+ className,
131
+ )}
132
+ {...props}
133
+ >
134
+ {/* 外层容器:阶梯旋转(5.6s = 4 × 1.4s) */}
135
+ <span
136
+ data-slot="spinner-dots"
137
+ className={cn('relative inline-block', sizeToken.container)}
138
+ style={{ animation: '__sdots-orbit__ 5.6s linear infinite' }}
139
+ >
140
+ {/* 左 · opacity 1 — 径向向右脉动 (nextVectorDotsX) */}
141
+ <span
142
+ style={{
143
+ ...dotBase,
144
+ top: 0,
145
+ bottom: 0,
146
+ left: 0,
147
+ opacity: 1,
148
+ animationName: '__sdot-x__',
149
+ }}
150
+ />
151
+ {/* 上 · opacity 0.8 — 径向向下脉动 (nextVectorDotsY) */}
152
+ <span
153
+ style={{
154
+ ...dotBase,
155
+ left: 0,
156
+ right: 0,
157
+ top: 0,
158
+ opacity: 0.8,
159
+ animationName: '__sdot-y__',
160
+ }}
161
+ />
162
+ {/* 右 · opacity 0.6 — 径向向左脉动 (nextVectorDotsXR) */}
163
+ <span
164
+ style={{
165
+ ...dotBase,
166
+ top: 0,
167
+ bottom: 0,
168
+ right: 0,
169
+ opacity: 0.6,
170
+ animationName: '__sdot-xr__',
171
+ }}
172
+ />
173
+ {/* 下 · opacity 0.2 — 径向向上脉动 (nextVectorDotsYR) */}
174
+ <span
175
+ style={{
176
+ ...dotBase,
177
+ left: 0,
178
+ right: 0,
179
+ bottom: 0,
180
+ opacity: 0.2,
181
+ animationName: '__sdot-yr__',
182
+ }}
183
+ />
184
+ </span>
185
+ {label ? <span className="sr-only">{label}</span> : null}
186
+ </span>
187
+ );
188
+ }
189
+
22
190
  /* ────────────────────────────────────────────────────────────────────────── */
23
191
  /* Spinner(原子) */
24
192
  /* ────────────────────────────────────────────────────────────────────────── */
@@ -72,6 +240,7 @@ function Spinner({
72
240
  data-size={size}
73
241
  data-tone={tone ?? 'default'}
74
242
  aria-hidden="true"
243
+ strokeWidth={1.5}
75
244
  className={cn(spinnerVariants({ size, tone }), className)}
76
245
  {...props}
77
246
  />
@@ -327,6 +496,7 @@ function SpinnerFullscreen({
327
496
 
328
497
  export {
329
498
  Spinner,
499
+ SpinnerDots,
330
500
  SpinnerTip,
331
501
  SpinnerOverlay,
332
502
  SpinnerOverlayMask,
@@ -1,4 +1,4 @@
1
- # Spinner
1
+ # SpinnerDots
2
2
 
3
3
  > 本文件由 `pnpm gen:meta` 自动生成,**请勿手动编辑**。
4
4
 
@@ -19,6 +19,8 @@ shadcn v4 composable 套件:原子 Spinner + SpinnerTip 文案 + SpinnerOverla
19
19
 
20
20
  | 名称 | 类型 | 默认值 | 必填 | 说明 |
21
21
  | --- | --- | --- | --- | --- |
22
+ | `size` | `'sm' \| 'md' \| 'lg'` | – | – | 三档尺寸,控制点的扩散半径 |
23
+ | `label` | `string` | – | – | sr-only 读屏文本 |
22
24
  | `size` | `"xs" \| "sm" \| "md" \| "lg"` | – | – | – |
23
25
  | `tone` | `"default" \| "primary" \| "inverse" \| "muted"` | `"default"` | – | – |
24
26
  | `label` | `string` | – | – | sr-only 读屏文本,默认 `加载中`;如父级已有可见 tip 可置空 |
@@ -146,6 +148,30 @@ tip 位置:right(行内) 与 bottom(堆叠)。
146
148
  </SpinnerOverlay>
147
149
  ```
148
150
 
151
+ ### DotsDefault
152
+
153
+ SpinnerDots 三档尺寸:四点沿弧线轨道位移动画。
154
+
155
+ ```tsx
156
+ <div className="flex items-center gap-6">
157
+ <SpinnerDots size="sm" />
158
+ <SpinnerDots size="md" />
159
+ <SpinnerDots size="lg" />
160
+ </div>
161
+ ```
162
+
163
+ ### OverlayWithDots
164
+
165
+ SpinnerDots 作为 SpinnerOverlay 的自定义 indicator。
166
+
167
+ ```tsx
168
+ <SpinnerOverlay indicator={<SpinnerDots size="md" />} tip="加载中...">
169
+ <div className="h-20 w-48 rounded border border-border p-4 text-xs">
170
+ 四点轨道动画
171
+ </div>
172
+ </SpinnerOverlay>
173
+ ```
174
+
149
175
  ### InlineVsBlock
150
176
 
151
177
  inline(默认)与 block 通栏模式对比。
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import * as React from 'react';
3
3
  import {
4
4
  Spinner,
5
+ SpinnerDots,
5
6
  SpinnerTip,
6
7
  SpinnerOverlay,
7
8
  SpinnerFullscreen,
@@ -172,6 +173,28 @@ export const Fullscreen: Story = {
172
173
  },
173
174
  };
174
175
 
176
+ /** SpinnerDots 三档尺寸:四点沿弧线轨道位移动画。 */
177
+ export const DotsDefault: Story = {
178
+ render: () => (
179
+ <div className="flex items-center gap-6">
180
+ <SpinnerDots size="sm" />
181
+ <SpinnerDots size="md" />
182
+ <SpinnerDots size="lg" />
183
+ </div>
184
+ ),
185
+ };
186
+
187
+ /** SpinnerDots 作为 SpinnerOverlay 的自定义 indicator。 */
188
+ export const OverlayWithDots: Story = {
189
+ render: () => (
190
+ <SpinnerOverlay indicator={<SpinnerDots size="md" />} tip="加载中...">
191
+ <div className="h-20 w-48 rounded border border-border p-4 text-xs">
192
+ 四点轨道动画
193
+ </div>
194
+ </SpinnerOverlay>
195
+ ),
196
+ };
197
+
175
198
  /** inline(默认)与 block 通栏模式对比。 */
176
199
  export const InlineVsBlock: Story = {
177
200
  render: () => (
@@ -135,6 +135,7 @@ const ArrowSteps = React.forwardRef<
135
135
  <div
136
136
  ref={ref}
137
137
  data-slot="steps"
138
+ data-shape="arrow"
138
139
  className={cn('flex flex-row', className)}
139
140
  {...props}
140
141
  >
@@ -150,6 +151,7 @@ const ArrowSteps = React.forwardRef<
150
151
  <button
151
152
  key={i}
152
153
  type="button"
154
+ data-status={s}
153
155
  tabIndex={clickable ? 0 : -1}
154
156
  onClick={clickable ? () => onChange!(i) : undefined}
155
157
  aria-disabled={isDisabled || undefined}
@@ -259,6 +261,7 @@ function CircleDotSteps({
259
261
  <div
260
262
  ref={forwardedRef}
261
263
  data-slot="steps"
264
+ data-shape={shape}
262
265
  className={cn(
263
266
  'flex',
264
267
  isVertical ? 'flex-col gap-0' : 'flex-row items-start gap-0',
@@ -286,7 +289,7 @@ function CircleDotSteps({
286
289
  ? null
287
290
  : item.icon ??
288
291
  (s === 'finish' ? (
289
- <Check className={iconSize} />
292
+ <Check className={iconSize} strokeWidth={3} />
290
293
  ) : s === 'error' ? (
291
294
  <X className={iconSize} />
292
295
  ) : (
@@ -296,6 +299,7 @@ function CircleDotSteps({
296
299
  const indicatorBtn = (
297
300
  <button
298
301
  type="button"
302
+ data-status={s}
299
303
  tabIndex={clickable ? 0 : -1}
300
304
  onClick={clickable ? () => onChange!(i) : undefined}
301
305
  className={cn(
@@ -23,7 +23,7 @@ import { cn } from '@/lib/utils';
23
23
  // ─── cva ────────────────────────────────────────────────────────────────────
24
24
 
25
25
  const switchVariants = cva(
26
- 'peer group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[loading=true]:pointer-events-none data-[loading=true]:opacity-70',
26
+ 'peer group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-ring/20 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[loading=true]:pointer-events-none data-[loading=true]:opacity-70',
27
27
  {
28
28
  variants: {
29
29
  size: {
@@ -176,11 +176,15 @@ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
176
176
  // ─── TableBody ──────────────────────────────────────────────────────────────
177
177
 
178
178
  function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
179
- const { striped } = useTableContext();
179
+ const { striped, bordered } = useTableContext();
180
180
  return (
181
181
  <tbody
182
182
  data-slot="table-body"
183
- className={cn(striped && '[&_tr:nth-child(even)]:bg-muted/30', className)}
183
+ className={cn(
184
+ striped && '[&_tr:nth-child(even)]:bg-muted/30',
185
+ bordered && '[&>tr:last-child]:border-b-0',
186
+ className,
187
+ )}
184
188
  {...props}
185
189
  />
186
190
  );