@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-module-landing",
3
- "version": "0.1.55",
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.55",
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, description }}
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 ? <FeaturesSection features={content.features} /> : null}
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
- "border-t py-6 text-center text-xs text-muted-foreground",
65
+ "landing-powered-by py-6 text-center text-xs",
60
66
  !options.site?.footer && "mb-8",
61
67
  )}
62
68
  >
@@ -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="border-y bg-muted/20">
23
- <div className="mx-auto max-w-screen-xl px-4 py-14 lg:px-8">
24
- <div className="mb-8 flex flex-wrap items-end justify-between gap-4">
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="mb-3 font-mono text-sm font-semibold text-primary">
27
- // the catalog
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 text-muted-foreground">
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="inline-flex items-center gap-2 rounded-lg border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted/50"
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="rounded-xl border bg-card/70 p-4 transition hover:-translate-y-0.5 hover:border-primary/30"
48
+ className="landing-api-card block"
53
49
  >
54
- <div className="mb-3 flex items-center justify-between">
55
- <div className="flex size-9 items-center justify-center rounded-lg border bg-muted/40 text-primary">
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="rounded-md bg-primary/10 px-2 py-0.5 font-mono text-[11px] font-semibold text-primary">
60
- {item.latency}
61
- </span>
55
+ <span className="landing-latency-badge">{item.latency}</span>
62
56
  ) : null}
63
57
  </div>
64
- <h3 className="font-medium">{item.name}</h3>
65
- <p className="mt-1 font-mono text-[11px] text-muted-foreground">
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 className="overflow-hidden rounded-2xl border bg-[#0c0f16] shadow-lg">
16
- <div className="flex items-center gap-2 border-b border-white/10 px-4 py-3">
17
- <span className="size-2.5 rounded-full bg-[#ff5f57]" />
18
- <span className="size-2.5 rounded-full bg-[#febc2e]" />
19
- <span className="size-2.5 rounded-full bg-[#28c840]" />
20
- <span className="ms-2 font-mono text-xs text-white/45">{filename}</span>
21
- <TerminalIcon size={14} className="ms-auto text-white/30" />
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-4 font-mono text-xs leading-relaxed text-[#cdd3e0]">
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
- title?: string;
7
- description?: string;
6
+ section?: ResolvedLandingContent["featuresSection"];
8
7
  };
9
8
 
10
9
  export const FeaturesSection = ({
11
10
  features,
12
- title = "Everything you need to build on your API",
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-screen-xl px-4 py-12 lg:px-8 lg:pb-16">
21
- <div className="mb-10 max-w-2xl">
22
- <p className="mb-3 font-mono text-sm font-semibold text-primary">
23
- // why us
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="mt-3 text-lg text-muted-foreground">{description}</p>
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
- key={feature.title}
35
- className="rounded-xl border bg-card/60 p-6 shadow-xs backdrop-blur-sm"
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-lg font-medium">{feature.title}</h3>
41
- <p className="mt-2 text-sm leading-relaxed text-muted-foreground">
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 { Link } from "@lukoweb/apitogo/components";
3
- import { Button } from "@lukoweb/apitogo/ui/Button.js";
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 border-b">
25
+ <section className="landing-hero-section relative overflow-hidden">
23
26
  <div
24
27
  aria-hidden
25
- className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[520px] bg-[radial-gradient(ellipse_at_top_right,var(--primary)_0%,transparent_70%)] opacity-[0.12]"
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-screen-xl items-center gap-10 px-4 py-16 lg:grid-cols-2 lg:gap-14 lg:px-8 lg:py-20"
32
- : "mx-auto flex max-w-screen-xl flex-col gap-10 px-4 py-16 lg:px-8 lg:py-24"
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
- hasCodeExample
38
- ? "flex flex-col gap-6"
39
- : "mx-auto flex max-w-3xl flex-col items-center gap-6 text-center"
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="inline-flex w-fit flex-wrap items-center gap-2 rounded-full border bg-muted/50 px-3 py-1 text-xs font-medium text-muted-foreground">
45
+ <div className="landing-badge">
44
46
  {hero.badge ? (
45
- <span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 font-semibold text-primary">
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-4xl font-semibold tracking-tight text-balance sm:text-5xl lg:text-[3.25rem] lg:leading-[1.05]">
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-xl text-lg text-muted-foreground text-pretty">
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 pt-1">
75
+ <div className="flex flex-wrap items-center gap-3">
65
76
  {primaryAction ? (
66
- <Button asChild size="lg">
67
- <Link to={primaryAction.href}>
68
- {primaryAction.label}
69
- <ArrowRightIcon className="ms-1" />
70
- </Link>
71
- </Button>
77
+ <LandingButton
78
+ href={primaryAction.href}
79
+ label={primaryAction.label}
80
+ icon={ArrowRightIcon}
81
+ variant="primary"
82
+ />
72
83
  ) : null}
73
84
  {secondaryAction ? (
74
- <Button asChild variant="outline" size="lg">
75
- <Link to={secondaryAction.href}>{secondaryAction.label}</Link>
76
- </Button>
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-5 pt-2 text-sm text-muted-foreground">
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-2">
85
- <CheckIcon size={15} className="text-primary" />
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
- <CodeExampleCard example={hero.codeExample} />
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-screen-xl px-4 py-16 text-center lg:px-8 lg:py-20">
18
- <h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
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 mt-4 max-w-xl text-lg text-muted-foreground">
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="mt-8 flex flex-wrap items-center justify-center gap-3">
23
+ <div className="flex flex-wrap items-center justify-center gap-3">
25
24
  {pricingCta.primaryAction ? (
26
- <Button asChild size="lg">
27
- <Link to={pricingCta.primaryAction.href}>
28
- {pricingCta.primaryAction.label}
29
- <ArrowRightIcon className="ms-1" />
30
- </Link>
31
- </Button>
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
- <Button asChild variant="outline" size="lg">
35
- <Link to={pricingCta.secondaryAction.href}>
36
- {pricingCta.secondaryAction.label}
37
- </Link>
38
- </Button>
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="overflow-hidden border-t bg-muted/30">
17
- <div className="flex w-max animate-[ticker_34s_linear_infinite]">
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 whitespace-nowrap border-r px-6 py-3 font-mono text-sm"
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 text-muted-foreground">
24
- {item.label}
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
- item.change.trim().startsWith("-")
31
- ? "font-semibold text-destructive"
32
- : "font-semibold text-primary"
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-screen-xl px-4 py-10 lg:px-8">
14
+ <section className="mx-auto max-w-[1200px] px-6 py-10">
15
15
  {trust.title ? (
16
- <p className="mb-6 text-center text-xs font-semibold tracking-[0.08em] text-muted-foreground uppercase">
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-10 gap-y-4 opacity-80">
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
  ))}