@lukoweb/apitogo-module-landing 0.1.55 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/DefaultLandingPage.tsx +23 -17
- package/src/landing.css +288 -0
- package/src/sections/ApiShowcaseSection.tsx +17 -21
- package/src/sections/CodeExampleCard.tsx +17 -8
- package/src/sections/FeaturesSection.tsx +23 -19
- package/src/sections/HeroSection.tsx +53 -30
- package/src/sections/LandingButtons.tsx +47 -0
- package/src/sections/LandingIcon.tsx +4 -0
- package/src/sections/PricingCtaSection.tsx +16 -17
- package/src/sections/TickerBar.tsx +14 -13
- package/src/sections/TrustSection.tsx +4 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lukoweb/apitogo-module-landing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.tsx"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/react": "19.2.14",
|
|
16
16
|
"@types/react-dom": "19.2.3",
|
|
17
|
-
"@lukoweb/apitogo": "0.1.
|
|
17
|
+
"@lukoweb/apitogo": "0.1.56",
|
|
18
18
|
"react": "19.2.4",
|
|
19
19
|
"react-dom": "19.2.4",
|
|
20
20
|
"typescript": "5.9.3"
|
|
@@ -5,8 +5,8 @@ import { ApiShowcaseSection } from "./sections/ApiShowcaseSection.js";
|
|
|
5
5
|
import { FeaturesSection } from "./sections/FeaturesSection.js";
|
|
6
6
|
import { HeroSection } from "./sections/HeroSection.js";
|
|
7
7
|
import { PricingCtaSection } from "./sections/PricingCtaSection.js";
|
|
8
|
-
import { TickerBar } from "./sections/TickerBar.js";
|
|
9
8
|
import { TrustSection } from "./sections/TrustSection.js";
|
|
9
|
+
import "./landing.css";
|
|
10
10
|
|
|
11
11
|
export type DefaultLandingPageProps = {
|
|
12
12
|
content: ResolvedLandingContent;
|
|
@@ -17,35 +17,41 @@ export const DefaultLandingPage = ({ content }: DefaultLandingPageProps) => {
|
|
|
17
17
|
const siteTitle =
|
|
18
18
|
options.metadata?.applicationName ?? options.site?.title ?? "APIToGo";
|
|
19
19
|
|
|
20
|
-
const hero = content.hero ?? {
|
|
21
|
-
title: siteTitle,
|
|
22
|
-
description:
|
|
23
|
-
options.metadata?.description ??
|
|
24
|
-
"Beautiful API documentation and developer portals powered by APIToGo.",
|
|
25
|
-
};
|
|
26
|
-
|
|
20
|
+
const hero = content.hero ?? { title: siteTitle };
|
|
27
21
|
const title = hero.title ?? siteTitle;
|
|
28
|
-
const description =
|
|
29
|
-
hero.description ??
|
|
30
|
-
options.metadata?.description ??
|
|
31
|
-
"Beautiful API documentation and developer portals powered by APIToGo.";
|
|
22
|
+
const description = hero.description;
|
|
32
23
|
|
|
33
24
|
return (
|
|
34
|
-
<div className="relative overflow-hidden">
|
|
25
|
+
<div className="apitogo-landing relative overflow-hidden">
|
|
35
26
|
<Head>
|
|
27
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
28
|
+
<link
|
|
29
|
+
rel="preconnect"
|
|
30
|
+
href="https://fonts.gstatic.com"
|
|
31
|
+
crossOrigin=""
|
|
32
|
+
/>
|
|
33
|
+
<link
|
|
34
|
+
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
35
|
+
rel="stylesheet"
|
|
36
|
+
/>
|
|
36
37
|
<title>{title}</title>
|
|
37
38
|
{description ? <meta name="description" content={description} /> : null}
|
|
38
39
|
</Head>
|
|
39
40
|
|
|
40
41
|
<HeroSection
|
|
41
|
-
hero={{ ...hero, title
|
|
42
|
+
hero={{ ...hero, title }}
|
|
42
43
|
primaryAction={content.primaryAction}
|
|
43
44
|
secondaryAction={content.secondaryAction}
|
|
45
|
+
ticker={content.ticker}
|
|
44
46
|
/>
|
|
45
47
|
|
|
46
|
-
{content.ticker ? <TickerBar ticker={content.ticker} /> : null}
|
|
47
48
|
{content.trust ? <TrustSection trust={content.trust} /> : null}
|
|
48
|
-
{content.features ?
|
|
49
|
+
{content.features ? (
|
|
50
|
+
<FeaturesSection
|
|
51
|
+
features={content.features}
|
|
52
|
+
section={content.featuresSection}
|
|
53
|
+
/>
|
|
54
|
+
) : null}
|
|
49
55
|
{content.apiShowcase ? (
|
|
50
56
|
<ApiShowcaseSection apiShowcase={content.apiShowcase} />
|
|
51
57
|
) : null}
|
|
@@ -56,7 +62,7 @@ export const DefaultLandingPage = ({ content }: DefaultLandingPageProps) => {
|
|
|
56
62
|
{content.showPoweredBy !== false ? (
|
|
57
63
|
<div
|
|
58
64
|
className={cn(
|
|
59
|
-
"
|
|
65
|
+
"landing-powered-by py-6 text-center text-xs",
|
|
60
66
|
!options.site?.footer && "mb-8",
|
|
61
67
|
)}
|
|
62
68
|
>
|
package/src/landing.css
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
.apitogo-landing {
|
|
2
|
+
--landing-display: "Space Grotesk", var(--font-sans, system-ui), sans-serif;
|
|
3
|
+
--landing-body: "IBM Plex Sans", var(--font-sans, system-ui), sans-serif;
|
|
4
|
+
--landing-mono: "JetBrains Mono", var(--font-mono, ui-monospace), monospace;
|
|
5
|
+
--landing-bg: #0b0d12;
|
|
6
|
+
--landing-bg-2: #0e1119;
|
|
7
|
+
--landing-surface: #14171f;
|
|
8
|
+
--landing-surface-2: #191d27;
|
|
9
|
+
--landing-border: #252b37;
|
|
10
|
+
--landing-border-2: #333a48;
|
|
11
|
+
--landing-text: #edeff5;
|
|
12
|
+
--landing-text-2: #a6aebf;
|
|
13
|
+
--landing-text-3: #6b7385;
|
|
14
|
+
--landing-accent: #f23645;
|
|
15
|
+
--landing-accent-hover: #ff515d;
|
|
16
|
+
--landing-accent-soft: rgba(242, 54, 69, 0.13);
|
|
17
|
+
--landing-on-accent: #fff;
|
|
18
|
+
--landing-green: #16c784;
|
|
19
|
+
--landing-green-soft: rgba(22, 199, 132, 0.13);
|
|
20
|
+
--landing-peri: #aeb9e8;
|
|
21
|
+
--landing-code-bg: #0c0f16;
|
|
22
|
+
--landing-shadow:
|
|
23
|
+
0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
|
|
24
|
+
background: var(--landing-bg);
|
|
25
|
+
color: var(--landing-text);
|
|
26
|
+
font-family: var(--landing-body);
|
|
27
|
+
-webkit-font-smoothing: antialiased;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:not(.dark) .apitogo-landing {
|
|
31
|
+
--landing-bg: #f5f6f9;
|
|
32
|
+
--landing-bg-2: #ffffff;
|
|
33
|
+
--landing-surface: #ffffff;
|
|
34
|
+
--landing-surface-2: #fafbfc;
|
|
35
|
+
--landing-border: #e6e8ef;
|
|
36
|
+
--landing-border-2: #d6dae3;
|
|
37
|
+
--landing-text: #0e1016;
|
|
38
|
+
--landing-text-2: #565e70;
|
|
39
|
+
--landing-text-3: #8a92a3;
|
|
40
|
+
--landing-accent: #e11d2a;
|
|
41
|
+
--landing-accent-hover: #c2151f;
|
|
42
|
+
--landing-accent-soft: rgba(225, 29, 42, 0.08);
|
|
43
|
+
--landing-green: #0e9e63;
|
|
44
|
+
--landing-green-soft: rgba(14, 158, 99, 0.1);
|
|
45
|
+
--landing-peri: #4e5ac0;
|
|
46
|
+
--landing-shadow:
|
|
47
|
+
0 1px 2px rgba(16, 24, 40, 0.05), 0 8px 24px -10px rgba(16, 24, 40, 0.14);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@keyframes landing-fu {
|
|
51
|
+
from {
|
|
52
|
+
opacity: 0;
|
|
53
|
+
transform: translateY(10px);
|
|
54
|
+
}
|
|
55
|
+
to {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
transform: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@keyframes landing-ticker {
|
|
62
|
+
from {
|
|
63
|
+
transform: translateX(0);
|
|
64
|
+
}
|
|
65
|
+
to {
|
|
66
|
+
transform: translateX(-50%);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.apitogo-landing .landing-display {
|
|
71
|
+
font-family: var(--landing-display);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.apitogo-landing .landing-mono {
|
|
75
|
+
font-family: var(--landing-mono);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.apitogo-landing .landing-fu {
|
|
79
|
+
animation: landing-fu 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.apitogo-landing .landing-section-label {
|
|
83
|
+
color: var(--landing-accent);
|
|
84
|
+
font-family: var(--landing-mono);
|
|
85
|
+
font-size: 0.8125rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
margin-bottom: 0.75rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.apitogo-landing .landing-muted {
|
|
91
|
+
color: var(--landing-text-2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.apitogo-landing .landing-muted-3 {
|
|
95
|
+
color: var(--landing-text-3);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.apitogo-landing .landing-btn-primary {
|
|
99
|
+
align-items: center;
|
|
100
|
+
background: var(--landing-accent);
|
|
101
|
+
border: none;
|
|
102
|
+
border-radius: 11px;
|
|
103
|
+
box-shadow: 0 8px 24px -8px var(--landing-accent-soft);
|
|
104
|
+
color: var(--landing-on-accent);
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
font-size: 0.96875rem;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
gap: 0.5rem;
|
|
111
|
+
padding: 0.8125rem 1.375rem;
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
transition: background 0.15s ease;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.apitogo-landing .landing-btn-primary:hover {
|
|
117
|
+
background: var(--landing-accent-hover);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.apitogo-landing .landing-btn-secondary {
|
|
121
|
+
align-items: center;
|
|
122
|
+
background: var(--landing-surface);
|
|
123
|
+
border: 1px solid var(--landing-border);
|
|
124
|
+
border-radius: 11px;
|
|
125
|
+
color: var(--landing-text);
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
font-family: inherit;
|
|
129
|
+
font-size: 0.96875rem;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
gap: 0.5rem;
|
|
132
|
+
padding: 0.8125rem 1.375rem;
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
transition:
|
|
135
|
+
border-color 0.15s ease,
|
|
136
|
+
background 0.15s ease;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.apitogo-landing .landing-btn-secondary:hover {
|
|
140
|
+
border-color: var(--landing-border-2);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.apitogo-landing .landing-btn-sm {
|
|
144
|
+
border-radius: 9px;
|
|
145
|
+
font-size: 0.875rem;
|
|
146
|
+
padding: 0.625rem 1rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.apitogo-landing .landing-hero-gradient {
|
|
150
|
+
background: radial-gradient(
|
|
151
|
+
1000px 480px at 78% -8%,
|
|
152
|
+
var(--landing-accent-soft),
|
|
153
|
+
transparent 70%
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.apitogo-landing .landing-badge {
|
|
158
|
+
align-items: center;
|
|
159
|
+
background: var(--landing-surface);
|
|
160
|
+
border: 1px solid var(--landing-border);
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
color: var(--landing-text-2);
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
font-size: 0.78125rem;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
margin-bottom: 1.375rem;
|
|
167
|
+
padding: 0.375rem 0.75rem 0.375rem 0.5rem;
|
|
168
|
+
width: fit-content;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.apitogo-landing .landing-status-pill {
|
|
172
|
+
align-items: center;
|
|
173
|
+
background: var(--landing-green-soft);
|
|
174
|
+
border-radius: 999px;
|
|
175
|
+
color: var(--landing-green);
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
font-family: var(--landing-mono);
|
|
178
|
+
font-size: 0.6875rem;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
gap: 0.3125rem;
|
|
181
|
+
padding: 0.125rem 0.5rem;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.apitogo-landing .landing-status-dot {
|
|
185
|
+
background: var(--landing-green);
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
height: 6px;
|
|
188
|
+
width: 6px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.apitogo-landing .landing-feature-card {
|
|
192
|
+
background: var(--landing-surface);
|
|
193
|
+
border: 1px solid var(--landing-border);
|
|
194
|
+
border-radius: 14px;
|
|
195
|
+
padding: 1.5rem;
|
|
196
|
+
transition: border-color 0.15s ease;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.apitogo-landing .landing-feature-card:hover {
|
|
200
|
+
border-color: var(--landing-border-2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.apitogo-landing .landing-feature-icon {
|
|
204
|
+
align-items: center;
|
|
205
|
+
background: var(--landing-accent-soft);
|
|
206
|
+
border-radius: 11px;
|
|
207
|
+
color: var(--landing-accent);
|
|
208
|
+
display: grid;
|
|
209
|
+
height: 2.625rem;
|
|
210
|
+
margin-bottom: 1rem;
|
|
211
|
+
place-items: center;
|
|
212
|
+
width: 2.625rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.apitogo-landing .landing-api-card {
|
|
216
|
+
background: var(--landing-surface);
|
|
217
|
+
border: 1px solid var(--landing-border);
|
|
218
|
+
border-radius: 13px;
|
|
219
|
+
color: var(--landing-text);
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
padding: 1.125rem;
|
|
222
|
+
text-decoration: none;
|
|
223
|
+
transition:
|
|
224
|
+
border-color 0.15s ease,
|
|
225
|
+
transform 0.15s ease;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.apitogo-landing .landing-api-card:hover {
|
|
229
|
+
border-color: var(--landing-border-2);
|
|
230
|
+
transform: translateY(-2px);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.apitogo-landing .landing-api-icon {
|
|
234
|
+
align-items: center;
|
|
235
|
+
background: var(--landing-surface-2);
|
|
236
|
+
border: 1px solid var(--landing-border);
|
|
237
|
+
border-radius: 9px;
|
|
238
|
+
color: var(--landing-peri);
|
|
239
|
+
display: grid;
|
|
240
|
+
height: 2.25rem;
|
|
241
|
+
place-items: center;
|
|
242
|
+
width: 2.25rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.apitogo-landing .landing-latency-badge {
|
|
246
|
+
background: var(--landing-green-soft);
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
color: var(--landing-green);
|
|
249
|
+
font-family: var(--landing-mono);
|
|
250
|
+
font-size: 0.6875rem;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
padding: 0.1875rem 0.4375rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.apitogo-landing .landing-ticker-track {
|
|
256
|
+
animation: landing-ticker 34s linear infinite;
|
|
257
|
+
display: flex;
|
|
258
|
+
width: max-content;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.apitogo-landing .landing-ticker-shell {
|
|
262
|
+
background: var(--landing-bg-2);
|
|
263
|
+
border-top: 1px solid var(--landing-border);
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.apitogo-landing .landing-trust-logo {
|
|
268
|
+
color: var(--landing-text-2);
|
|
269
|
+
font-family: var(--landing-display);
|
|
270
|
+
font-size: 1.1875rem;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
letter-spacing: -0.01em;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.apitogo-landing .landing-showcase-section {
|
|
276
|
+
background: var(--landing-bg-2);
|
|
277
|
+
border-bottom: 1px solid var(--landing-border);
|
|
278
|
+
border-top: 1px solid var(--landing-border);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.apitogo-landing .landing-hero-section {
|
|
282
|
+
border-bottom: 1px solid var(--landing-border);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.apitogo-landing .landing-powered-by {
|
|
286
|
+
border-top: 1px solid var(--landing-border);
|
|
287
|
+
color: var(--landing-text-3);
|
|
288
|
+
}
|
|
@@ -19,50 +19,46 @@ export const ApiShowcaseSection = ({
|
|
|
19
19
|
const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<section className="
|
|
23
|
-
<div className="mx-auto max-w-
|
|
24
|
-
<div className="mb-8 flex flex-wrap items-end justify-between gap-
|
|
22
|
+
<section className="landing-showcase-section">
|
|
23
|
+
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
24
|
+
<div className="mb-8 flex flex-wrap items-end justify-between gap-5">
|
|
25
25
|
<div>
|
|
26
|
-
<p className="
|
|
27
|
-
|
|
28
|
-
</p>
|
|
29
|
-
<h2 className="text-3xl font-semibold tracking-tight">
|
|
26
|
+
<p className="landing-section-label">// the catalog</p>
|
|
27
|
+
<h2 className="landing-display text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
30
28
|
{apiShowcase.title ?? "Production APIs. One key."}
|
|
31
29
|
</h2>
|
|
32
30
|
{apiShowcase.subtitle ? (
|
|
33
|
-
<p className="mt-2
|
|
34
|
-
{apiShowcase.subtitle}
|
|
35
|
-
</p>
|
|
31
|
+
<p className="landing-muted mt-2">{apiShowcase.subtitle}</p>
|
|
36
32
|
) : null}
|
|
37
33
|
</div>
|
|
38
34
|
<Link
|
|
39
35
|
to={browseHref}
|
|
40
|
-
className="
|
|
36
|
+
className="landing-btn-secondary landing-btn-sm"
|
|
41
37
|
>
|
|
42
38
|
{browseLabel}
|
|
43
|
-
<ArrowRightIcon size={15} />
|
|
39
|
+
<ArrowRightIcon size={15} aria-hidden />
|
|
44
40
|
</Link>
|
|
45
41
|
</div>
|
|
46
42
|
|
|
47
|
-
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
43
|
+
<div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
|
48
44
|
{items.map((item) => (
|
|
49
45
|
<Link
|
|
50
46
|
key={`${item.name}-${item.path}`}
|
|
51
47
|
to={browseHref}
|
|
52
|
-
className="
|
|
48
|
+
className="landing-api-card block"
|
|
53
49
|
>
|
|
54
|
-
<div className="mb-3 flex items-center justify-between">
|
|
55
|
-
<div className="
|
|
50
|
+
<div className="mb-3.5 flex items-center justify-between">
|
|
51
|
+
<div className="landing-api-icon">
|
|
56
52
|
<LandingIcon name={item.icon ?? "boxes"} size={18} />
|
|
57
53
|
</div>
|
|
58
54
|
{item.latency ? (
|
|
59
|
-
<span className="
|
|
60
|
-
{item.latency}
|
|
61
|
-
</span>
|
|
55
|
+
<span className="landing-latency-badge">{item.latency}</span>
|
|
62
56
|
) : null}
|
|
63
57
|
</div>
|
|
64
|
-
<h3 className="font-
|
|
65
|
-
|
|
58
|
+
<h3 className="landing-display mb-1 text-[0.9375rem] font-semibold">
|
|
59
|
+
{item.name}
|
|
60
|
+
</h3>
|
|
61
|
+
<p className="landing-mono landing-muted-3 text-[0.71875rem]">
|
|
66
62
|
{item.path}
|
|
67
63
|
</p>
|
|
68
64
|
</Link>
|
|
@@ -12,15 +12,24 @@ export const CodeExampleCard = ({ example }: CodeExampleCardProps) => {
|
|
|
12
12
|
const language = example.language ?? "bash";
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<div
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<span className="
|
|
21
|
-
<
|
|
15
|
+
<div
|
|
16
|
+
className="overflow-hidden rounded-2xl border border-[var(--landing-border)] shadow-[var(--landing-shadow)]"
|
|
17
|
+
style={{ background: "var(--landing-code-bg)" }}
|
|
18
|
+
>
|
|
19
|
+
<div className="flex items-center gap-2 border-b border-[#1b1f2a] px-4 py-3">
|
|
20
|
+
<span className="size-[11px] rounded-full bg-[#ff5f57]" aria-hidden />
|
|
21
|
+
<span className="size-[11px] rounded-full bg-[#febc2e]" aria-hidden />
|
|
22
|
+
<span className="size-[11px] rounded-full bg-[#28c840]" aria-hidden />
|
|
23
|
+
<span className="landing-mono ms-2 text-xs text-[#6b7385]">
|
|
24
|
+
{filename}
|
|
25
|
+
</span>
|
|
26
|
+
<TerminalIcon
|
|
27
|
+
size={14}
|
|
28
|
+
className="ms-auto text-[#4b5162]"
|
|
29
|
+
aria-hidden
|
|
30
|
+
/>
|
|
22
31
|
</div>
|
|
23
|
-
<pre className="overflow-x-auto p-
|
|
32
|
+
<pre className="landing-mono overflow-x-auto p-[1.125rem] text-[0.78125rem] leading-[1.85] text-[#cdd3e0]">
|
|
24
33
|
<code data-language={language}>{example.code}</code>
|
|
25
34
|
</pre>
|
|
26
35
|
</div>
|
|
@@ -3,42 +3,46 @@ import { LandingIcon } from "./LandingIcon.js";
|
|
|
3
3
|
|
|
4
4
|
export type FeaturesSectionProps = {
|
|
5
5
|
features: NonNullable<ResolvedLandingContent["features"]>;
|
|
6
|
-
|
|
7
|
-
description?: string;
|
|
6
|
+
section?: ResolvedLandingContent["featuresSection"];
|
|
8
7
|
};
|
|
9
8
|
|
|
10
9
|
export const FeaturesSection = ({
|
|
11
10
|
features,
|
|
12
|
-
|
|
13
|
-
description = "Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.",
|
|
11
|
+
section,
|
|
14
12
|
}: FeaturesSectionProps) => {
|
|
15
13
|
if (features.length === 0) {
|
|
16
14
|
return null;
|
|
17
15
|
}
|
|
18
16
|
|
|
17
|
+
const label = section?.label ?? "// why us";
|
|
18
|
+
const title =
|
|
19
|
+
section?.title ?? "Everything you need to build on your API";
|
|
20
|
+
const description =
|
|
21
|
+
section?.description ??
|
|
22
|
+
"Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.";
|
|
23
|
+
|
|
19
24
|
return (
|
|
20
|
-
<section className="mx-auto max-w-
|
|
21
|
-
<div className="mb-
|
|
22
|
-
<p className="
|
|
23
|
-
|
|
24
|
-
</p>
|
|
25
|
-
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
|
25
|
+
<section className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
|
|
26
|
+
<div className="mb-11 max-w-[640px]">
|
|
27
|
+
<p className="landing-section-label">{label}</p>
|
|
28
|
+
<h2 className="landing-display mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
26
29
|
{title}
|
|
27
30
|
</h2>
|
|
28
|
-
<p className="
|
|
31
|
+
<p className="landing-muted text-[1.0625rem] leading-relaxed">
|
|
32
|
+
{description}
|
|
33
|
+
</p>
|
|
29
34
|
</div>
|
|
30
35
|
|
|
31
36
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
32
37
|
{features.map((feature) => (
|
|
33
|
-
<article
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
>
|
|
37
|
-
<div className="mb-4 flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
38
|
-
<LandingIcon name={feature.icon} />
|
|
38
|
+
<article key={feature.title} className="landing-feature-card">
|
|
39
|
+
<div className="landing-feature-icon">
|
|
40
|
+
<LandingIcon name={feature.icon} size={21} />
|
|
39
41
|
</div>
|
|
40
|
-
<h3 className="text-
|
|
41
|
-
|
|
42
|
+
<h3 className="landing-display mb-2 text-[1.09375rem] font-semibold">
|
|
43
|
+
{feature.title}
|
|
44
|
+
</h3>
|
|
45
|
+
<p className="landing-muted text-[0.90625rem] leading-relaxed">
|
|
42
46
|
{feature.description}
|
|
43
47
|
</p>
|
|
44
48
|
</article>
|
|
@@ -1,48 +1,51 @@
|
|
|
1
1
|
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ArrowRightIcon, CheckIcon } from "lucide-react";
|
|
2
|
+
import { cn } from "@lukoweb/apitogo";
|
|
3
|
+
import { ArrowRightIcon, BookOpenIcon, CheckIcon } from "lucide-react";
|
|
5
4
|
import { CodeExampleCard } from "./CodeExampleCard.js";
|
|
5
|
+
import { LandingButton } from "./LandingButtons.js";
|
|
6
|
+
import { TickerBar } from "./TickerBar.js";
|
|
6
7
|
|
|
7
8
|
export type HeroSectionProps = {
|
|
8
9
|
hero: NonNullable<ResolvedLandingContent["hero"]>;
|
|
9
10
|
primaryAction?: ResolvedLandingContent["primaryAction"];
|
|
10
11
|
secondaryAction?: ResolvedLandingContent["secondaryAction"];
|
|
12
|
+
ticker?: ResolvedLandingContent["ticker"];
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export const HeroSection = ({
|
|
14
16
|
hero,
|
|
15
17
|
primaryAction,
|
|
16
18
|
secondaryAction,
|
|
19
|
+
ticker,
|
|
17
20
|
}: HeroSectionProps) => {
|
|
18
21
|
const hasCodeExample = Boolean(hero.codeExample?.code);
|
|
19
22
|
const highlights = hero.highlights ?? [];
|
|
20
23
|
|
|
21
24
|
return (
|
|
22
|
-
<section className="relative overflow-hidden
|
|
25
|
+
<section className="landing-hero-section relative overflow-hidden">
|
|
23
26
|
<div
|
|
24
27
|
aria-hidden
|
|
25
|
-
className="pointer-events-none absolute inset-
|
|
28
|
+
className="landing-hero-gradient pointer-events-none absolute inset-0"
|
|
26
29
|
/>
|
|
27
30
|
|
|
28
31
|
<div
|
|
29
32
|
className={
|
|
30
33
|
hasCodeExample
|
|
31
|
-
? "mx-auto grid max-w-
|
|
32
|
-
: "mx-auto flex max-w-
|
|
34
|
+
? "relative mx-auto grid max-w-[1200px] items-center gap-10 px-6 py-16 lg:grid-cols-[1.05fr_0.95fr] lg:gap-14 lg:px-6 lg:py-[4.75rem] lg:pb-16"
|
|
35
|
+
: "relative mx-auto flex max-w-[1200px] flex-col gap-10 px-6 py-16 lg:px-6 lg:py-24"
|
|
33
36
|
}
|
|
34
37
|
>
|
|
35
38
|
<div
|
|
36
|
-
className={
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
39
|
+
className={cn(
|
|
40
|
+
"landing-fu flex flex-col",
|
|
41
|
+
!hasCodeExample && "mx-auto max-w-3xl items-center text-center",
|
|
42
|
+
)}
|
|
41
43
|
>
|
|
42
44
|
{(hero.badge || hero.subtitle) && (
|
|
43
|
-
<div className="
|
|
45
|
+
<div className="landing-badge">
|
|
44
46
|
{hero.badge ? (
|
|
45
|
-
<span className="
|
|
47
|
+
<span className="landing-status-pill">
|
|
48
|
+
<span className="landing-status-dot" aria-hidden />
|
|
46
49
|
{hero.badge}
|
|
47
50
|
</span>
|
|
48
51
|
) : null}
|
|
@@ -50,39 +53,55 @@ export const HeroSection = ({
|
|
|
50
53
|
</div>
|
|
51
54
|
)}
|
|
52
55
|
|
|
53
|
-
<h1 className="text-
|
|
56
|
+
<h1 className="landing-display mb-5 text-[2.5rem] leading-[1.02] font-bold tracking-[-0.03em] text-balance sm:text-5xl lg:text-[3.5rem]">
|
|
54
57
|
{hero.title}
|
|
58
|
+
{hero.titleAccent ? (
|
|
59
|
+
<>
|
|
60
|
+
<br />
|
|
61
|
+
<span className="text-[var(--landing-accent)]">
|
|
62
|
+
{hero.titleAccent}
|
|
63
|
+
</span>
|
|
64
|
+
</>
|
|
65
|
+
) : null}
|
|
55
66
|
</h1>
|
|
56
67
|
|
|
57
68
|
{hero.description ? (
|
|
58
|
-
<p className="max-w-
|
|
69
|
+
<p className="landing-muted mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty">
|
|
59
70
|
{hero.description}
|
|
60
71
|
</p>
|
|
61
72
|
) : null}
|
|
62
73
|
|
|
63
74
|
{(primaryAction || secondaryAction) && (
|
|
64
|
-
<div className="flex flex-wrap items-center gap-3
|
|
75
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
65
76
|
{primaryAction ? (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
<LandingButton
|
|
78
|
+
href={primaryAction.href}
|
|
79
|
+
label={primaryAction.label}
|
|
80
|
+
icon={ArrowRightIcon}
|
|
81
|
+
variant="primary"
|
|
82
|
+
/>
|
|
72
83
|
) : null}
|
|
73
84
|
{secondaryAction ? (
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
<LandingButton
|
|
86
|
+
href={secondaryAction.href}
|
|
87
|
+
label={secondaryAction.label}
|
|
88
|
+
icon={BookOpenIcon}
|
|
89
|
+
iconPosition="start"
|
|
90
|
+
variant="secondary"
|
|
91
|
+
/>
|
|
77
92
|
) : null}
|
|
78
93
|
</div>
|
|
79
94
|
)}
|
|
80
95
|
|
|
81
96
|
{highlights.length > 0 ? (
|
|
82
|
-
<ul className="flex flex-wrap items-center gap-
|
|
97
|
+
<ul className="landing-muted-3 mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem]">
|
|
83
98
|
{highlights.map((item) => (
|
|
84
|
-
<li key={item} className="inline-flex items-center gap-
|
|
85
|
-
<CheckIcon
|
|
99
|
+
<li key={item} className="inline-flex items-center gap-[0.4375rem]">
|
|
100
|
+
<CheckIcon
|
|
101
|
+
size={15}
|
|
102
|
+
className="text-[var(--landing-green)]"
|
|
103
|
+
aria-hidden
|
|
104
|
+
/>
|
|
86
105
|
{item}
|
|
87
106
|
</li>
|
|
88
107
|
))}
|
|
@@ -91,9 +110,13 @@ export const HeroSection = ({
|
|
|
91
110
|
</div>
|
|
92
111
|
|
|
93
112
|
{hasCodeExample && hero.codeExample ? (
|
|
94
|
-
<
|
|
113
|
+
<div className="landing-fu">
|
|
114
|
+
<CodeExampleCard example={hero.codeExample} />
|
|
115
|
+
</div>
|
|
95
116
|
) : null}
|
|
96
117
|
</div>
|
|
118
|
+
|
|
119
|
+
{ticker ? <TickerBar ticker={ticker} embedded /> : null}
|
|
97
120
|
</section>
|
|
98
121
|
);
|
|
99
122
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
2
|
+
import { cn } from "@lukoweb/apitogo";
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
type LandingButtonProps = {
|
|
7
|
+
href: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
iconPosition?: "start" | "end";
|
|
11
|
+
variant?: "primary" | "secondary";
|
|
12
|
+
size?: "default" | "sm";
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const LandingButton = ({
|
|
18
|
+
href,
|
|
19
|
+
label,
|
|
20
|
+
icon: Icon,
|
|
21
|
+
iconPosition = "end",
|
|
22
|
+
variant = "primary",
|
|
23
|
+
size = "default",
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
}: LandingButtonProps) => {
|
|
27
|
+
const content = children ?? (
|
|
28
|
+
<>
|
|
29
|
+
{Icon && iconPosition === "start" ? <Icon size={17} aria-hidden /> : null}
|
|
30
|
+
{label}
|
|
31
|
+
{Icon && iconPosition === "end" ? <Icon size={17} aria-hidden /> : null}
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Link
|
|
37
|
+
to={href}
|
|
38
|
+
className={cn(
|
|
39
|
+
variant === "primary" ? "landing-btn-primary" : "landing-btn-secondary",
|
|
40
|
+
size === "sm" && "landing-btn-sm",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{content}
|
|
45
|
+
</Link>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ActivityIcon,
|
|
4
4
|
ArrowRightLeftIcon,
|
|
5
5
|
BadgeDollarSignIcon,
|
|
6
|
+
BlocksIcon,
|
|
6
7
|
BookOpenIcon,
|
|
7
8
|
BoxesIcon,
|
|
8
9
|
CheckIcon,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
LayersIcon,
|
|
13
14
|
RadioIcon,
|
|
14
15
|
ShieldCheckIcon,
|
|
16
|
+
WalletIcon,
|
|
15
17
|
WebhookIcon,
|
|
16
18
|
ZapIcon,
|
|
17
19
|
} from "lucide-react";
|
|
@@ -21,6 +23,7 @@ const ICONS: Record<string, LucideIcon> = {
|
|
|
21
23
|
"arrow-left-right": ArrowRightLeftIcon,
|
|
22
24
|
"arrow-right-left": ArrowRightLeftIcon,
|
|
23
25
|
"badge-dollar-sign": BadgeDollarSignIcon,
|
|
26
|
+
blocks: BlocksIcon,
|
|
24
27
|
"book-open": BookOpenIcon,
|
|
25
28
|
boxes: BoxesIcon,
|
|
26
29
|
check: CheckIcon,
|
|
@@ -30,6 +33,7 @@ const ICONS: Record<string, LucideIcon> = {
|
|
|
30
33
|
layers: LayersIcon,
|
|
31
34
|
radio: RadioIcon,
|
|
32
35
|
"shield-check": ShieldCheckIcon,
|
|
36
|
+
wallet: WalletIcon,
|
|
33
37
|
webhook: WebhookIcon,
|
|
34
38
|
zap: ZapIcon,
|
|
35
39
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
-
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
-
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
4
2
|
import { ArrowRightIcon } from "lucide-react";
|
|
3
|
+
import { LandingButton } from "./LandingButtons.js";
|
|
5
4
|
|
|
6
5
|
export type PricingCtaSectionProps = {
|
|
7
6
|
pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
|
|
@@ -14,28 +13,28 @@ export const PricingCtaSection = ({ pricingCta }: PricingCtaSectionProps) => {
|
|
|
14
13
|
"Generous free tier. Upgrade only when your app takes off.";
|
|
15
14
|
|
|
16
15
|
return (
|
|
17
|
-
<section className="mx-auto max-w-
|
|
18
|
-
<h2 className="text-
|
|
16
|
+
<section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
|
|
17
|
+
<h2 className="landing-display mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
|
|
19
18
|
{title}
|
|
20
19
|
</h2>
|
|
21
|
-
<p className="mx-auto
|
|
20
|
+
<p className="landing-muted mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed">
|
|
22
21
|
{description}
|
|
23
22
|
</p>
|
|
24
|
-
<div className="
|
|
23
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
25
24
|
{pricingCta.primaryAction ? (
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
<LandingButton
|
|
26
|
+
href={pricingCta.primaryAction.href}
|
|
27
|
+
label={pricingCta.primaryAction.label}
|
|
28
|
+
icon={ArrowRightIcon}
|
|
29
|
+
variant="primary"
|
|
30
|
+
/>
|
|
32
31
|
) : null}
|
|
33
32
|
{pricingCta.secondaryAction ? (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
<LandingButton
|
|
34
|
+
href={pricingCta.secondaryAction.href}
|
|
35
|
+
label={pricingCta.secondaryAction.label}
|
|
36
|
+
variant="secondary"
|
|
37
|
+
/>
|
|
39
38
|
) : null}
|
|
40
39
|
</div>
|
|
41
40
|
</section>
|
|
@@ -2,9 +2,10 @@ import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
|
2
2
|
|
|
3
3
|
export type TickerBarProps = {
|
|
4
4
|
ticker: NonNullable<ResolvedLandingContent["ticker"]>;
|
|
5
|
+
embedded?: boolean;
|
|
5
6
|
};
|
|
6
7
|
|
|
7
|
-
export const TickerBar = ({ ticker }: TickerBarProps) => {
|
|
8
|
+
export const TickerBar = ({ ticker, embedded = false }: TickerBarProps) => {
|
|
8
9
|
const items = ticker.items ?? [];
|
|
9
10
|
if (items.length === 0) {
|
|
10
11
|
return null;
|
|
@@ -13,24 +14,25 @@ export const TickerBar = ({ ticker }: TickerBarProps) => {
|
|
|
13
14
|
const loop = [...items, ...items];
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
|
-
<div className="
|
|
17
|
-
<div className="
|
|
17
|
+
<div className={embedded ? "landing-ticker-shell" : "landing-ticker-shell border-t-0"}>
|
|
18
|
+
<div className="landing-ticker-track">
|
|
18
19
|
{loop.map((item, index) => (
|
|
19
20
|
<div
|
|
20
21
|
key={`${item.label}-${index}`}
|
|
21
|
-
className="flex items-center gap-2.5
|
|
22
|
+
className="landing-mono flex items-center gap-2.5 border-r border-[var(--landing-border)] px-[1.625rem] py-[0.8125rem] text-[0.8125rem] whitespace-nowrap"
|
|
22
23
|
>
|
|
23
|
-
<span className="font-semibold
|
|
24
|
-
|
|
24
|
+
<span className="landing-muted-3 font-semibold">{item.label}</span>
|
|
25
|
+
<span className="font-semibold text-[var(--landing-text)]">
|
|
26
|
+
{item.value}
|
|
25
27
|
</span>
|
|
26
|
-
<span className="font-semibold">{item.value}</span>
|
|
27
28
|
{item.change ? (
|
|
28
29
|
<span
|
|
29
|
-
className=
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
className="font-semibold"
|
|
31
|
+
style={{
|
|
32
|
+
color: item.change.trim().startsWith("-")
|
|
33
|
+
? "var(--landing-accent)"
|
|
34
|
+
: "var(--landing-green)",
|
|
35
|
+
}}
|
|
34
36
|
>
|
|
35
37
|
{item.change}
|
|
36
38
|
</span>
|
|
@@ -38,7 +40,6 @@ export const TickerBar = ({ ticker }: TickerBarProps) => {
|
|
|
38
40
|
</div>
|
|
39
41
|
))}
|
|
40
42
|
</div>
|
|
41
|
-
<style>{`@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }`}</style>
|
|
42
43
|
</div>
|
|
43
44
|
);
|
|
44
45
|
};
|
|
@@ -11,18 +11,15 @@ export const TrustSection = ({ trust }: TrustSectionProps) => {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<section className="mx-auto max-w-
|
|
14
|
+
<section className="mx-auto max-w-[1200px] px-6 py-10">
|
|
15
15
|
{trust.title ? (
|
|
16
|
-
<p className="mb-
|
|
16
|
+
<p className="landing-muted-3 mb-[1.375rem] text-center text-[0.78125rem] font-semibold tracking-[0.08em] uppercase">
|
|
17
17
|
{trust.title}
|
|
18
18
|
</p>
|
|
19
19
|
) : null}
|
|
20
|
-
<div className="flex flex-wrap items-center justify-center gap-x-
|
|
20
|
+
<div className="flex flex-wrap items-center justify-center gap-x-[2.875rem] gap-y-3.5 opacity-70">
|
|
21
21
|
{logos.map((logo) => (
|
|
22
|
-
<span
|
|
23
|
-
key={logo}
|
|
24
|
-
className="text-lg font-semibold tracking-tight text-muted-foreground"
|
|
25
|
-
>
|
|
22
|
+
<span key={logo} className="landing-trust-logo">
|
|
26
23
|
{logo}
|
|
27
24
|
</span>
|
|
28
25
|
))}
|