@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.
Files changed (38) hide show
  1. package/README.md +70 -8
  2. package/package.json +23 -16
  3. package/src/brand/index.ts +1 -0
  4. package/src/brand/productApps.ts +20 -0
  5. package/src/client.ts +15 -2
  6. package/src/icons/animated/animated-bell-icon.tsx +68 -0
  7. package/src/icons/animated/animated-counter.tsx +45 -0
  8. package/src/icons/animated/animated-key-icon.tsx +79 -0
  9. package/src/icons/animated/animated-notification-icon.tsx +202 -0
  10. package/src/icons/animated/animated-progress.tsx +74 -0
  11. package/src/icons/animated/animated-status-dot.tsx +98 -0
  12. package/src/icons/animated/index.ts +22 -0
  13. package/src/icons/animated/morphing-menu-icon.tsx +71 -0
  14. package/src/icons/animated/morphing-mic-icon.tsx +52 -0
  15. package/src/icons/animated/pulsing-rings.tsx +50 -0
  16. package/src/index.ts +15 -2
  17. package/src/logos/chrome-icon.tsx +29 -0
  18. package/src/logos/claude-icon.tsx +13 -0
  19. package/src/logos/convex-icon.tsx +21 -0
  20. package/src/logos/cursor-icon.tsx +10 -0
  21. package/src/logos/dropbox-icon.tsx +14 -0
  22. package/src/logos/figma-icon.tsx +26 -0
  23. package/src/logos/github-icon.tsx +10 -0
  24. package/src/logos/gmail-icon.tsx +34 -0
  25. package/src/logos/google-drive-icon.tsx +33 -0
  26. package/src/logos/index.ts +19 -0
  27. package/src/logos/linear-icon.tsx +13 -0
  28. package/src/logos/logo-svg.tsx +32 -0
  29. package/src/logos/notion-icon.tsx +19 -0
  30. package/src/logos/onedrive-icon.tsx +165 -0
  31. package/src/logos/sharepoint-icon.tsx +222 -0
  32. package/src/logos/slack-icon.tsx +25 -0
  33. package/src/logos/vercel-icon.tsx +10 -0
  34. package/src/marketing/SiteFooter.tsx +86 -22
  35. package/src/marketing/landing.tsx +35 -7
  36. package/src/ui/page-title.tsx +29 -0
  37. package/app/README.md +0 -269
  38. package/tokens/README.md +0 -122
