@lukoweb/apitogo-module-landing 0.1.58 → 0.1.60

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.58",
3
+ "version": "0.1.60",
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.58",
17
+ "@lukoweb/apitogo": "0.1.60",
18
18
  "react": "19.2.4",
19
19
  "react-dom": "19.2.4",
20
20
  "typescript": "5.9.3"
@@ -45,6 +45,7 @@ export const DefaultLandingPage = ({ content }: DefaultLandingPageProps) => {
45
45
  {content.pricingCta ? (
46
46
  <PricingCtaSection pricingCta={content.pricingCta} />
47
47
  ) : null}
48
+ {/* content.footer renders portal-wide via the layout "footer" slot. */}
48
49
  </LandingShell>
49
50
  );
50
51
  };
@@ -0,0 +1,21 @@
1
+ import type { ResolvedLandingContent } from "@lukoweb/apitogo";
2
+ import type { ComponentType } from "react";
3
+ import { FooterSection } from "./sections/FooterSection.js";
4
+
5
+ export type LandingFooterSlotConfig = ResolvedLandingContent["footer"];
6
+
7
+ /**
8
+ * Builds a component for the layout "footer" slot so the landing footer
9
+ * renders on every page (home, docs, API reference, pricing, auth).
10
+ */
11
+ export const createLandingFooterSlot = (
12
+ footer: LandingFooterSlotConfig,
13
+ ): ComponentType | undefined => {
14
+ if (!footer || footer.enabled === false) {
15
+ return undefined;
16
+ }
17
+
18
+ const LandingFooterSlot = () => <FooterSection footer={footer} />;
19
+
20
+ return LandingFooterSlot;
21
+ };
@@ -1,15 +1,38 @@
1
1
  import { cn } from "@lukoweb/apitogo";
2
2
  import { Head, useZudoku } from "@lukoweb/apitogo/components";
3
- import type { ReactNode } from "react";
3
+ import { Children, isValidElement, type ReactNode } from "react";
4
+ import { FooterSection } from "./sections/FooterSection.js";
4
5
  import "./landing.css";
5
6
 
7
+ /**
8
+ * Detects a composed `FooterSection` in the element tree so the
9
+ * "Built with APIToGo" line can be replaced by the real footer.
10
+ */
11
+ const containsFooterSection = (node: ReactNode): boolean =>
12
+ Children.toArray(node).some((child) => {
13
+ if (!isValidElement(child)) {
14
+ return false;
15
+ }
16
+
17
+ if (child.type === FooterSection) {
18
+ return true;
19
+ }
20
+
21
+ const { children } = child.props as { children?: ReactNode };
22
+ return children ? containsFooterSection(children) : false;
23
+ });
24
+
6
25
  export type LandingShellProps = {
7
26
  children: ReactNode;
8
27
  /** Page title for `<Head>`. Defaults to site title from config. */
9
28
  title?: string;
10
29
  /** Meta description for `<Head>`. */
11
30
  description?: string;
12
- /** Show "Built with APIToGo" footer. Defaults to true. */
31
+ /**
32
+ * Show the "Built with APIToGo" line. Defaults to true.
33
+ * Automatically hidden when a `FooterSection` is composed as a child or
34
+ * a footer slot is configured (e.g. `modules.landing.content.footer`).
35
+ */
13
36
  showPoweredBy?: boolean;
14
37
  };
15
38
 
