@vvedantb/ui 0.35.0 → 0.37.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 +70 -8
- package/package.json +23 -16
- package/src/brand/index.ts +1 -0
- package/src/brand/productApps.ts +20 -0
- package/src/client.ts +15 -2
- 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 +15 -2
- 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/marketing/SiteFooter.tsx +86 -22
- package/src/marketing/landing.tsx +35 -7
- package/src/ui/page-title.tsx +29 -0
- package/app/README.md +0 -269
- package/tokens/README.md +0 -122
|
@@ -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
|
@@ -50,6 +50,7 @@ export * from "./ui/avatar";
|
|
|
50
50
|
export * from "./ui/badge";
|
|
51
51
|
export * from "./ui/border-beam";
|
|
52
52
|
export * from "./ui/breadcrumb";
|
|
53
|
+
export * from "./ui/page-title";
|
|
53
54
|
export * from "./ui/button";
|
|
54
55
|
export * from "./ui/button-group";
|
|
55
56
|
export * from "./ui/card";
|
|
@@ -285,6 +286,8 @@ export {
|
|
|
285
286
|
} from "./marketing/landing";
|
|
286
287
|
export type {
|
|
287
288
|
LandingFeature,
|
|
289
|
+
LandingFooterAppProps,
|
|
290
|
+
LandingFooterLegacyProps,
|
|
288
291
|
LandingFooterProps,
|
|
289
292
|
LandingLatticeProps,
|
|
290
293
|
LandingLink,
|
|
@@ -311,8 +314,15 @@ export {
|
|
|
311
314
|
landingShellClass,
|
|
312
315
|
} from "./marketing/marketing-classes";
|
|
313
316
|
export type { PricingModalTone, SiteFooterVariant } from "./marketing/marketing-classes";
|
|
314
|
-
export { SiteFooter } from "./marketing/SiteFooter";
|
|
315
|
-
export type {
|
|
317
|
+
export { PROJECT_V_LEGAL_LINKS, PROJECT_V_SOCIALS, SITE_FOOTER_WORDMARK_SIZE, SiteFooter } from "./marketing/SiteFooter";
|
|
318
|
+
export type {
|
|
319
|
+
SiteFooterAppProps,
|
|
320
|
+
SiteFooterColumn,
|
|
321
|
+
SiteFooterLegacyProps,
|
|
322
|
+
SiteFooterLink,
|
|
323
|
+
SiteFooterProps,
|
|
324
|
+
SiteFooterSocial,
|
|
325
|
+
} from "./marketing/SiteFooter";
|
|
316
326
|
export { PricingModal, V_PRO_APP_BENEFITS } from "./marketing/PricingModal";
|
|
317
327
|
export type {
|
|
318
328
|
PricingModalLegacyProps,
|
|
@@ -330,6 +340,9 @@ export * from "../app/src/index";
|
|
|
330
340
|
// Family app marks (Icon* components, deprecated AppMark and raw SVG strings for favicons).
|
|
331
341
|
export * from "./brand";
|
|
332
342
|
|
|
343
|
+
// Third-party brand logos (`GitHubIcon`, `SlackIcon`, `ClaudeIcon`, ...). Also `@vvedantb/ui/logos`.
|
|
344
|
+
export * from "./logos";
|
|
345
|
+
|
|
333
346
|
// Native tokens (JS values that match tokens.css).
|
|
334
347
|
export { tokens } from "../tokens/src/native";
|
|
335
348
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Figma mark, in a square viewBox. */
|
|
4
|
+
export function FigmaIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="-9.5 0 57 57" {...props}>
|
|
7
|
+
<path
|
|
8
|
+
d="M19 28.5a9.5 9.5 0 1 1 19 0 9.5 9.5 0 0 1-19 0z"
|
|
9
|
+
fill="#1abcfe"
|
|
10
|
+
/>
|
|
11
|
+
<path
|
|
12
|
+
d="M0 47.5A9.5 9.5 0 0 1 9.5 38H19v9.5a9.5 9.5 0 1 1-19 0z"
|
|
13
|
+
fill="#0acf83"
|
|
14
|
+
/>
|
|
15
|
+
<path d="M19 0v19h9.5a9.5 9.5 0 1 0 0-19H19z" fill="#ff7262" />
|
|
16
|
+
<path
|
|
17
|
+
d="M0 9.5A9.5 9.5 0 0 0 9.5 19H19V0H9.5A9.5 9.5 0 0 0 0 9.5z"
|
|
18
|
+
fill="#f24e1e"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
d="M0 28.5A9.5 9.5 0 0 0 9.5 38H19V19H9.5A9.5 9.5 0 0 0 0 28.5z"
|
|
22
|
+
fill="#a259ff"
|
|
23
|
+
/>
|
|
24
|
+
</LogoSvg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** GitHub mark. Monochrome: uses `currentColor` so it follows light and dark themes. */
|
|
4
|
+
export function GitHubIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 24 24" fill="currentColor" {...props}>
|
|
7
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
8
|
+
</LogoSvg>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Gmail mark (svgl `gmail.svg`). */
|
|
4
|
+
export function GmailIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 49.4 512 399.42" {...props}>
|
|
7
|
+
<g fill="none" fillRule="evenodd">
|
|
8
|
+
<g fillRule="nonzero">
|
|
9
|
+
<path
|
|
10
|
+
fill="#4285f4"
|
|
11
|
+
d="M34.91 448.818h81.454V251L0 163.727V413.91c0 19.287 15.622 34.91 34.91 34.91z"
|
|
12
|
+
/>
|
|
13
|
+
<path
|
|
14
|
+
fill="#34a853"
|
|
15
|
+
d="M395.636 448.818h81.455c19.287 0 34.909-15.622 34.909-34.909V163.727L395.636 251z"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
fill="#fbbc04"
|
|
19
|
+
d="M395.636 99.727V251L512 163.727v-46.545c0-43.142-49.25-67.782-83.782-41.891z"
|
|
20
|
+
/>
|
|
21
|
+
</g>
|
|
22
|
+
<path
|
|
23
|
+
fill="#ea4335"
|
|
24
|
+
d="M116.364 251V99.727L256 204.455 395.636 99.727V251L256 355.727z"
|
|
25
|
+
/>
|
|
26
|
+
<path
|
|
27
|
+
fill="#c5221f"
|
|
28
|
+
fillRule="nonzero"
|
|
29
|
+
d="M0 117.182v46.545L116.364 251V99.727L83.782 75.291C49.25 49.4 0 74.04 0 117.18z"
|
|
30
|
+
/>
|
|
31
|
+
</g>
|
|
32
|
+
</LogoSvg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Google Drive mark. */
|
|
4
|
+
export function GoogleDriveIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 87.3 78" {...props}>
|
|
7
|
+
<path
|
|
8
|
+
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
|
|
9
|
+
fill="#0066da"
|
|
10
|
+
/>
|
|
11
|
+
<path
|
|
12
|
+
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
|
|
13
|
+
fill="#00ac47"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
|
|
17
|
+
fill="#ea4335"
|
|
18
|
+
/>
|
|
19
|
+
<path
|
|
20
|
+
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
|
|
21
|
+
fill="#00832d"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
|
|
25
|
+
fill="#2684fc"
|
|
26
|
+
/>
|
|
27
|
+
<path
|
|
28
|
+
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
|
|
29
|
+
fill="#ffba00"
|
|
30
|
+
/>
|
|
31
|
+
</LogoSvg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Third-party brand logos (full-colour unless noted). Family app marks live in `../brand`.
|
|
2
|
+
// Each logo takes `LogoProps`: `size` (default 24), `title` (labelled `role="img"`; otherwise
|
|
3
|
+
// `aria-hidden`) and any other SVG prop. Paths are the vendors' own; do not recolour them.
|
|
4
|
+
export type { LogoProps } from "./logo-svg";
|
|
5
|
+
export { ChromeIcon } from "./chrome-icon";
|
|
6
|
+
export { ClaudeIcon } from "./claude-icon";
|
|
7
|
+
export { ConvexIcon } from "./convex-icon";
|
|
8
|
+
export { CursorIcon } from "./cursor-icon";
|
|
9
|
+
export { DropboxIcon } from "./dropbox-icon";
|
|
10
|
+
export { FigmaIcon } from "./figma-icon";
|
|
11
|
+
export { GitHubIcon } from "./github-icon";
|
|
12
|
+
export { GmailIcon } from "./gmail-icon";
|
|
13
|
+
export { GoogleDriveIcon } from "./google-drive-icon";
|
|
14
|
+
export { LinearIcon } from "./linear-icon";
|
|
15
|
+
export { NotionIcon } from "./notion-icon";
|
|
16
|
+
export { OneDriveIcon } from "./onedrive-icon";
|
|
17
|
+
export { SharePointIcon } from "./sharepoint-icon";
|
|
18
|
+
export { SlackIcon } from "./slack-icon";
|
|
19
|
+
export { VercelIcon } from "./vercel-icon";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LogoSvg, type LogoProps } from "./logo-svg";
|
|
2
|
+
|
|
3
|
+
/** Linear mark (the current four-band logo, 100x100 viewBox). */
|
|
4
|
+
export function LinearIcon(props: LogoProps) {
|
|
5
|
+
return (
|
|
6
|
+
<LogoSvg viewBox="0 0 100 100" fill="none" {...props}>
|
|
7
|
+
<path
|
|
8
|
+
fill="#5E6AD2"
|
|
9
|
+
d="M1.225 61.523c-.222-.949.908-1.546 1.597-.857l36.512 36.512c.69.69.092 1.82-.857 1.597-18.425-4.323-32.93-18.827-37.252-37.252ZM.002 46.889a.99.99 0 0 0 .29.76L52.35 99.71c.201.2.478.307.76.29 2.37-.149 4.695-.46 6.963-.927.765-.157 1.03-1.096.478-1.648L2.576 39.448c-.552-.551-1.491-.286-1.648.479a50.067 50.067 0 0 0-.926 6.962ZM4.21 29.705a.988.988 0 0 0 .208 1.1l64.776 64.776c.289.29.726.375 1.1.208a49.908 49.908 0 0 0 5.185-2.684.981.981 0 0 0 .183-1.54L8.436 24.336a.981.981 0 0 0-1.541.183 49.896 49.896 0 0 0-2.684 5.185Zm8.448-11.631a.986.986 0 0 1-.045-1.354C21.78 6.46 35.111 0 49.952 0 77.592 0 100 22.407 100 50.048c0 14.84-6.46 28.172-16.72 37.338a.986.986 0 0 1-1.354-.045L12.659 18.074Z"
|
|
10
|
+
/>
|
|
11
|
+
</LogoSvg>
|
|
12
|
+
);
|
|
13
|
+
}
|