@@ -0,0 +1,32 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+
3
+ /** Props shared by every third-party logo. Other SVG props (`id`, `style`, `data-*`, `ref`) go to the svg. */
4
+ export interface LogoProps extends Omit<ComponentProps<"svg">, "children" | "width" | "height" | "viewBox"> {
5
+ /** Width and height in px (or any CSS length). Default `24`. */
6
+ size?: number | string;
7
+ /**
8
+ * Accessible name. With `title` (or an `aria-label`) the svg is a labelled
9
+ * `role="img"`; without one it is decorative and `aria-hidden`.
10
+ */
11
+ title?: string;
12
+ }
13
+
14
+ /** The svg wrapper the logos share: size, viewBox and the decorative or labelled a11y pattern. */
15
+ export function LogoSvg({
16
+ size = 24,
17
+ title,
18
+ viewBox,
19
+ children,
20
+ ...props
21
+ }: LogoProps & { viewBox: string; children: ReactNode }) {
22
+ const label = title ?? props["aria-label"];
23
+ const a11y = label
24
+ ? { role: "img" as const, "aria-label": label }
25
+ : { "aria-hidden": true as const };
26
+ return (
27
+ <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox={viewBox} {...a11y} {...props}>
28
+ {title ? <title>{title}</title> : null}
29
+ {children}
30
+ </svg>
31
+ );
32
+ }
@@ -0,0 +1,19 @@
1
+ import { LogoSvg, type LogoProps } from "./logo-svg";
2
+
3
+ /** Notion mark. */
4
+ export function NotionIcon(props: LogoProps) {
5
+ return (
6
+ <LogoSvg viewBox="0 0 100 100" {...props}>
7
+ <path
8
+ d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z"
9
+ fill="#fff"
10
+ />
11
+ <path
12
+ fillRule="evenodd"
13
+ clipRule="evenodd"
14
+ d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z"
15
+ fill="#000"
16
+ />
17
+ </LogoSvg>
18
+ );
19
+ }
@@ -0,0 +1,165 @@
1
+ import { useId } from "react";
2
+ import { LogoSvg, type LogoProps } from "./logo-svg";
3
+
4
+ /** Microsoft OneDrive mark (svgl `microsoft-onedrive.svg`). Gradient ids are unique per instance. */
5
+ export function OneDriveIcon(props: LogoProps) {
6
+ const id = useId();
7
+ return (
8
+ <LogoSvg viewBox="0 0 1000 615" {...props}>
9
+ <defs>
10
+ <radialGradient
11
+ id={`${id}a`}
12
+ cx="-446.23"
13
+ cy="850.24"
14
+ r="6.99"
15
+ fx="-446.23"
16
+ fy="850.24"
17
+ gradientTransform="matrix(28.87975 32.00675 53.69646 -48.39975 -32750.77 55564.7)"
18
+ gradientUnits="userSpaceOnUse"
19
+ >
20
+ <stop offset="0" stopColor="#4894fe" />
21
+ <stop offset=".7" stopColor="#0934b3" />
22
+ </radialGradient>
23
+ <radialGradient
24
+ id={`${id}b`}
25
+ cx="-463.71"
26
+ cy="855.09"
27
+ r="6.99"
28
+ fx="-463.71"
29
+ fy="855.09"
30
+ gradientTransform="matrix(-126.93754 135.45874 101.23704 94.7798 -144561.83 -18444.24)"
31
+ gradientUnits="userSpaceOnUse"
32
+ >
33
+ <stop offset=".17" stopColor="#23c0fe" />
34
+ <stop offset=".53" stopColor="#1c91ff" />
35
+ </radialGradient>
36
+ <radialGradient
37
+ id={`${id}c`}
38
+ cx="-478.67"
39
+ cy="847.12"
40
+ r="6.99"
41
+ fx="-478.67"
42
+ fy="847.12"
43
+ gradientTransform="matrix(-30.17956 -23.43498 -52.80172 67.93278 30509.91 -68620.88)"
44
+ gradientUnits="userSpaceOnUse"
45
+ >
46
+ <stop offset="0" stopColor="#fff" />
47
+ <stop offset=".66" stopColor="#adc0ff" stopOpacity="0" />
48
+ </radialGradient>
49
+ <radialGradient
50
+ id={`${id}d`}
51
+ cx="-484.89"
52
+ cy="847.31"
53
+ r="6.99"
54
+ fx="-484.89"
55
+ fy="847.31"
56
+ gradientTransform="matrix(-33.90072 -26.53382 -39.69188 50.66325 17714.49 -55348.26)"
57
+ gradientUnits="userSpaceOnUse"
58
+ >
59
+ <stop offset="0" stopColor="#033acc" />
60
+ <stop offset="1" stopColor="#368eff" stopOpacity="0" />
61
+ </radialGradient>
62
+ <radialGradient
63
+ id={`${id}e`}
64
+ cx="-454.42"
65
+ cy="853.18"
66
+ r="6.99"
67
+ fx="-454.42"
68
+ fy="853.18"
69
+ gradientTransform="matrix(38.74213 82.7056 94.03873 -44.01576 -62416.51 75114.97)"
70
+ gradientUnits="userSpaceOnUse"
71
+ >
72
+ <stop offset=".59" stopColor="#3464e3" stopOpacity="0" />
73
+ <stop offset="1" stopColor="#033acc" />
74
+ </radialGradient>
75
+ <radialGradient
76
+ id={`${id}f`}
77
+ cx="-465.3"
78
+ cy="852.63"
79
+ r="6.99"
80
+ fx="-465.3"
81
+ fy="852.63"
82
+ gradientTransform="matrix(-101.35519 93.7574 146.5162 158.24743 -171232.53 -91444.13)"
83
+ gradientUnits="userSpaceOnUse"
84
+ >
85
+ <stop offset="0" stopColor="#4bfde8" />
86
+ <stop offset=".54" stopColor="#4bfde8" stopOpacity="0" />
87
+ </radialGradient>
88
+ <radialGradient
89
+ id={`${id}h`}
90
+ cx="-445.42"
91
+ cy="847.35"
92
+ r="6.99"
93
+ fx="-445.42"
94
+ fy="847.35"
95
+ gradientTransform="matrix(60.3777 22.14291 39.59688 -107.87213 -6264.92 101508.79)"
96
+ gradientUnits="userSpaceOnUse"
97
+ >
98
+ <stop offset="0" stopColor="#fff" />
99
+ <stop offset=".79" stopColor="#fff" stopOpacity="0" />
100
+ </radialGradient>
101
+ <radialGradient
102
+ id={`${id}i`}
103
+ cx="-468.67"
104
+ cy="861.39"
105
+ r="6.99"
106
+ fx="-468.67"
107
+ fy="861.39"
108
+ gradientTransform="matrix(-67.45933 53.77501 53.21816 66.68832 -76468.45 -32083.78)"
109
+ gradientUnits="userSpaceOnUse"
110
+ >
111
+ <stop offset="0" stopColor="#4bfde8" />
112
+ <stop offset=".58" stopColor="#4bfde8" stopOpacity="0" />
113
+ </radialGradient>
114
+ <linearGradient
115
+ id={`${id}g`}
116
+ x1="638.67"
117
+ x2="638.67"
118
+ y1="2.44"
119
+ y2="421.76"
120
+ gradientTransform="matrix(1 0 0 -1 0 617.01)"
121
+ gradientUnits="userSpaceOnUse"
122
+ >
123
+ <stop offset="0" stopColor="#0086ff" />
124
+ <stop offset=".49" stopColor="#0bf" />
125
+ </linearGradient>
126
+ </defs>
127
+ <path
128
+ d="M276.36 94.08C123.48 94.08 9.21 209.84.6 338.79c5.33 27.79 22.83 82.65 50.24 79.84 34.26-3.52 120.56 0 194.17-123.26 53.77-90.04 164.37-201.29 31.35-201.29Z"
129
+ style={{ fill: `url(#${id}a)` }}
130
+ />
131
+ <path
132
+ d="M240.99 142.19c-51.39 75.26-120.56 183.1-143.91 217.03-27.75 40.34-101.25 23.2-95.16-34.62a237.4 237.4 0 0 0-1.38 14.19C-9.51 489.22 119.43 614.14 279.88 614.14c176.84 0 598.58-203.81 555.9-408.02C790.8 86.1 664.36 0 521.07 0S285.94 76.36 241 142.19Z"
133
+ style={{ fill: `url(#${id}b)` }}
134
+ />
135
+ <path
136
+ d="M240.99 142.19c-51.39 75.26-120.56 183.1-143.91 217.03-27.75 40.34-101.25 23.2-95.16-34.62a237.4 237.4 0 0 0-1.38 14.19C-9.51 489.22 119.43 614.14 279.88 614.14c176.84 0 598.58-203.81 555.9-408.02C790.8 86.1 664.36 0 521.07 0S285.94 76.36 241 142.19Z"
137
+ style={{ fillOpacity: 0.4, fill: `url(#${id}c)` }}
138
+ />
139
+ <path
140
+ d="M240.99 142.19c-51.39 75.26-120.56 183.1-143.91 217.03-27.75 40.34-101.25 23.2-95.16-34.62a237.4 237.4 0 0 0-1.38 14.19C-9.51 489.22 119.43 614.14 279.88 614.14c176.84 0 598.58-203.81 555.9-408.02C790.8 86.1 664.36 0 521.07 0S285.94 76.36 241 142.19Z"
141
+ style={{ fill: `url(#${id}d)` }}
142
+ />
143
+ <path
144
+ d="M240.99 142.19c-51.39 75.26-120.56 183.1-143.91 217.03-27.75 40.34-101.25 23.2-95.16-34.62a237.4 237.4 0 0 0-1.38 14.19C-9.51 489.22 119.43 614.14 279.88 614.14c176.84 0 598.58-203.81 555.9-408.02C790.8 86.1 664.36 0 521.07 0S285.94 76.36 241 142.19Z"
145
+ style={{ fill: `url(#${id}e)`, fillOpacity: 0.6 }}
146
+ />
147
+ <path
148
+ d="M240.99 142.19c-51.39 75.26-120.56 183.1-143.91 217.03-27.75 40.34-101.25 23.2-95.16-34.62a237.4 237.4 0 0 0-1.38 14.19C-9.51 489.22 119.43 614.14 279.88 614.14c176.84 0 598.58-203.81 555.9-408.02C790.8 86.1 664.36 0 521.07 0S285.94 76.36 241 142.19Z"
149
+ style={{ fillOpacity: 0.9, fill: `url(#${id}f)` }}
150
+ />
151
+ <path
152
+ d="M277.34 614.23s422.24.77 493.86.77c129.97 0 228.8-98.16 228.8-212.69s-100.8-212.1-228.8-212.1-201.7 88.57-257.06 185.25c-64.87 113.29-147.62 237.41-236.8 238.77Z"
153
+ style={{ fill: `url(#${id}g)` }}
154
+ />
155
+ <path
156
+ d="M277.34 614.23s422.24.77 493.86.77c129.97 0 228.8-98.16 228.8-212.69s-100.8-212.1-228.8-212.1-201.7 88.57-257.06 185.25c-64.87 113.29-147.62 237.41-236.8 238.77Z"
157
+ style={{ fill: `url(#${id}h)`, fillOpacity: 0.4 }}
158
+ />
159
+ <path
160
+ d="M277.34 614.23s422.24.77 493.86.77c129.97 0 228.8-98.16 228.8-212.69s-100.8-212.1-228.8-212.1-201.7 88.57-257.06 185.25c-64.87 113.29-147.62 237.41-236.8 238.77Z"
161
+ style={{ fill: `url(#${id}i)`, fillOpacity: 0.9 }}
162
+ />
163
+ </LogoSvg>
164
+ );
165
+ }
@@ -0,0 +1,222 @@
1
+ import { useId } from "react";
2
+ import { LogoSvg, type LogoProps } from "./logo-svg";
3
+
4
+ /** Microsoft SharePoint mark. Gradient ids are unique per instance. */
5
+ export function SharePointIcon(props: LogoProps) {
6
+ const id = useId();
7
+ return (
8
+ <LogoSvg viewBox="0 0 450 500" {...props}>
9
+ <defs>
10
+ <radialGradient
11
+ id={`${id}b`}
12
+ cx="-777.14"
13
+ cy="770.84"
14
+ r=".9"
15
+ fx="-777.14"
16
+ fy="770.84"
17
+ gradientTransform="matrix(-98.02204 -236.87999 -426.55553 175.97802 252868.13 -319393.72)"
18
+ gradientUnits="userSpaceOnUse"
19
+ >
20
+ <stop offset=".29" stopColor="#003b5d" />
21
+ <stop offset=".61" stopColor="#004a6c" stopOpacity=".69" />
22
+ <stop offset=".97" stopColor="#006f94" stopOpacity="0" />
23
+ </radialGradient>
24
+ <radialGradient
25
+ id={`${id}c`}
26
+ cx="-785.23"
27
+ cy="766.9"
28
+ r=".9"
29
+ fx="-785.23"
30
+ fy="766.9"
31
+ gradientTransform="matrix(-81.36865 -200.4872 -360.58286 145.8959 212855.73 -269057.38)"
32
+ gradientUnits="userSpaceOnUse"
33
+ >
34
+ <stop offset=".26" stopColor="#002a42" />
35
+ <stop offset=".61" stopColor="#004261" stopOpacity=".69" />
36
+ <stop offset=".97" stopColor="#006f94" stopOpacity="0" />
37
+ </radialGradient>
38
+ <radialGradient
39
+ id={`${id}d`}
40
+ cx="-739.84"
41
+ cy="872.71"
42
+ r=".9"
43
+ fx="-739.84"
44
+ fy="872.71"
45
+ gradientTransform="matrix(-102.53313 149.24243 149.46801 102.38138 -206024.5 21094.22)"
46
+ gradientUnits="userSpaceOnUse"
47
+ >
48
+ <stop offset="0" stopColor="#78edff" />
49
+ <stop offset="1" stopColor="#2ccfca" stopOpacity="0" />
50
+ </radialGradient>
51
+ <radialGradient
52
+ id={`${id}f`}
53
+ cx="-746.38"
54
+ cy="743.75"
55
+ r=".9"
56
+ fx="-746.38"
57
+ fy="743.75"
58
+ gradientTransform="matrix(56.60729 -162.28035 -292.83982 -101.83188 260296.26 -45019.25)"
59
+ gradientUnits="userSpaceOnUse"
60
+ >
61
+ <stop offset="0" stopColor="#003b5d" />
62
+ <stop offset=".49" stopColor="#004c6c" stopOpacity=".72" />
63
+ <stop offset=".97" stopColor="#007a86" stopOpacity="0" />
64
+ </radialGradient>
65
+ <radialGradient
66
+ id={`${id}g`}
67
+ cx="-741.46"
68
+ cy="888.91"
69
+ r=".9"
70
+ fx="-741.46"
71
+ fy="888.91"
72
+ gradientTransform="matrix(-85.44333 124.36732 124.55255 85.31498 -173675.69 16546.9)"
73
+ gradientUnits="userSpaceOnUse"
74
+ >
75
+ <stop offset="0" stopColor="#78edff" />
76
+ <stop offset="1" stopColor="#2ccfca" stopOpacity="0" />
77
+ </radialGradient>
78
+ <radialGradient
79
+ id={`${id}j`}
80
+ cx="-686.6"
81
+ cy="804.18"
82
+ r=".9"
83
+ fx="-686.6"
84
+ fy="804.18"
85
+ gradientTransform="matrix(222.887 222.57601 222.92613 -222.53738 -26208.17 332025.54)"
86
+ gradientUnits="userSpaceOnUse"
87
+ >
88
+ <stop offset=".06" stopColor="#00b6bd" />
89
+ <stop offset=".89" stopColor="#00495c" />
90
+ </radialGradient>
91
+ <radialGradient
92
+ id={`${id}k`}
93
+ cx="-686.07"
94
+ cy="864.41"
95
+ r=".9"
96
+ fx="-686.07"
97
+ fy="864.41"
98
+ gradientTransform="matrix(0 155.79 177.62 0 -153439.14 107241.64)"
99
+ gradientUnits="userSpaceOnUse"
100
+ >
101
+ <stop offset=".57" stopColor="#1e8581" stopOpacity="0" />
102
+ <stop offset=".97" stopColor="#1ecbe6" />
103
+ </radialGradient>
104
+ <linearGradient
105
+ id={`${id}a`}
106
+ x1="92.18"
107
+ x2="292.29"
108
+ y1="477.48"
109
+ y2="214.84"
110
+ gradientTransform="matrix(1 0 0 -1 0 502)"
111
+ gradientUnits="userSpaceOnUse"
112
+ >
113
+ <stop offset="0" stopColor="#00e3df" />
114
+ <stop offset=".41" stopColor="#0097a8" />
115
+ <stop offset="1" stopColor="#007791" />
116
+ </linearGradient>
117
+ <linearGradient
118
+ id={`${id}e`}
119
+ x1="245.14"
120
+ x2="411.9"
121
+ y1="331.57"
122
+ y2="112.7"
123
+ gradientTransform="matrix(1 0 0 -1 0 502)"
124
+ gradientUnits="userSpaceOnUse"
125
+ >
126
+ <stop offset="0" stopColor="#00e3df" />
127
+ <stop offset=".48" stopColor="#00a2b8" />
128
+ <stop offset=".95" stopColor="#00637c" />
129
+ </linearGradient>
130
+ <linearGradient
131
+ id={`${id}h`}
132
+ x1="153.42"
133
+ x2="259.71"
134
+ y1="214.53"
135
+ y2="1.97"
136
+ gradientTransform="matrix(1 0 0 -1 0 502)"
137
+ gradientUnits="userSpaceOnUse"
138
+ >
139
+ <stop offset=".05" stopColor="#75fff6" />
140
+ <stop offset=".51" stopColor="#00c7d1" />
141
+ <stop offset=".96" stopColor="#0096ad" />
142
+ </linearGradient>
143
+ <linearGradient
144
+ id={`${id}i`}
145
+ x1="292.44"
146
+ x2="235.63"
147
+ y1="-12.27"
148
+ y2="67.45"
149
+ gradientTransform="matrix(1 0 0 -1 0 502)"
150
+ gradientUnits="userSpaceOnUse"
151
+ >
152
+ <stop offset=".26" stopColor="#0e5a5d" />
153
+ <stop offset=".54" stopColor="#126c6b" stopOpacity=".69" />
154
+ <stop offset=".97" stopColor="#1c948a" stopOpacity="0" />
155
+ </linearGradient>
156
+ </defs>
157
+ <path
158
+ d="M187.79 300c82.97 0 150.23-67.16 150.23-150S270.75 0 187.79 0 37.56 67.16 37.56 150s67.26 150 150.23 150Z"
159
+ fill={`url(#${id}a)`}
160
+ />
161
+ <path
162
+ d="M187.79 300c82.97 0 150.23-67.16 150.23-150S270.75 0 187.79 0 37.56 67.16 37.56 150s67.26 150 150.23 150Z"
163
+ fill={`url(#${id}b)`}
164
+ fillOpacity=".2"
165
+ />
166
+ <path
167
+ d="M187.79 300c82.97 0 150.23-67.16 150.23-150S270.75 0 187.79 0 37.56 67.16 37.56 150s67.26 150 150.23 150Z"
168
+ fill={`url(#${id}c)`}
169
+ fillOpacity=".31"
170
+ />
171
+ <path
172
+ d="M187.79 300c82.97 0 150.23-67.16 150.23-150S270.75 0 187.79 0 37.56 67.16 37.56 150s67.26 150 150.23 150Z"
173
+ fill={`url(#${id}d)`}
174
+ fillOpacity=".7"
175
+ />
176
+ <path
177
+ d="M324.81 400C393.95 400 450 344.04 450 275s-56.05-125-125.19-125-125.19 55.96-125.19 125 56.05 125 125.19 125Z"
178
+ fill={`url(#${id}e)`}
179
+ />
180
+ <path
181
+ d="M324.81 400C393.95 400 450 344.04 450 275s-56.05-125-125.19-125-125.19 55.96-125.19 125 56.05 125 125.19 125Z"
182
+ fill={`url(#${id}f)`}
183
+ fillOpacity=".5"
184
+ />
185
+ <path
186
+ d="M324.81 400C393.95 400 450 344.04 450 275s-56.05-125-125.19-125-125.19 55.96-125.19 125 56.05 125 125.19 125Z"
187
+ fill={`url(#${id}g)`}
188
+ fillOpacity=".7"
189
+ />
190
+ <path
191
+ d="M206.56 500c58.77 0 106.41-47.57 106.41-106.25S265.33 287.5 206.56 287.5s-106.41 47.57-106.41 106.25S147.79 500 206.56 500Z"
192
+ fill={`url(#${id}h)`}
193
+ />
194
+ <path
195
+ d="M206.56 500c58.77 0 106.41-47.57 106.41-106.25S265.33 287.5 206.56 287.5s-106.41 47.57-106.41 106.25S147.79 500 206.56 500Z"
196
+ fill={`url(#${id}i)`}
197
+ fillOpacity=".32"
198
+ />
199
+ <rect
200
+ width="200.3"
201
+ height="200"
202
+ y="237.5"
203
+ rx="40.66"
204
+ ry="40.66"
205
+ fill={`url(#${id}j)`}
206
+ />
207
+ <rect
208
+ width="200.3"
209
+ height="200"
210
+ y="237.5"
211
+ rx="40.66"
212
+ ry="40.66"
213
+ fill={`url(#${id}k)`}
214
+ fillOpacity=".6"
215
+ />
216
+ <path
217
+ d="m57 372.15 21.72-11.32c2.45 4.94 5.64 8.58 9.58 10.92 3.99 2.34 8.36 3.51 13.09 3.51 5.27 0 9.29-1.06 12.05-3.19 2.77-2.18 4.15-5.45 4.15-9.8 0-3.4-1.33-6.27-3.99-8.61-2.66-2.39-7.37-4.2-14.13-5.42-12.88-2.34-22.25-6.43-28.1-12.28-5.8-5.85-8.7-13.13-8.7-21.84 0-10.84 3.83-19.5 11.5-25.99 7.66-6.48 17.78-9.72 30.34-9.72 8.46 0 15.91 1.73 22.35 5.18 6.44 3.45 11.55 8.4 15.33 14.83l-21.23 10.92c-2.34-3.61-4.87-6.22-7.58-7.81-2.72-1.65-6.12-2.47-10.22-2.47-4.9 0-8.62 1.06-11.18 3.19-2.5 2.13-3.75 4.89-3.75 8.29 0 2.92 1.2 5.5 3.59 7.73 2.45 2.18 7.34 3.96 14.69 5.34 12.35 2.34 21.56 6.59 27.62 12.76 6.12 6.11 9.18 13.84 9.18 23.2 0 11.37-3.65 20.38-10.94 27.02-7.29 6.64-17.7 9.96-31.21 9.96-9.79 0-18.63-2.13-26.5-6.38-7.82-4.3-13.7-10.31-17.64-18.02Z"
218
+ fill="#fff"
219
+ />
220
+ </LogoSvg>
221
+ );
222
+ }
@@ -0,0 +1,25 @@
1
+ import { LogoSvg, type LogoProps } from "./logo-svg";
2
+
3
+ /** Slack mark. */
4
+ export function SlackIcon(props: LogoProps) {
5
+ return (
6
+ <LogoSvg viewBox="0 0 24 24" {...props}>
7
+ <path
8
+ d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z"
9
+ fill="#E01E5A"
10
+ />
11
+ <path
12
+ d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z"
13
+ fill="#36C5F0"
14
+ />
15
+ <path
16
+ d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312z"
17
+ fill="#2EB67D"
18
+ />
19
+ <path
20
+ d="M15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"
21
+ fill="#ECB22E"
22
+ />
23
+ </LogoSvg>
24
+ );
25
+ }
@@ -0,0 +1,10 @@
1
+ import { LogoSvg, type LogoProps } from "./logo-svg";
2
+
3
+ /** Vercel mark (svgl). Monochrome: uses `currentColor`, so set the colour with a text class. */
4
+ export function VercelIcon(props: LogoProps) {
5
+ return (
6
+ <LogoSvg viewBox="0 0 24 24" fill="currentColor" {...props}>
7
+ <path d="M24 22.525H0l12-21.05 12 21.05z" />
8
+ </LogoSvg>
9
+ );
10
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType, ReactNode } from "react";
2
- import { APP_ICONS, type AppName } from "../brand";
2
+ import { IconBrandGithub, IconBrandInstagram, IconBrandX } from "@tabler/icons-react";
3
+ import { APP_ICONS, AppWordmark, projectVAppsColumn, type AppName, type ProductAppName } from "../brand";
3
4
  import { Button } from "../ui/button";