@@ -23,30 +46,22 @@ export const LandingShell = ({
23
46
  const siteTitle =
24
47
  options.metadata?.applicationName ?? options.site?.title ?? "APIToGo";
25
48
  const pageTitle = title ?? siteTitle;
49
+ const hasFooter =
50
+ containsFooterSection(children) || Boolean(options.slots?.footer);
26
51
 
27
52
  return (
28
- <div className="apitogo-landing relative overflow-hidden">
53
+ <div className="relative overflow-hidden bg-background text-foreground">
29
54
  <Head>
30
- <link rel="preconnect" href="https://fonts.googleapis.com" />
31
- <link
32
- rel="preconnect"
33
- href="https://fonts.gstatic.com"
34
- crossOrigin=""
35
- />
36
- <link
37
- 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"
38
- rel="stylesheet"
39
- />
40
55
  <title>{pageTitle}</title>
41
56
  {description ? <meta name="description" content={description} /> : null}
42
57
  </Head>
43
58
 
44
59
  {children}
45
60
 
46
- {showPoweredBy ? (
61
+ {showPoweredBy && !hasFooter ? (
47
62
  <div
48
63
  className={cn(
49
- "landing-powered-by py-6 text-center text-xs",
64
+ "border-t border-border py-6 text-center text-xs text-muted-foreground/70",
50
65
  !options.site?.footer && "mb-8",
51
66
  )}
52
67
  >
package/src/index.tsx CHANGED
@@ -4,6 +4,8 @@ export { LandingShell } from "./LandingShell.js";
4
4
  export type { LandingShellProps } from "./LandingShell.js";
5
5
  export { landingModule } from "./landingModule.js";
6
6
  export type { LandingModuleOptions } from "./landingModule.js";
7
+ export { createLandingFooterSlot } from "./LandingFooterSlot.js";
8
+ export type { LandingFooterSlotConfig } from "./LandingFooterSlot.js";
7
9
  export { HeroSection } from "./sections/HeroSection.js";
8
10
  export type { HeroSectionProps } from "./sections/HeroSection.js";
9
11
  export { TrustSection } from "./sections/TrustSection.js";
@@ -14,5 +16,7 @@ export { ApiShowcaseSection } from "./sections/ApiShowcaseSection.js";
14
16
  export type { ApiShowcaseSectionProps } from "./sections/ApiShowcaseSection.js";
15
17
  export { PricingCtaSection } from "./sections/PricingCtaSection.js";
16
18
  export type { PricingCtaSectionProps } from "./sections/PricingCtaSection.js";
19
+ export { FooterSection } from "./sections/FooterSection.js";
20
+ export type { FooterSectionProps } from "./sections/FooterSection.js";
17
21
  export { CodeExampleCard } from "./sections/CodeExampleCard.js";
18
22
  export type { CodeExampleCardProps } from "./sections/CodeExampleCard.js";
package/src/landing.css CHANGED
@@ -1,52 +1,3 @@
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
1
  @keyframes landing-fu {
51
2
  from {
52
3
  opacity: 0;
@@ -58,208 +9,6 @@
58
9
  }
59
10
  }
60
11
 
61
- .apitogo-landing .landing-display {
62
- font-family: var(--landing-display);
63
- }
64
-
65
- .apitogo-landing .landing-mono {
66
- font-family: var(--landing-mono);
67
- }
68
-
69
- .apitogo-landing .landing-fu {
12
+ .landing-fu {
70
13
  animation: landing-fu 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
71
14
  }
72
-
73
- .apitogo-landing .landing-section-label {
74
- color: var(--landing-accent);
75
- font-family: var(--landing-mono);
76
- font-size: 0.8125rem;
77
- font-weight: 600;
78
- margin-bottom: 0.75rem;
79
- }
80
-
81
- .apitogo-landing .landing-muted {
82
- color: var(--landing-text-2);
83
- }
84
-
85
- .apitogo-landing .landing-muted-3 {
86
- color: var(--landing-text-3);
87
- }
88
-
89
- .apitogo-landing .landing-btn-primary {
90
- align-items: center;
91
- background: var(--landing-accent);
92
- border: none;
93
- border-radius: 11px;
94
- box-shadow: 0 8px 24px -8px var(--landing-accent-soft);
95
- color: var(--landing-on-accent);
96
- cursor: pointer;
97
- display: inline-flex;
98
- font-family: inherit;
99
- font-size: 0.96875rem;
100
- font-weight: 600;
101
- gap: 0.5rem;
102
- padding: 0.8125rem 1.375rem;
103
- text-decoration: none;
104
- transition: background 0.15s ease;
105
- }
106
-
107
- .apitogo-landing .landing-btn-primary:hover {
108
- background: var(--landing-accent-hover);
109
- }
110
-
111
- .apitogo-landing .landing-btn-secondary {
112
- align-items: center;
113
- background: var(--landing-surface);
114
- border: 1px solid var(--landing-border);
115
- border-radius: 11px;
116
- color: var(--landing-text);
117
- cursor: pointer;
118
- display: inline-flex;
119
- font-family: inherit;
120
- font-size: 0.96875rem;
121
- font-weight: 600;
122
- gap: 0.5rem;
123
- padding: 0.8125rem 1.375rem;
124
- text-decoration: none;
125
- transition:
126
- border-color 0.15s ease,
127
- background 0.15s ease;
128
- }
129
-
130
- .apitogo-landing .landing-btn-secondary:hover {
131
- border-color: var(--landing-border-2);
132
- }
133
-
134
- .apitogo-landing .landing-btn-sm {
135
- border-radius: 9px;
136
- font-size: 0.875rem;
137
- padding: 0.625rem 1rem;
138
- }
139
-
140
- .apitogo-landing .landing-hero-gradient {
141
- background: radial-gradient(
142
- 1000px 480px at 78% -8%,
143
- var(--landing-accent-soft),
144
- transparent 70%
145
- );
146
- }
147
-
148
- .apitogo-landing .landing-badge {
149
- align-items: center;
150
- background: var(--landing-surface);
151
- border: 1px solid var(--landing-border);
152
- border-radius: 999px;
153
- color: var(--landing-text-2);
154
- display: inline-flex;
155
- font-size: 0.78125rem;
156
- gap: 0.5rem;
157
- margin-bottom: 1.375rem;
158
- padding: 0.375rem 0.75rem 0.375rem 0.5rem;
159
- width: fit-content;
160
- }
161
-
162
- .apitogo-landing .landing-status-pill {
163
- align-items: center;
164
- background: var(--landing-green-soft);
165
- border-radius: 999px;
166
- color: var(--landing-green);
167
- display: inline-flex;
168
- font-family: var(--landing-mono);
169
- font-size: 0.6875rem;
170
- font-weight: 600;
171
- gap: 0.3125rem;
172
- padding: 0.125rem 0.5rem;
173
- }
174
-
175
- .apitogo-landing .landing-status-dot {
176
- background: var(--landing-green);
177
- border-radius: 50%;
178
- height: 6px;
179
- width: 6px;
180
- }
181
-
182
- .apitogo-landing .landing-feature-card {
183
- background: var(--landing-surface);
184
- border: 1px solid var(--landing-border);
185
- border-radius: 14px;
186
- padding: 1.5rem;
187
- transition: border-color 0.15s ease;
188
- }
189
-
190
- .apitogo-landing .landing-feature-card:hover {
191
- border-color: var(--landing-border-2);
192
- }
193
-
194
- .apitogo-landing .landing-feature-icon {
195
- background: var(--landing-accent-soft);
196
- border-radius: 11px;
197
- color: var(--landing-accent);
198
- display: grid;
199
- height: 2.625rem;
200
- margin-bottom: 1rem;
201
- place-items: center;
202
- width: 2.625rem;
203
- }
204
-
205
- .apitogo-landing .landing-api-card {
206
- background: var(--landing-surface);
207
- border: 1px solid var(--landing-border);
208
- border-radius: 13px;
209
- color: var(--landing-text);
210
- cursor: pointer;
211
- padding: 1.125rem;
212
- text-decoration: none;
213
- transition:
214
- border-color 0.15s ease,
215
- transform 0.15s ease;
216
- }
217
-
218
- .apitogo-landing .landing-api-card:hover {
219
- border-color: var(--landing-border-2);
220
- transform: translateY(-2px);
221
- }
222
-
223
- .apitogo-landing .landing-api-icon {
224
- background: var(--landing-surface-2);
225
- border: 1px solid var(--landing-border);
226
- border-radius: 9px;
227
- color: var(--landing-peri);
228
- display: grid;
229
- height: 2.25rem;
230
- place-items: center;
231
- width: 2.25rem;
232
- }
233
-
234
- .apitogo-landing .landing-latency-badge {
235
- background: var(--landing-green-soft);
236
- border-radius: 6px;
237
- color: var(--landing-green);
238
- font-family: var(--landing-mono);
239
- font-size: 0.6875rem;
240
- font-weight: 600;
241
- padding: 0.1875rem 0.4375rem;
242
- }
243
-
244
- .apitogo-landing .landing-trust-logo {
245
- color: var(--landing-text-2);
246
- font-family: var(--landing-display);
247
- font-size: 1.1875rem;
248
- font-weight: 600;
249
- letter-spacing: -0.01em;
250
- }
251
-
252
- .apitogo-landing .landing-showcase-section {
253
- background: var(--landing-bg-2);
254
- border-bottom: 1px solid var(--landing-border);
255
- border-top: 1px solid var(--landing-border);
256
- }
257
-
258
- .apitogo-landing .landing-hero-section {
259
- border-bottom: 1px solid var(--landing-border);
260
- }
261
-
262
- .apitogo-landing .landing-powered-by {
263
- border-top: 1px solid var(--landing-border);
264
- color: var(--landing-text-3);
265
- }
@@ -1,5 +1,6 @@
1
1
  import type { ResolvedLandingContent } from "@lukoweb/apitogo";
2
2
  import { Link } from "@lukoweb/apitogo/components";
3
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
3
4
  import { ArrowRightIcon } from "lucide-react";
4
5
  import { LandingIcon } from "./LandingIcon.js";
5
6
 
@@ -19,25 +20,28 @@ export const ApiShowcaseSection = ({
19
20
  const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
20
21
 
21
22
  return (
22
- <section className="landing-showcase-section">
23
+ <section className="border-y border-border bg-muted/30">
23
24
  <div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
24
25
  <div className="mb-8 flex flex-wrap items-end justify-between gap-5">
25
26
  <div>
26
- <p className="landing-section-label">{"// the catalog"}</p>
27
- <h2 className="landing-display text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
27
+ <p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
28
+ {"// the catalog"}
29
+ </p>
30
+ <h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
28
31
  {apiShowcase.title ?? "Production APIs. One key."}
29
32
  </h2>
30
33
  {apiShowcase.subtitle ? (
31
- <p className="landing-muted mt-2">{apiShowcase.subtitle}</p>
34
+ <p className="mt-2 text-muted-foreground">
35
+ {apiShowcase.subtitle}
36
+ </p>
32
37
  ) : null}
33
38
  </div>
34
- <Link
35
- to={browseHref}
36
- className="landing-btn-secondary landing-btn-sm"
37
- >
38
- {browseLabel}
39
- <ArrowRightIcon size={15} aria-hidden />
40
- </Link>
39
+ <Button asChild variant="outline" size="sm">
40
+ <Link to={browseHref}>
41
+ {browseLabel}
42
+ <ArrowRightIcon size={15} aria-hidden />
43
+ </Link>
44
+ </Button>
41
45
  </div>
42
46
 
43
47
  <div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
@@ -45,20 +49,22 @@ export const ApiShowcaseSection = ({
45
49
  <Link
46
50
  key={`${item.name}-${item.path}`}
47
51
  to={browseHref}
48
- className="landing-api-card block"
52
+ className="block rounded-[13px] border border-border bg-card p-[1.125rem] text-foreground no-underline transition-[border-color,transform] hover:-translate-y-0.5 hover:border-muted-foreground/30"
49
53
  >
50
54
  <div className="mb-3.5 flex items-center justify-between">
51
- <div className="landing-api-icon">
55
+ <div className="grid size-9 place-items-center rounded-[9px] border border-border bg-muted/40 text-primary">
52
56
  <LandingIcon name={item.icon ?? "boxes"} size={18} />
53
57
  </div>
54
58
  {item.latency ? (
55
- <span className="landing-latency-badge">{item.latency}</span>
59
+ <span className="rounded-md bg-primary/10 px-[0.4375rem] py-[0.1875rem] font-mono text-[0.6875rem] font-semibold text-primary">
60
+ {item.latency}
61
+ </span>
56
62
  ) : null}
57
63
  </div>
58
- <h3 className="landing-display mb-1 text-[0.9375rem] font-semibold">
64
+ <h3 className="mb-1 text-[0.9375rem] font-semibold">
59
65
  {item.name}
60
66
  </h3>
61
- <p className="landing-mono landing-muted-3 text-[0.71875rem]">
67
+ <p className="font-mono text-[0.71875rem] text-muted-foreground/70">
62
68
  {item.path}
63
69
  </p>
64
70
  </Link>
@@ -12,24 +12,27 @@ export const CodeExampleCard = ({ example }: CodeExampleCardProps) => {
12
12
  const language = example.language ?? "bash";
13
13
 
14
14
  return (
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]">
15
+ <div className="overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
16
+ <div className="flex items-center gap-2 border-b border-border bg-muted/40 px-4 py-3">
17
+ <span
18
+ className="size-[11px] rounded-full bg-destructive/80"
19
+ aria-hidden
20
+ />
21
+ <span
22
+ className="size-[11px] rounded-full bg-warning"
23
+ aria-hidden
24
+ />
25
+ <span className="size-[11px] rounded-full bg-primary" aria-hidden />
26
+ <span className="ms-2 font-mono text-xs text-muted-foreground">
24
27
  {filename}
25
28
  </span>
26
29
  <TerminalIcon
27
30
  size={14}
28
- className="ms-auto text-[#4b5162]"
31
+ className="ms-auto text-muted-foreground/70"
29
32
  aria-hidden
30
33
  />
31
34
  </div>
32
- <pre className="landing-mono overflow-x-auto p-[1.125rem] text-[0.78125rem] leading-[1.85] text-[#cdd3e0]">
35
+ <pre className="overflow-x-auto p-[1.125rem] font-mono text-[0.78125rem] leading-[1.85] text-foreground">
33
36
  <code data-language={language}>{example.code}</code>
34
37
  </pre>
35
38
  </div>
@@ -23,25 +23,30 @@ export const FeaturesSection = ({
23
23
  return (
24
24
  <section className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
25
25
  <div className="mb-11 max-w-[640px]">
26
- <p className="landing-section-label">{label}</p>
27
- <h2 className="landing-display mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
26
+ <p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
27
+ {label}
28
+ </p>
29
+ <h2 className="mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
28
30
  {title}
29
31
  </h2>
30
- <p className="landing-muted text-[1.0625rem] leading-relaxed">
32
+ <p className="text-[1.0625rem] leading-relaxed text-muted-foreground">
31
33
  {description}
32
34
  </p>
33
35
  </div>
34
36
 
35
37
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
36
38
  {features.map((feature) => (
37
- <article key={feature.title} className="landing-feature-card">
38
- <div className="landing-feature-icon">
39
+ <article
40
+ key={feature.title}
41
+ className="rounded-[14px] border border-border bg-card p-6 transition-colors hover:border-muted-foreground/30"
42
+ >
43
+ <div className="mb-4 grid size-[2.625rem] place-items-center rounded-[11px] bg-primary/10 text-primary">
39
44
  <LandingIcon name={feature.icon} size={21} />
40
45
  </div>
41
- <h3 className="landing-display mb-2 text-[1.09375rem] font-semibold">
46
+ <h3 className="mb-2 text-[1.09375rem] font-semibold">
42
47
  {feature.title}
43
48
  </h3>
44
- <p className="landing-muted text-[0.90625rem] leading-relaxed">
49
+ <p className="text-[0.90625rem] leading-relaxed text-muted-foreground">
45
50
  {feature.description}
46
51
  </p>
47
52
  </article>
@@ -0,0 +1,150 @@
1
+ import type { ResolvedLandingContent } from "@lukoweb/apitogo";
2
+ import { useZudoku } from "@lukoweb/apitogo/components";
3
+ import type { ReactElement } from "react";
4
+
5
+ export type FooterSectionProps = {
6
+ footer: NonNullable<ResolvedLandingContent["footer"]>;
7
+ };
8
+
9
+ const GitHubIcon = ({ className }: { className?: string }) => (
10
+ <svg
11
+ viewBox="0 0 24 24"
12
+ fill="currentColor"
13
+ className={className}
14
+ aria-hidden
15
+ >
16
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z" />
17
+ </svg>
18
+ );
19
+
20
+ const XIcon = ({ className }: { className?: string }) => (
21
+ <svg
22
+ viewBox="0 0 24 24"
23
+ fill="currentColor"
24
+ className={className}
25
+ aria-hidden
26
+ >
27
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
28
+ </svg>
29
+ );
30
+
31
+ const SOCIAL_ICONS: Record<
32
+ string,
33
+ (props: { className?: string }) => ReactElement
34
+ > = {
35
+ github: GitHubIcon,
36
+ x: XIcon,
37
+ twitter: XIcon,
38
+ };
39
+
40
+ const FooterSocialLink = ({
41
+ icon,
42
+ href,
43
+ label,
44
+ }: {
45
+ icon: string;
46
+ href: string;
47
+ label?: string;
48
+ }) => {
49
+ const Icon = SOCIAL_ICONS[icon.toLowerCase()];
50
+ const accessibleLabel = label ?? icon;
51
+
52
+ return (
53
+ <a
54
+ href={href}
55
+ target="_blank"
56
+ rel="noopener noreferrer"
57
+ aria-label={accessibleLabel}
58
+ className="inline-flex items-center justify-center text-muted-foreground/70 transition-colors hover:text-foreground"
59
+ >
60
+ {Icon ? (
61
+ <Icon className="h-[1.125rem] w-[1.125rem]" />
62
+ ) : (
63
+ <span className="text-xs font-medium">{accessibleLabel}</span>
64
+ )}
65
+ </a>
66
+ );
67
+ };
68
+
69
+ export const FooterSection = ({ footer }: FooterSectionProps) => {
70
+ const { options } = useZudoku();
71
+ const siteTitle =
72
+ options.metadata?.applicationName ?? options.site?.title ?? "APIToGo";
73
+
74
+ const brandName = footer.brand?.name ?? siteTitle;
75
+ const tagline = footer.brand?.tagline;
76
+ const logoSrc = footer.brand?.logoSrc;
77
+ const columns = footer.columns ?? [];
78
+ const social = footer.social ?? [];
79
+ const year = new Date().getFullYear();
80
+ const copyright =
81
+ footer.copyright ?? `© ${year} ${brandName}. All rights reserved.`;
82
+
83
+ if (columns.length === 0 && !tagline && !logoSrc && social.length === 0) {
84
+ return null;
85
+ }
86
+
87
+ return (
88
+ <footer className="border-t bg-background">
89
+ <div className="mx-auto max-w-[1200px] px-6 py-12">
90
+ <div className="grid gap-10 sm:grid-cols-2 lg:grid-cols-[1.4fr_repeat(3,minmax(0,1fr))]">
91
+ <div className="sm:col-span-2 lg:col-span-1">
92
+ <div className="mb-3 flex items-center gap-2.5">
93
+ {logoSrc ? (
94
+ <img
95
+ src={logoSrc}
96
+ alt={brandName}
97
+ className="h-7 w-auto object-contain"
98
+ />
99
+ ) : (
100
+ <span className="text-[1.125rem] font-bold tracking-[-0.02em]">
101
+ <span className="text-primary">API</span>
102
+ <span>ToGo</span>
103
+ </span>
104
+ )}
105
+ </div>
106
+ {tagline ? (
107
+ <p className="max-w-[280px] text-[0.875rem] leading-relaxed text-muted-foreground">
108
+ {tagline}
109
+ </p>
110
+ ) : null}
111
+ </div>
112
+
113
+ {columns.map((column) => (
114
+ <div key={column.title}>
115
+ <h3 className="mb-4 text-sm font-semibold">{column.title}</h3>
116
+ <ul className="space-y-2.5">
117
+ {column.links.map((link) => (
118
+ <li key={`${column.title}-${link.label}`}>
119
+ <a
120
+ href={link.href}
121
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
122
+ >
123
+ {link.label}
124
+ </a>
125
+ </li>
126
+ ))}
127
+ </ul>
128
+ </div>
129
+ ))}
130
+ </div>
131
+
132
+ <div className="mt-10 flex flex-col items-center justify-between gap-4 border-t border-border pt-6 sm:flex-row">
133
+ <p className="text-sm text-muted-foreground/70">{copyright}</p>
134
+ {social.length > 0 ? (
135
+ <div className="flex items-center gap-4">
136
+ {social.map((item) => (
137
+ <FooterSocialLink
138
+ key={`${item.icon}-${item.href}`}
139
+ icon={item.icon}
140
+ href={item.href}
141
+ label={item.label}
142
+ />
143
+ ))}
144
+ </div>
145
+ ) : null}
146
+ </div>
147
+ </div>
148
+ </footer>
149
+ );
150
+ };
@@ -19,10 +19,10 @@ export const HeroSection = ({
19
19
  const highlights = hero.highlights ?? [];
20
20
 
21
21
  return (
22
- <section className="landing-hero-section relative overflow-hidden">
22
+ <section className="relative overflow-hidden border-b border-border">
23
23
  <div
24
24
  aria-hidden
25
- className="landing-hero-gradient pointer-events-none absolute inset-0"
25
+ className="pointer-events-none absolute inset-0 bg-[radial-gradient(1000px_480px_at_78%_-8%,color-mix(in_oklch,var(--primary)_13%,transparent),transparent_70%)]"
26
26
  />
27
27
 
28
28
  <div
@@ -39,10 +39,10 @@ export const HeroSection = ({
39
39
  )}
40
40
  >
41
41
  {(hero.badge || hero.subtitle) && (
42
- <div className="landing-badge">
42
+ <div className="mb-[1.375rem] inline-flex w-fit items-center gap-2 rounded-full border border-border bg-card px-3 py-1.5 text-[0.78125rem] text-muted-foreground">
43
43
  {hero.badge ? (
44
- <span className="landing-status-pill">
45
- <span className="landing-status-dot" aria-hidden />
44
+ <span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 font-mono text-[0.6875rem] font-semibold text-primary">
45
+ <span className="size-1.5 rounded-full bg-primary" aria-hidden />
46
46
  {hero.badge}
47
47
  </span>
48
48
  ) : null}
@@ -50,20 +50,18 @@ export const HeroSection = ({
50
50
  </div>
51
51
  )}
52
52
 
53
- <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]">
53
+ <h1 className="mb-5 text-[2.5rem] leading-[1.02] font-bold tracking-[-0.03em] text-balance sm:text-5xl lg:text-[3.5rem]">
54
54
  {hero.title}
55
55
  {hero.titleAccent ? (
56
56
  <>
57
57
  <br />
58
- <span className="text-[var(--landing-accent)]">
59
- {hero.titleAccent}
60
- </span>
58
+ <span className="text-primary">{hero.titleAccent}</span>
61
59
  </>
62
60
  ) : null}
63
61
  </h1>
64
62
 
65
63
  {hero.description ? (
66
- <p className="landing-muted mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty">
64
+ <p className="mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty text-muted-foreground">
67
65
  {hero.description}
68
66
  </p>
69
67
  ) : null}
@@ -91,17 +89,13 @@ export const HeroSection = ({
91
89
  )}
92
90
 
93
91
  {highlights.length > 0 ? (
94
- <ul className="landing-muted-3 mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem]">
92
+ <ul className="mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem] text-muted-foreground/70">
95
93
  {highlights.map((item) => (
96
94
  <li
97
95
  key={item}
98
96
  className="inline-flex items-center gap-[0.4375rem]"
99
97
  >
100
- <CheckIcon
101
- size={15}
102
- className="text-[var(--landing-green)]"
103
- aria-hidden
104
- />
98
+ <CheckIcon size={15} className="text-primary" aria-hidden />
105
99
  {item}
106
100
  </li>
107
101
  ))}
@@ -1,5 +1,6 @@
1
1
  import { cn } from "@lukoweb/apitogo";
2
2
  import { Link } from "@lukoweb/apitogo/components";
3
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
3
4
  import type { LucideIcon } from "lucide-react";
4
5
  import type { ReactNode } from "react";
5
6
 
@@ -33,15 +34,13 @@ export const LandingButton = ({
33
34
  );
34
35
 
35
36
  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
- )}
37
+ <Button
38
+ asChild
39
+ variant={variant === "primary" ? "default" : "outline"}
40
+ size={size === "sm" ? "sm" : "lg"}
41
+ className={cn(className)}
43
42
  >
44
- {content}
45
- </Link>
43
+ <Link to={href}>{content}</Link>
44
+ </Button>
46
45
  );
47
46
  };
@@ -14,10 +14,10 @@ export const PricingCtaSection = ({ pricingCta }: PricingCtaSectionProps) => {
14
14
 
15
15
  return (
16
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">
17
+ <h2 className="mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
18
18
  {title}
19
19
  </h2>
20
- <p className="landing-muted mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed">
20
+ <p className="mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed text-muted-foreground">
21
21
  {description}
22
22
  </p>
23
23
  <div className="flex flex-wrap items-center justify-center gap-3">
@@ -13,13 +13,16 @@ export const TrustSection = ({ trust }: TrustSectionProps) => {
13
13
  return (
14
14
  <section className="mx-auto max-w-[1200px] px-6 py-10">
15
15
  {trust.title ? (
16
- <p className="landing-muted-3 mb-[1.375rem] text-center text-[0.78125rem] font-semibold tracking-[0.08em] uppercase">
16
+ <p className="mb-[1.375rem] text-center text-[0.78125rem] font-semibold tracking-[0.08em] text-muted-foreground/70 uppercase">
17
17
  {trust.title}
18
18
  </p>
19
19
  ) : null}
20
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 key={logo} className="landing-trust-logo">
22
+ <span
23
+ key={logo}
24
+ className="text-[1.1875rem] font-semibold tracking-[-0.01em] text-muted-foreground"
25
+ >
23
26
  {logo}
24
27
  </span>
25
28
  ))}