@vvedantb/ui 0.34.0 → 0.36.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.
- package/README.md +50 -6
- package/package.json +10 -2
- package/src/ai-elements/activity-shared.tsx +1 -1
- package/src/ai-elements/activity-tasks.tsx +5 -19
- package/src/ai-elements/attachments.tsx +2 -2
- package/src/ai-elements/code-block.tsx +3 -1
- package/src/ai-elements/conversation.tsx +10 -4
- package/src/ai-elements/prompt-input.tsx +5 -4
- package/src/client.ts +3 -0
- package/src/icons/animated/animated-bell-icon.tsx +68 -0
- package/src/icons/animated/animated-counter.tsx +45 -0
- package/src/icons/animated/animated-key-icon.tsx +79 -0
- package/src/icons/animated/animated-notification-icon.tsx +202 -0
- package/src/icons/animated/animated-progress.tsx +74 -0
- package/src/icons/animated/animated-status-dot.tsx +98 -0
- package/src/icons/animated/index.ts +22 -0
- package/src/icons/animated/morphing-menu-icon.tsx +71 -0
- package/src/icons/animated/morphing-mic-icon.tsx +52 -0
- package/src/icons/animated/pulsing-rings.tsx +50 -0
- package/src/index.ts +3 -0
- package/src/logos/chrome-icon.tsx +29 -0
- package/src/logos/claude-icon.tsx +13 -0
- package/src/logos/convex-icon.tsx +21 -0
- package/src/logos/cursor-icon.tsx +10 -0
- package/src/logos/dropbox-icon.tsx +14 -0
- package/src/logos/figma-icon.tsx +26 -0
- package/src/logos/github-icon.tsx +10 -0
- package/src/logos/gmail-icon.tsx +34 -0
- package/src/logos/google-drive-icon.tsx +33 -0
- package/src/logos/index.ts +19 -0
- package/src/logos/linear-icon.tsx +13 -0
- package/src/logos/logo-svg.tsx +32 -0
- package/src/logos/notion-icon.tsx +19 -0
- package/src/logos/onedrive-icon.tsx +165 -0
- package/src/logos/sharepoint-icon.tsx +222 -0
- package/src/logos/slack-icon.tsx +25 -0
- package/src/logos/vercel-icon.tsx +10 -0
- package/src/ui/_menu-parts.tsx +1 -1
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/clear-input.tsx +2 -2
- package/src/ui/command.tsx +2 -2
- package/src/ui/context-menu.tsx +3 -3
- package/src/ui/dropdown-menu.tsx +1 -1
- package/src/ui/hover-card.tsx +3 -3
- package/src/ui/input-group.tsx +4 -4
- package/src/ui/popover.tsx +3 -5
- package/src/ui/search-input.tsx +1 -1
- package/src/ui/select.tsx +5 -5
- package/src/ui/sonner.tsx +3 -0
- package/src/ui/tooltip.tsx +3 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { m } from "motion/react";
|
|
2
|
+
|
|
3
|
+
export type NotificationIconType = "success" | "warning" | "error" | "info";
|
|
4
|
+
|
|
5
|
+
export interface AnimatedNotificationIconProps {
|
|
6
|
+
type: NotificationIconType;
|
|
7
|
+
/** Width and height in px. Default `20`. */
|
|
8
|
+
size?: number;
|
|
9
|
+
/** Stroke colour. Default: the type's token (`--color-success`, `--color-warning`, `--color-danger`, `--color-primary` for `info`). */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Class on the wrapping `span`. */
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TYPE_COLORS: Record<NotificationIconType, string> = {
|
|
16
|
+
success: "var(--color-success)",
|
|
17
|
+
warning: "var(--color-warning)",
|
|
18
|
+
error: "var(--color-danger)",
|
|
19
|
+
info: "var(--color-primary)",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface PartProps {
|
|
23
|
+
size: number;
|
|
24
|
+
color: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SuccessIcon({ size, color }: PartProps) {
|
|
28
|
+
return (
|
|
29
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
30
|
+
{/* Circle */}
|
|
31
|
+
<m.circle
|
|
32
|
+
cx="12"
|
|
33
|
+
cy="12"
|
|
34
|
+
r="10"
|
|
35
|
+
stroke={color}
|
|
36
|
+
strokeWidth="1.5"
|
|
37
|
+
fill="none"
|
|
38
|
+
initial={{ pathLength: 0, opacity: 0 }}
|
|
39
|
+
animate={{ pathLength: 1, opacity: 1 }}
|
|
40
|
+
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
41
|
+
/>
|
|
42
|
+
{/* Checkmark draws itself */}
|
|
43
|
+
<m.path
|
|
44
|
+
d="M8 12l2.5 2.5L16 9"
|
|
45
|
+
stroke={color}
|
|
46
|
+
strokeWidth="1.5"
|
|
47
|
+
strokeLinecap="round"
|
|
48
|
+
strokeLinejoin="round"
|
|
49
|
+
fill="none"
|
|
50
|
+
initial={{ pathLength: 0 }}
|
|
51
|
+
animate={{ pathLength: 1 }}
|
|
52
|
+
transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }}
|
|
53
|
+
/>
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function WarningIcon({ size, color }: PartProps) {
|
|
59
|
+
return (
|
|
60
|
+
<m.svg
|
|
61
|
+
width={size}
|
|
62
|
+
height={size}
|
|
63
|
+
viewBox="0 0 24 24"
|
|
64
|
+
fill="none"
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
initial={{ rotate: 0 }}
|
|
67
|
+
animate={{ rotate: [0, -5, 5, -3, 3, 0] }}
|
|
68
|
+
transition={{ duration: 0.5, delay: 0.2 }}
|
|
69
|
+
>
|
|
70
|
+
{/* Triangle */}
|
|
71
|
+
<m.path
|
|
72
|
+
d="M12 2L2 20h20L12 2z"
|
|
73
|
+
stroke={color}
|
|
74
|
+
strokeWidth="1.5"
|
|
75
|
+
strokeLinecap="round"
|
|
76
|
+
strokeLinejoin="round"
|
|
77
|
+
fill="none"
|
|
78
|
+
initial={{ pathLength: 0, opacity: 0 }}
|
|
79
|
+
animate={{ pathLength: 1, opacity: 1 }}
|
|
80
|
+
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
81
|
+
/>
|
|
82
|
+
{/* Exclamation line */}
|
|
83
|
+
<m.path
|
|
84
|
+
d="M12 9v4"
|
|
85
|
+
stroke={color}
|
|
86
|
+
strokeWidth="1.5"
|
|
87
|
+
strokeLinecap="round"
|
|
88
|
+
initial={{ pathLength: 0 }}
|
|
89
|
+
animate={{ pathLength: 1 }}
|
|
90
|
+
transition={{ duration: 0.2, delay: 0.3 }}
|
|
91
|
+
/>
|
|
92
|
+
{/* Exclamation dot */}
|
|
93
|
+
<m.circle
|
|
94
|
+
cx="12"
|
|
95
|
+
cy="17"
|
|
96
|
+
r="1"
|
|
97
|
+
fill={color}
|
|
98
|
+
initial={{ scale: 0 }}
|
|
99
|
+
animate={{ scale: 1 }}
|
|
100
|
+
transition={{ duration: 0.15, delay: 0.45, type: "spring" }}
|
|
101
|
+
/>
|
|
102
|
+
</m.svg>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ErrorIcon({ size, color }: PartProps) {
|
|
107
|
+
return (
|
|
108
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
109
|
+
{/* Circle */}
|
|
110
|
+
<m.circle
|
|
111
|
+
cx="12"
|
|
112
|
+
cy="12"
|
|
113
|
+
r="10"
|
|
114
|
+
stroke={color}
|
|
115
|
+
strokeWidth="1.5"
|
|
116
|
+
fill="none"
|
|
117
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
118
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
119
|
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
120
|
+
/>
|
|
121
|
+
{/* X, first stroke */}
|
|
122
|
+
<m.path
|
|
123
|
+
d="M15 9l-6 6"
|
|
124
|
+
stroke={color}
|
|
125
|
+
strokeWidth="1.5"
|
|
126
|
+
strokeLinecap="round"
|
|
127
|
+
initial={{ pathLength: 0 }}
|
|
128
|
+
animate={{ pathLength: 1 }}
|
|
129
|
+
transition={{ duration: 0.2, delay: 0.2 }}
|
|
130
|
+
/>
|
|
131
|
+
{/* X, second stroke */}
|
|
132
|
+
<m.path
|
|
133
|
+
d="M9 9l6 6"
|
|
134
|
+
stroke={color}
|
|
135
|
+
strokeWidth="1.5"
|
|
136
|
+
strokeLinecap="round"
|
|
137
|
+
initial={{ pathLength: 0 }}
|
|
138
|
+
animate={{ pathLength: 1 }}
|
|
139
|
+
transition={{ duration: 0.2, delay: 0.3 }}
|
|
140
|
+
/>
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function InfoIcon({ size, color }: PartProps) {
|
|
146
|
+
return (
|
|
147
|
+
<m.svg
|
|
148
|
+
width={size}
|
|
149
|
+
height={size}
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
fill="none"
|
|
152
|
+
aria-hidden="true"
|
|
153
|
+
initial={{ y: 2, opacity: 0 }}
|
|
154
|
+
animate={{ y: 0, opacity: 1 }}
|
|
155
|
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
156
|
+
>
|
|
157
|
+
{/* Circle */}
|
|
158
|
+
<circle cx="12" cy="12" r="10" stroke={color} strokeWidth="1.5" fill="none" />
|
|
159
|
+
{/* Dot */}
|
|
160
|
+
<m.circle
|
|
161
|
+
cx="12"
|
|
162
|
+
cy="8"
|
|
163
|
+
r="1"
|
|
164
|
+
fill={color}
|
|
165
|
+
initial={{ scale: 0 }}
|
|
166
|
+
animate={{ scale: 1 }}
|
|
167
|
+
transition={{ duration: 0.2, delay: 0.2, type: "spring" }}
|
|
168
|
+
/>
|
|
169
|
+
{/* Line */}
|
|
170
|
+
<m.path
|
|
171
|
+
d="M12 12v4"
|
|
172
|
+
stroke={color}
|
|
173
|
+
strokeWidth="1.5"
|
|
174
|
+
strokeLinecap="round"
|
|
175
|
+
initial={{ pathLength: 0 }}
|
|
176
|
+
animate={{ pathLength: 1 }}
|
|
177
|
+
transition={{ duration: 0.2, delay: 0.3 }}
|
|
178
|
+
/>
|
|
179
|
+
</m.svg>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const PARTS: Record<NotificationIconType, (props: PartProps) => React.JSX.Element> = {
|
|
184
|
+
success: SuccessIcon,
|
|
185
|
+
warning: WarningIcon,
|
|
186
|
+
error: ErrorIcon,
|
|
187
|
+
info: InfoIcon,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Notification status icon that animates on mount: the check draws itself,
|
|
192
|
+
* the warning shakes, the error pops in and the info icon rises. Decorative
|
|
193
|
+
* (`aria-hidden`); put the message text beside it.
|
|
194
|
+
*/
|
|
195
|
+
export function AnimatedNotificationIcon({ type, size = 20, color, className }: AnimatedNotificationIconProps) {
|
|
196
|
+
const Part = PARTS[type];
|
|
197
|
+
return (
|
|
198
|
+
<span className={className} data-type={type}>
|
|
199
|
+
<Part size={size} color={color ?? TYPE_COLORS[type]} />
|
|
200
|
+
</span>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { m, useSpring, useTransform } from "motion/react";
|
|
2
|
+
import { useEffect, useId } from "react";
|
|
3
|
+
|
|
4
|
+
export interface AnimatedProgressProps {
|
|
5
|
+
/** Progress from 0 to 100 (clamped). */
|
|
6
|
+
value: number;
|
|
7
|
+
/** Bar height in px. Default `4`. */
|
|
8
|
+
height?: number;
|
|
9
|
+
/** Class on the wrapping `div`. */
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Run the moving gradient and shimmer. Default `true`. */
|
|
12
|
+
showGradient?: boolean;
|
|
13
|
+
/** Accessible name for the `progressbar`. */
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Thin progress bar whose fill springs to `value`, with a moving gradient
|
|
19
|
+
* and shimmer (SVG `<animate>`). Fill: `--color-primary`; track:
|
|
20
|
+
* `--color-surface-secondary`. Renders `role="progressbar"`.
|
|
21
|
+
*/
|
|
22
|
+
export function AnimatedProgress({ value, height = 4, className, showGradient = true, label }: AnimatedProgressProps) {
|
|
23
|
+
const id = useId();
|
|
24
|
+
const gradientId = `${id}-gradient`;
|
|
25
|
+
const shimmerId = `${id}-shimmer`;
|
|
26
|
+
const clamped = Math.min(Math.max(value, 0), 100);
|
|
27
|
+
const springValue = useSpring(0, { stiffness: 100, damping: 30 });
|
|
28
|
+
const width = useTransform(springValue, (v) => `${v}%`);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
springValue.set(clamped);
|
|
32
|
+
}, [clamped, springValue]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={className}
|
|
37
|
+
role="progressbar"
|
|
38
|
+
aria-label={label}
|
|
39
|
+
aria-valuemin={0}
|
|
40
|
+
aria-valuemax={100}
|
|
41
|
+
aria-valuenow={Math.round(clamped)}
|
|
42
|
+
style={{ height, overflow: "hidden", borderRadius: height / 2 }}
|
|
43
|
+
>
|
|
44
|
+
<svg width="100%" height={height} className="block" aria-hidden="true">
|
|
45
|
+
<defs>
|
|
46
|
+
{/* Moving gradient */}
|
|
47
|
+
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
|
48
|
+
<stop offset="0%" stopColor="var(--color-primary)" />
|
|
49
|
+
<stop offset="50%" stopColor="color-mix(in oklch, var(--color-primary) 80%, transparent)" />
|
|
50
|
+
<stop offset="100%" stopColor="var(--color-primary)" />
|
|
51
|
+
{showGradient ? <animate attributeName="x1" values="-100%;100%" dur="2s" repeatCount="indefinite" /> : null}
|
|
52
|
+
{showGradient ? <animate attributeName="x2" values="0%;200%" dur="2s" repeatCount="indefinite" /> : null}
|
|
53
|
+
</linearGradient>
|
|
54
|
+
{/* Shimmer overlay */}
|
|
55
|
+
<linearGradient id={shimmerId} x1="0%" y1="0%" x2="100%" y2="0%">
|
|
56
|
+
<stop offset="0%" stopColor="transparent" />
|
|
57
|
+
<stop offset="50%" stopColor="rgba(255,255,255,0.3)" />
|
|
58
|
+
<stop offset="100%" stopColor="transparent" />
|
|
59
|
+
<animate attributeName="x1" values="-50%;150%" dur="1.5s" repeatCount="indefinite" />
|
|
60
|
+
<animate attributeName="x2" values="0%;200%" dur="1.5s" repeatCount="indefinite" />
|
|
61
|
+
</linearGradient>
|
|
62
|
+
</defs>
|
|
63
|
+
{/* Track */}
|
|
64
|
+
<rect x="0" y="0" width="100%" height={height} rx={height / 2} fill="var(--color-surface-secondary)" />
|
|
65
|
+
{/* Fill */}
|
|
66
|
+
<m.rect x="0" y="0" height={height} rx={height / 2} fill={`url(#${gradientId})`} style={{ width }} />
|
|
67
|
+
{/* Shimmer */}
|
|
68
|
+
{showGradient && clamped > 0 ? (
|
|
69
|
+
<m.rect x="0" y="0" height={height} rx={height / 2} fill={`url(#${shimmerId})`} style={{ width }} />
|
|
70
|
+
) : null}
|
|
71
|
+
</svg>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { m } from "motion/react";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
|
|
4
|
+
export type StatusDotPhase = "idle" | "listening" | "thinking" | "speaking" | "error";
|
|
5
|
+
|
|
6
|
+
export interface AnimatedStatusDotProps {
|
|
7
|
+
phase: StatusDotPhase;
|
|
8
|
+
/** Dot diameter in px. The svg is `size * 3` square to leave room for the ripples. Default `10`. */
|
|
9
|
+
size?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PHASE_COLORS: Record<StatusDotPhase, string> = {
|
|
14
|
+
idle: "var(--color-muted-foreground)",
|
|
15
|
+
listening: "var(--color-danger)",
|
|
16
|
+
thinking: "var(--color-warning)",
|
|
17
|
+
speaking: "var(--color-success)",
|
|
18
|
+
error: "var(--color-danger)",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const RIPPLE_DELAYS = [0, 0.5, 1];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Status dot for voice states: ripples while `listening`, a glow pulse while
|
|
25
|
+
* `listening` or `speaking`. Decorative (`aria-hidden`); show the phase as text too.
|
|
26
|
+
*/
|
|
27
|
+
export function AnimatedStatusDot({ phase, size = 10, className }: AnimatedStatusDotProps) {
|
|
28
|
+
const filterId = `${useId()}-glow`;
|
|
29
|
+
const color = PHASE_COLORS[phase];
|
|
30
|
+
const isListening = phase === "listening";
|
|
31
|
+
const isActive = isListening || phase === "speaking";
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<svg
|
|
35
|
+
width={size * 3}
|
|
36
|
+
height={size * 3}
|
|
37
|
+
viewBox="0 0 30 30"
|
|
38
|
+
className={className}
|
|
39
|
+
overflow="visible"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
data-phase={phase}
|
|
42
|
+
>
|
|
43
|
+
<defs>
|
|
44
|
+
<filter id={filterId} x="-50%" y="-50%" width="200%" height="200%">
|
|
45
|
+
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
|
46
|
+
<feMerge>
|
|
47
|
+
<feMergeNode in="coloredBlur" />
|
|
48
|
+
<feMergeNode in="SourceGraphic" />
|
|
49
|
+
</feMerge>
|
|
50
|
+
</filter>
|
|
51
|
+
</defs>
|
|
52
|
+
|
|
53
|
+
{/* Ripples while listening */}
|
|
54
|
+
{isListening
|
|
55
|
+
? RIPPLE_DELAYS.map((delay) => (
|
|
56
|
+
<m.circle
|
|
57
|
+
key={delay}
|
|
58
|
+
cx="15"
|
|
59
|
+
cy="15"
|
|
60
|
+
r="5"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke={color}
|
|
63
|
+
strokeWidth="1"
|
|
64
|
+
initial={{ r: 5, opacity: 0.6 }}
|
|
65
|
+
animate={{ r: 14, opacity: 0 }}
|
|
66
|
+
transition={{ duration: 1.5, repeat: Infinity, ease: "easeOut", delay }}
|
|
67
|
+
/>
|
|
68
|
+
))
|
|
69
|
+
: null}
|
|
70
|
+
|
|
71
|
+
{/* Glow ring while active */}
|
|
72
|
+
{isActive ? (
|
|
73
|
+
<m.circle
|
|
74
|
+
cx="15"
|
|
75
|
+
cy="15"
|
|
76
|
+
r="7"
|
|
77
|
+
fill={color}
|
|
78
|
+
opacity={0.3}
|
|
79
|
+
initial={{ scale: 1 }}
|
|
80
|
+
animate={{ scale: [1, 1.2, 1], opacity: [0.3, 0.15, 0.3] }}
|
|
81
|
+
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
|
82
|
+
/>
|
|
83
|
+
) : null}
|
|
84
|
+
|
|
85
|
+
{/* Dot */}
|
|
86
|
+
<m.circle
|
|
87
|
+
cx="15"
|
|
88
|
+
cy="15"
|
|
89
|
+
r="5"
|
|
90
|
+
fill={color}
|
|
91
|
+
filter={isActive ? `url(#${filterId})` : undefined}
|
|
92
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
93
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
94
|
+
transition={{ duration: 0.2 }}
|
|
95
|
+
/>
|
|
96
|
+
</svg>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Motion-driven animated SVG icons. Entry: `@vvedantb/ui/icons/animated` (not re-exported from
|
|
2
|
+
// the root or `/client`, so apps that do not use them do not load them). They need the optional
|
|
3
|
+
// `motion` peer and animate through `m.*`, so wrap the app in `MotionProvider` (or its own
|
|
4
|
+
// `LazyMotion`); without it they render their resting frame. No stylesheet is needed.
|
|
5
|
+
export { AnimatedBellIcon } from "./animated-bell-icon";
|
|
6
|
+
export type { AnimatedBellIconProps } from "./animated-bell-icon";
|
|
7
|
+
export { AnimatedCounter } from "./animated-counter";
|
|
8
|
+
export type { AnimatedCounterProps } from "./animated-counter";
|
|
9
|
+
export { AnimatedKeyIcon } from "./animated-key-icon";
|
|
10
|
+
export type { AnimatedKeyIconProps } from "./animated-key-icon";
|
|
11
|
+
export { AnimatedNotificationIcon } from "./animated-notification-icon";
|
|
12
|
+
export type { AnimatedNotificationIconProps, NotificationIconType } from "./animated-notification-icon";
|
|
13
|
+
export { AnimatedProgress } from "./animated-progress";
|
|
14
|
+
export type { AnimatedProgressProps } from "./animated-progress";
|
|
15
|
+
export { AnimatedStatusDot } from "./animated-status-dot";
|
|
16
|
+
export type { AnimatedStatusDotProps, StatusDotPhase } from "./animated-status-dot";
|
|
17
|
+
export { MorphingMenuIcon } from "./morphing-menu-icon";
|
|
18
|
+
export type { MorphingMenuIconProps } from "./morphing-menu-icon";
|
|
19
|
+
export { MorphingMicIcon } from "./morphing-mic-icon";
|
|
20
|
+
export type { MorphingMicIconProps } from "./morphing-mic-icon";
|
|
21
|
+
export { PulsingRings } from "./pulsing-rings";
|
|
22
|
+
export type { PulsingRingsProps } from "./pulsing-rings";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { m } from "motion/react";
|
|
2
|
+
|
|
3
|
+
export interface MorphingMenuIconProps {
|
|
4
|
+
/** `true` draws the X (close), `false` the three bars (menu). */
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
/** Width and height in px. Default `20`. */
|
|
7
|
+
size?: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LINE = {
|
|
12
|
+
stroke: "currentColor",
|
|
13
|
+
strokeWidth: 2,
|
|
14
|
+
strokeLinecap: "round" as const,
|
|
15
|
+
strokeLinejoin: "round" as const,
|
|
16
|
+
};
|
|
17
|
+
const MORPH = { duration: 0.25, ease: "easeInOut" } as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hamburger that morphs into an X: the outer bars cross and the middle bar
|
|
21
|
+
* fades out. Uses `currentColor`. Decorative (`aria-hidden`): label the button
|
|
22
|
+
* that holds it and set its `aria-expanded`.
|
|
23
|
+
*/
|
|
24
|
+
export function MorphingMenuIcon({ isOpen, size = 20, className }: MorphingMenuIconProps) {
|
|
25
|
+
return (
|
|
26
|
+
<svg
|
|
27
|
+
width={size}
|
|
28
|
+
height={size}
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
fill="none"
|
|
31
|
+
className={className}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
data-state={isOpen ? "open" : "closed"}
|
|
34
|
+
>
|
|
35
|
+
{/* Top bar: becomes the "\" of the X */}
|
|
36
|
+
<m.line
|
|
37
|
+
x1="4"
|
|
38
|
+
y1="6"
|
|
39
|
+
x2="20"
|
|
40
|
+
y2="6"
|
|
41
|
+
{...LINE}
|
|
42
|
+
initial={false}
|
|
43
|
+
animate={{ x1: isOpen ? 5 : 4, y1: isOpen ? 5 : 6, x2: isOpen ? 19 : 20, y2: isOpen ? 19 : 6 }}
|
|
44
|
+
transition={MORPH}
|
|
45
|
+
/>
|
|
46
|
+
{/* Middle bar: fades out */}
|
|
47
|
+
<m.line
|
|
48
|
+
x1="4"
|
|
49
|
+
y1="12"
|
|
50
|
+
x2="20"
|
|
51
|
+
y2="12"
|
|
52
|
+
{...LINE}
|
|
53
|
+
initial={false}
|
|
54
|
+
animate={{ opacity: isOpen ? 0 : 1, scaleX: isOpen ? 0 : 1 }}
|
|
55
|
+
transition={{ duration: 0.15, ease: "easeInOut" }}
|
|
56
|
+
style={{ transformOrigin: "center" }}
|
|
57
|
+
/>
|
|
58
|
+
{/* Bottom bar: becomes the "/" of the X */}
|
|
59
|
+
<m.line
|
|
60
|
+
x1="4"
|
|
61
|
+
y1="18"
|
|
62
|
+
x2="20"
|
|
63
|
+
y2="18"
|
|
64
|
+
{...LINE}
|
|
65
|
+
initial={false}
|
|
66
|
+
animate={{ x1: isOpen ? 5 : 4, y1: isOpen ? 19 : 18, x2: isOpen ? 19 : 20, y2: isOpen ? 5 : 18 }}
|
|
67
|
+
transition={MORPH}
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { m } from "motion/react";
|
|
2
|
+
import { cn } from "../../utils/cn";
|
|
3
|
+
|
|
4
|
+
export interface MorphingMicIconProps {
|
|
5
|
+
/** `true` shows the stop square, `false` the microphone. */
|
|
6
|
+
isListening: boolean;
|
|
7
|
+
/** Width and height in px. Default `32`. */
|
|
8
|
+
size?: number;
|
|
9
|
+
strokeWidth?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ICON_TRANSITION = { type: "spring", duration: 0.3, bounce: 0 } as const;
|
|
14
|
+
const HIDDEN = { scale: 0.25, opacity: 0, filter: "blur(4px)" };
|
|
15
|
+
const VISIBLE = { scale: 1, opacity: 1, filter: "blur(0px)" };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cross-fades a microphone and a stop square (scale, opacity and blur).
|
|
19
|
+
* Uses `currentColor`. Decorative (`aria-hidden`): label the button that holds it.
|
|
20
|
+
*/
|
|
21
|
+
export function MorphingMicIcon({ isListening, size = 32, strokeWidth = 2, className }: MorphingMicIconProps) {
|
|
22
|
+
const svgProps = {
|
|
23
|
+
width: size,
|
|
24
|
+
height: size,
|
|
25
|
+
viewBox: "0 0 24 24",
|
|
26
|
+
fill: "none",
|
|
27
|
+
stroke: "currentColor",
|
|
28
|
+
strokeWidth,
|
|
29
|
+
strokeLinecap: "round" as const,
|
|
30
|
+
strokeLinejoin: "round" as const,
|
|
31
|
+
className: "absolute inset-0",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
className={cn("relative inline-flex shrink-0", className)}
|
|
37
|
+
style={{ width: size, height: size }}
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
data-state={isListening ? "listening" : "idle"}
|
|
40
|
+
>
|
|
41
|
+
<m.svg {...svgProps} data-slot="mic" initial={false} animate={isListening ? HIDDEN : VISIBLE} transition={ICON_TRANSITION}>
|
|
42
|
+
<rect x="9" y="1" width="6" height="12" rx="3" />
|
|
43
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
44
|
+
<path d="M12 19v4" />
|
|
45
|
+
<path d="M8 23h8" />
|
|
46
|
+
</m.svg>
|
|
47
|
+
<m.svg {...svgProps} data-slot="stop" initial={false} animate={isListening ? VISIBLE : HIDDEN} transition={ICON_TRANSITION}>
|
|
48
|
+
<rect x="6" y="6" width="12" height="12" rx="2" />
|
|
49
|
+
</m.svg>
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { m } from "motion/react";
|
|
2
|
+
import { cn } from "../../utils/cn";
|
|
3
|
+
|
|
4
|
+
export interface PulsingRingsProps {
|
|
5
|
+
/** Size of the element the rings surround (for example the record button), in px. The svg is `size * 1.6`. */
|
|
6
|
+
size: number;
|
|
7
|
+
/** Number of rings. Default `3`. */
|
|
8
|
+
ringCount?: number;
|
|
9
|
+
/** Ring colour. Default `var(--color-danger)`. */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Render the rings. Default `true`; `false` renders nothing. */
|
|
12
|
+
active?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Concentric rings that expand and fade with staggered timing, for a
|
|
18
|
+
* recording state. Absolutely centred: put it in a `relative` parent beside
|
|
19
|
+
* the button. Decorative and `pointer-events-none`.
|
|
20
|
+
*/
|
|
21
|
+
export function PulsingRings({ size, ringCount = 3, color = "var(--color-danger)", active = true, className }: PulsingRingsProps) {
|
|
22
|
+
if (!active) return null;
|
|
23
|
+
const baseDelay = 1.2 / ringCount;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<svg
|
|
27
|
+
width={size * 1.6}
|
|
28
|
+
height={size * 1.6}
|
|
29
|
+
viewBox="0 0 100 100"
|
|
30
|
+
className={cn("pointer-events-none absolute", className)}
|
|
31
|
+
style={{ left: "50%", top: "50%", transform: "translate(-50%, -50%)" }}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
{Array.from({ length: ringCount }, (_, i) => (
|
|
35
|
+
<m.circle
|
|
36
|
+
key={i}
|
|
37
|
+
cx="50"
|
|
38
|
+
cy="50"
|
|
39
|
+
r="30"
|
|
40
|
+
fill="none"
|
|
41
|
+
stroke={color}
|
|
42
|
+
strokeWidth="2"
|
|
43
|
+
initial={{ r: 30, opacity: 0.6 }}
|
|
44
|
+
animate={{ r: 48, opacity: 0 }}
|
|
45
|
+
transition={{ duration: 1.2, repeat: Infinity, ease: "easeOut", delay: i * baseDelay }}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</svg>
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -330,6 +330,9 @@ export * from "../app/src/index";
|
|
|
330
330
|
// Family app marks (Icon* components, deprecated AppMark and raw SVG strings for favicons).
|
|
331
331
|
export * from "./brand";
|
|
332
332
|
|
|
333
|
+
// Third-party brand logos (`GitHubIcon`, `SlackIcon`, `ClaudeIcon`, ...). Also `@vvedantb/ui/logos`.
|
|
334
|
+
export * from "./logos";
|
|
335
|
+
|
|
333
336
|
// Native tokens (JS values that match tokens.css).
|
|
334
337
|
export { tokens } from "../tokens/src/native";
|
|
335
338
|
export type { Tokens } from "../tokens/src/native";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Google Chrome mark (svgl `chrome.svg`). */
|
|
4
|
+
export function ChromeIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 190.5 190.5" preserveAspectRatio="xMidYMid" {...props}>
|
|
7
|
+
<path
|
|
8
|
+
fill="#fff"
|
|
9
|
+
d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z"
|
|
10
|
+
/>
|
|
11
|
+
<path
|
|
12
|
+
fill="#229342"
|
|
13
|
+
d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
fill="#fbc116"
|
|
17
|
+
d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z"
|
|
18
|
+
/>
|
|
19
|
+
<path
|
|
20
|
+
fill="#1a73e8"
|
|
21
|
+
d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
fill="#e33b2e"
|
|
25
|
+
d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z"
|
|
26
|
+
/>
|
|
27
|
+
</LogoSvg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Anthropic Claude mark. Monochrome: uses `currentColor`. */
|
|
4
|
+
export function ClaudeIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 24 24" {...props}>
|
|
7
|
+
<path
|
|
8
|
+
fill="currentColor"
|
|
9
|
+
d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"
|
|
10
|
+
/>
|
|
11
|
+
</LogoSvg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Convex mark (svgl `convex.svg`). */
|
|
4
|
+
export function ConvexIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="28 28 128 132" fill="none" {...props}>
|
|
7
|
+
<path
|
|
8
|
+
fill="#F3B01C"
|
|
9
|
+
d="M108.092 130.021c18.166-2.018 35.293-11.698 44.723-27.854-4.466 39.961-48.162 65.218-83.83 49.711-3.286-1.425-6.115-3.796-8.056-6.844-8.016-12.586-10.65-28.601-6.865-43.135 10.817 18.668 32.81 30.111 54.028 28.122Z"
|
|
10
|
+
/>
|
|
11
|
+
<path
|
|
12
|
+
fill="#8D2676"
|
|
13
|
+
d="M53.401 90.174c-7.364 17.017-7.682 36.94 1.345 53.336-31.77-23.902-31.423-75.052-.388-98.715 2.87-2.187 6.282-3.485 9.86-3.683 14.713-.776 29.662 4.91 40.146 15.507-21.3.212-42.046 13.857-50.963 33.555Z"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
fill="#EE342F"
|
|
17
|
+
d="M114.637 61.855C103.89 46.87 87.069 36.668 68.639 36.358c35.625-16.17 79.446 10.047 84.217 48.807.444 3.598-.139 7.267-1.734 10.512-6.656 13.518-18.998 24.002-33.42 27.882 10.567-19.599 9.263-43.544-3.065-61.704Z"
|
|
18
|
+
/>
|
|
19
|
+
</LogoSvg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Cursor mark (svgl `cursor_light.svg`). Monochrome: uses `currentColor`. */
|
|
4
|
+
export function CursorIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 466.73 532.09" fill="currentColor" {...props}>
|
|
7
|
+
<path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z" />
|
|
8
|
+
</LogoSvg>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Dropbox mark. */
|
|
4
|
+
export function DropboxIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 24 24" {...props}>
|
|
7
|
+
<path d="M6 2L0 6L6 10L12 6L6 2Z" fill="#0061FF" />
|
|
8
|
+
<path d="M18 2L12 6L18 10L24 6L18 2Z" fill="#0061FF" />
|
|
9
|
+
<path d="M0 14L6 18L12 14L6 10L0 14Z" fill="#0061FF" />
|
|
10
|
+
<path d="M18 10L12 14L18 18L24 14L18 10Z" fill="#0061FF" />
|
|
11
|
+
<path d="M6 19.5L12 15.5L18 19.5L12 23.5L6 19.5Z" fill="#0061FF" />
|
|
12
|
+
</LogoSvg>
|
|
13
|
+
);
|
|
14
|
+
}
|