@teamix-evo/ui 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/alert/index.tsx +1 -1
- package/src/components/alert-dialog/index.tsx +17 -24
- package/src/components/alert-dialog/meta.md +102 -8
- package/src/components/alert-dialog/stories.tsx +117 -7
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/badge/index.tsx +1 -1
- package/src/components/button/index.tsx +1 -29
- package/src/components/button/meta.md +24 -13
- package/src/components/button/stories.tsx +21 -12
- package/src/components/button-group/meta.md +6 -9
- package/src/components/button-group/stories.tsx +2 -6
- package/src/components/calendar/index.tsx +12 -7
- package/src/components/cascader-select/index.tsx +1 -1
- package/src/components/checkbox/index.tsx +1 -1
- package/src/components/combobox/index.tsx +54 -10
- package/src/components/combobox/meta.md +3 -5
- package/src/components/combobox/stories.tsx +104 -25
- package/src/components/data-table/stories.tsx +4 -1
- package/src/components/date-picker/index.tsx +25 -2
- package/src/components/field/index.tsx +1 -1
- package/src/components/filter-bar/index.tsx +1 -1
- package/src/components/float-button/meta.md +3 -15
- package/src/components/icon/index.tsx +3 -4
- package/src/components/icon/meta.md +1 -2
- package/src/components/input/index.tsx +10 -2
- package/src/components/input-group/index.tsx +3 -3
- package/src/components/input-group/meta.md +15 -0
- package/src/components/input-group/stories.tsx +14 -0
- package/src/components/input-ip/index.tsx +1 -1
- package/src/components/input-number/index.tsx +5 -5
- package/src/components/item/meta.md +11 -11
- package/src/components/radio-group/index.tsx +1 -1
- package/src/components/rate/index.tsx +3 -3
- package/src/components/select/index.tsx +2 -2
- package/src/components/sidebar/index.tsx +4 -4
- package/src/components/skeleton/index.tsx +1 -1
- package/src/components/skeleton/meta.md +6 -6
- package/src/components/skeleton/stories.tsx +8 -8
- package/src/components/slider/index.tsx +27 -1
- package/src/components/sonner/index.tsx +43 -40
- package/src/components/sonner/meta.md +84 -68
- package/src/components/sonner/stories.tsx +122 -83
- package/src/components/spinner/index.tsx +170 -0
- package/src/components/spinner/meta.md +27 -1
- package/src/components/spinner/stories.tsx +23 -0
- package/src/components/steps/index.tsx +5 -1
- package/src/components/switch/index.tsx +1 -1
- package/src/components/tag/index.tsx +14 -0
- package/src/components/tag/meta.md +1 -0
- package/src/components/tag/stories.tsx +13 -0
- package/src/components/textarea/index.tsx +1 -1
- package/src/components/textarea/stories.tsx +1 -1
- package/src/components/time-picker/index.tsx +3 -1
- package/src/components/toggle/index.tsx +1 -1
- package/src/components/tooltip/index.tsx +5 -1
- package/src/components/tooltip/meta.md +13 -28
- package/src/components/tooltip/stories.tsx +11 -28
- package/src/components/tree-select/index.tsx +1 -1
|
@@ -30,15 +30,18 @@
|
|
|
30
30
|
标题 + 描述 — 对应 cloud-design `Notification.open({ title, content })`。
|
|
31
31
|
|
|
32
32
|
```tsx
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
<>
|
|
34
|
+
<Button
|
|
35
|
+
onClick={() =>
|
|
36
|
+
toast('文件已保存', {
|
|
37
|
+
description: '你的修改已自动同步到云端,可在历史版本中找回。',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
触发
|
|
42
|
+
</Button>
|
|
43
|
+
<Toaster />
|
|
44
|
+
</>
|
|
42
45
|
```
|
|
43
46
|
|
|
44
47
|
### WithAction
|
|
@@ -46,19 +49,22 @@
|
|
|
46
49
|
操作按钮 — 业内主流 `action` props,单击执行下一步行动并关闭。
|
|
47
50
|
|
|
48
51
|
```tsx
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
<>
|
|
53
|
+
<Button
|
|
54
|
+
onClick={() =>
|
|
55
|
+
toast('已删除项目「示例」', {
|
|
56
|
+
description: '删除后 7 天内可在回收站恢复。',
|
|
57
|
+
action: {
|
|
58
|
+
label: '撤销',
|
|
59
|
+
onClick: () => toast.success('已撤销删除'),
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
触发
|
|
65
|
+
</Button>
|
|
66
|
+
<Toaster />
|
|
67
|
+
</>
|
|
62
68
|
```
|
|
63
69
|
|
|
64
70
|
### WithCancel
|
|
@@ -66,17 +72,20 @@
|
|
|
66
72
|
取消按钮 — 与 action 并存的次级行动点,可阻断默认行为。
|
|
67
73
|
|
|
68
74
|
```tsx
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
<>
|
|
76
|
+
<Button
|
|
77
|
+
onClick={() =>
|
|
78
|
+
toast('确认离开当前页面?', {
|
|
79
|
+
description: '未保存的修改将丢失。',
|
|
80
|
+
action: { label: '继续', onClick: () => toast.info('已继续') },
|
|
81
|
+
cancel: { label: '取消', onClick: () => toast('已取消') },
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
触发
|
|
86
|
+
</Button>
|
|
87
|
+
<Toaster />
|
|
88
|
+
</>
|
|
80
89
|
```
|
|
81
90
|
|
|
82
91
|
### LoadingThenResolve
|
|
@@ -84,19 +93,22 @@
|
|
|
84
93
|
按 id 链式更新 — 等价 cloud-design `Message.loading + Message.success({ key })`。
|
|
85
94
|
|
|
86
95
|
```tsx
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
<>
|
|
97
|
+
<Button
|
|
98
|
+
onClick={() => {
|
|
99
|
+
const id = toast.loading('正在上传...');
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
toast.success('上传完成', {
|
|
102
|
+
id,
|
|
103
|
+
description: '文件已发布到生产环境。',
|
|
104
|
+
});
|
|
105
|
+
}, 1500);
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
触发
|
|
109
|
+
</Button>
|
|
110
|
+
<Toaster />
|
|
111
|
+
</>
|
|
100
112
|
```
|
|
101
113
|
|
|
102
114
|
### Duration
|
|
@@ -121,6 +133,7 @@
|
|
|
121
133
|
>
|
|
122
134
|
持久 (Infinity)
|
|
123
135
|
</Button>
|
|
136
|
+
<Toaster />
|
|
124
137
|
</div>
|
|
125
138
|
```
|
|
126
139
|
|
|
@@ -129,28 +142,31 @@
|
|
|
129
142
|
自定义渲染 — `toast.custom` 完全接管节点,用于嵌入富内容卡片。
|
|
130
143
|
|
|
131
144
|
```tsx
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
<div className="
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
<>
|
|
146
|
+
<Button
|
|
147
|
+
onClick={() =>
|
|
148
|
+
toast.custom((id) => (
|
|
149
|
+
<div className="flex w-80 items-start gap-3 rounded-md border border-border bg-popover p-3 text-popover-foreground">
|
|
150
|
+
<HelpFilledIcon className="mt-0.5 size-4 shrink-0 text-help" />
|
|
151
|
+
<div className="flex-1">
|
|
152
|
+
<div className="text-sm font-medium">自定义卡片</div>
|
|
153
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
154
|
+
你可以在此渲染任意 JSX:表单、富文本、进度条等。
|
|
155
|
+
</div>
|
|
141
156
|
</div>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className="cursor-pointer text-xs text-muted-foreground hover:text-foreground"
|
|
160
|
+
onClick={() => toast.dismiss(id)}
|
|
161
|
+
>
|
|
162
|
+
关闭
|
|
163
|
+
</button>
|
|
142
164
|
</div>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</div>
|
|
151
|
-
))
|
|
152
|
-
}
|
|
153
|
-
>
|
|
154
|
-
触发
|
|
155
|
-
</Button>
|
|
165
|
+
))
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
触发
|
|
169
|
+
</Button>
|
|
170
|
+
<Toaster />
|
|
171
|
+
</>
|
|
156
172
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
<div className="
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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,
|