4
5
  import { Separator } from "../ui/separator";
5
6
  import { siteFooterClasses, type SiteFooterVariant } from "./marketing-classes";
@@ -23,47 +24,100 @@ export type SiteFooterSocial = {
23
24
  label?: string;
24
25
  };
25
26
 
26
- export type SiteFooterProps = {
27
- /** Wordmark text above the tagline. */
28
- brand: ReactNode;
27
+ /** Text size in px of the built-in `AppWordmark` brand. Fixed: `SiteFooter` has no size prop. */
28
+ export const SITE_FOOTER_WORDMARK_SIZE = 16;
29
+
30
+ /** Default `socials` on the `app` path: X, Instagram and GitHub. */
31
+ export const PROJECT_V_SOCIALS: readonly SiteFooterSocial[] = [
32
+ { href: "https://x.com/projectvdotuk", icon: IconBrandX, label: "Project V on X" },
33
+ { href: "https://www.instagram.com/projectvdotuk/", icon: IconBrandInstagram, label: "Project V on Instagram" },
34
+ { href: "https://github.com/projectvdotuk", icon: IconBrandGithub, label: "Project V on GitHub" },
35
+ ];
36
+
37
+ /** Default `legalLinks` on the `app` path. */
38
+ export const PROJECT_V_LEGAL_LINKS: readonly { href: string; label: string }[] = [
39
+ { href: "https://projectv.uk/privacy", label: "Privacy Policy" },
40
+ { href: "https://projectv.uk/terms", label: "Terms of Service" },
41
+ ];
42
+
43
+ export type SiteFooterColumn = { heading: string; links: readonly SiteFooterLink[] };
44
+
45
+ type SiteFooterCommonProps = {
29
46
  tagline: ReactNode;
30
- /** Icon buttons under the tagline. Always open in a new tab. */
31
- socials: readonly SiteFooterSocial[];
32
47
  /** Link columns to the right of the brand block. */
33
- columns: readonly { heading: string; links: readonly SiteFooterLink[] }[];
34
- /** Name in "Copyright © {year} {name}". */
35
- name: string;
48
+ columns: readonly SiteFooterColumn[];
36
49
  /** Default: the current year. */
37
50
  year?: number;
38
- /** Bottom-right links, for example Privacy Policy and Terms of Service. Always open in a new tab. */
39
- legalLinks: readonly { href: string; label: string }[];
40
51
  /**
41
52
  * `"muted"` (default) and `"muted-foreground"` pick the secondary text
42
53
  * colour class. `"touch"` uses `text-muted-foreground`, `max-sm:hit-target`
43
54
  * links and a safe-area bottom padding.
44
55
  */
45
56
  variant?: SiteFooterVariant;
57
+ /** Under the socials, for example a Sign-in button. Not rendered on the legacy path. */
58
+ actions?: ReactNode;
46
59
  };
47
60
 
61
+ /** Opinionated footer: set `app` and the Project V brand, socials, legal links, Apps column and product-of line follow. */
62
+ export type SiteFooterAppProps = SiteFooterCommonProps & {
63
+ /** Product app id. Sets the wordmark (size 16), the copyright name and the product-of line. */
64
+ app: ProductAppName;
65
+ /** Replaces the `AppWordmark`. */
66
+ brand?: ReactNode;
67
+ /** Default `PROJECT_V_SOCIALS`. `[]` hides them. */
68
+ socials?: readonly SiteFooterSocial[];
69
+ /** Default `PROJECT_V_LEGAL_LINKS`. */
70
+ legalLinks?: readonly { href: string; label: string }[];
71
+ /** Overrides the copyright name (default: `app`). */
72
+ name?: string;
73
+ /** Appends the standard Apps column unless `columns` has an "Apps" heading (default `true`). */
74
+ includeAppsColumn?: boolean;
75
+ /** Shows "{app} is a product of Project V." (default `true`). */
76
+ showProductOf?: boolean;
77
+ };
78
+
79
+ /** Legacy footer: you pass every part. Prefer `SiteFooterAppProps`. */
80
+ export type SiteFooterLegacyProps = SiteFooterCommonProps & {
81
+ app?: undefined;
82
+ /**
83
+ * @deprecated Set `app` instead: the footer renders `AppWordmark`. Without `app` the brand sits in the Michroma wrapper.
84
+ */
85
+ brand: ReactNode;
86
+ /** @deprecated Set `app` for the Project V defaults (`PROJECT_V_SOCIALS`). Icon buttons under the tagline; always open in a new tab. */
87
+ socials: readonly SiteFooterSocial[];
88
+ /** @deprecated Set `app`: the copyright name is then the app id. Name in "Copyright © {year} {name}". */
89
+ name: string;
90
+ /** @deprecated Set `app` for `PROJECT_V_LEGAL_LINKS`. Bottom-right links; always open in a new tab. */
91
+ legalLinks: readonly { href: string; label: string }[];
92
+ includeAppsColumn?: undefined;
93
+ showProductOf?: undefined;
94
+ };
95
+
96
+ export type SiteFooterProps = SiteFooterAppProps | SiteFooterLegacyProps;
97
+
48
98
  /** Marketing site footer: brand, socials, link columns, then copyright and legal links under a separator. */
49
- export function SiteFooter({
50
- brand,
51
- tagline,
52
- socials,
53
- columns,
54
- name,
55
- year = new Date().getFullYear(),
56
- legalLinks,
57
- variant = "muted",
58
- }: SiteFooterProps) {
99
+ export function SiteFooter(props: SiteFooterProps) {
100
+ const { tagline, year = new Date().getFullYear(), variant = "muted", actions } = props;
59
101
  const c = siteFooterClasses[variant];
102
+ const modern = props.app !== undefined;
103
+ const brandNode = modern ? (
104
+ <div className="mb-3 md:mb-4">{props.brand ?? <AppWordmark app={props.app} size={SITE_FOOTER_WORDMARK_SIZE} />}</div>
105
+ ) : (
106
+ <div className="text-lg font-michroma font-normal tracking-wider mb-3 md:mb-4">{props.brand}</div>
107
+ );
108
+ const socials = modern ? (props.socials ?? PROJECT_V_SOCIALS) : props.socials;
109
+ const legalLinks = modern ? (props.legalLinks ?? PROJECT_V_LEGAL_LINKS) : props.legalLinks;
110
+ const name = modern ? (props.name ?? props.app) : props.name;
111
+ const hasApps = props.columns.some((column) => column.heading.toLowerCase() === "apps");
112
+ const columns: readonly SiteFooterColumn[] = modern && (props.includeAppsColumn ?? true) && !hasApps ? [...props.columns, projectVAppsColumn()] : props.columns;
113
+ const showProductOf = modern && (props.showProductOf ?? true);
60
114
  return (
61
115
  <footer className={c.footer}>
62
116
  <div className="relative z-10">
63
117
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
64
118
  <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-8 lg:gap-12">
65
119
  <div className="flex-1 lg:flex-none">
66
- <div className="text-lg font-michroma font-normal tracking-wider mb-3 md:mb-4">{brand}</div>
120
+ {brandNode}
67
121
  <p className={c.tagline}>{tagline}</p>
68
122
  <div className="flex gap-3 md:gap-4">
69
123
  {socials.map(({ href, icon: Icon, label }) => (
@@ -74,6 +128,7 @@ export function SiteFooter({
74
128
  </a>
75
129
  ))}
76
130
  </div>
131
+ {modern && actions ? <div className="mt-3 md:mt-4">{actions}</div> : null}
77
132
  </div>
78
133
 
79
134
  <div className="flex flex-col sm:flex-row gap-8 md:gap-12 lg:gap-16 xl:gap-20">
@@ -120,6 +175,15 @@ export function SiteFooter({
120
175
  {` ${name}`}
121
176
  </p>
122
177
  <p className={c.rights}>All rights reserved.</p>
178
+ {showProductOf ? (
179
+ <p className={c.rights}>
180
+ {props.app} is a product of{" "}
181
+ <a href="https://projectv.uk" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">
182
+ Project V
183
+ </a>
184
+ .
185
+ </p>
186
+ ) : null}
123
187
  </div>
124
188
 
125
189
  <div className="flex flex-col gap-1 text-center sm:text-right">