@lukoweb/apitogo-module-landing 0.1.87 → 0.1.88

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.87",
3
+ "version": "0.1.88",
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.87",
17
+ "@lukoweb/apitogo": "0.1.88",
18
18
  "react": "19.2.4",
19
19
  "react-dom": "19.2.4",
20
20
  "typescript": "5.9.3"
package/src/index.tsx CHANGED
@@ -12,8 +12,18 @@ export { TrustSection } from "./sections/TrustSection.js";
12
12
  export type { TrustSectionProps } from "./sections/TrustSection.js";
13
13
  export { FeaturesSection } from "./sections/FeaturesSection.js";
14
14
  export type { FeaturesSectionProps } from "./sections/FeaturesSection.js";
15
+ export { StepsSection } from "./sections/StepsSection.js";
16
+ export type {
17
+ StepsSectionProps,
18
+ StepsSectionStep,
19
+ } from "./sections/StepsSection.js";
15
20
  export { ApiShowcaseSection } from "./sections/ApiShowcaseSection.js";
16
21
  export type { ApiShowcaseSectionProps } from "./sections/ApiShowcaseSection.js";
22
+ export { PlansPreviewSection } from "./sections/PlansPreviewSection.js";
23
+ export type {
24
+ PlansPreviewPlan,
25
+ PlansPreviewSectionProps,
26
+ } from "./sections/PlansPreviewSection.js";
17
27
  export { PricingCtaSection } from "./sections/PricingCtaSection.js";
18
28
  export type { PricingCtaSectionProps } from "./sections/PricingCtaSection.js";
19
29
  export { FooterSection } from "./sections/FooterSection.js";
@@ -1,11 +1,25 @@
1
1
  import type { ResolvedLandingContent } from "@lukoweb/apitogo";
2
+ import { cn } from "@lukoweb/apitogo";
2
3
  import { Link } from "@lukoweb/apitogo/components";
3
4
  import { Button } from "@lukoweb/apitogo/ui/Button.js";
4
5
  import { ArrowRightIcon } from "lucide-react";
5
6
  import { LandingIcon } from "./LandingIcon.js";
6
7
 
7
8
  export type ApiShowcaseSectionProps = {
8
- apiShowcase: NonNullable<ResolvedLandingContent["apiShowcase"]>;
9
+ apiShowcase: NonNullable<ResolvedLandingContent["apiShowcase"]> & {
10
+ layout?: "grid" | "list";
11
+ };
12
+ };
13
+
14
+ const methodTone = (method?: string) => {
15
+ const m = (method ?? "").toUpperCase();
16
+ if (m === "POST" || m === "PUT" || m === "PATCH") {
17
+ return "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300";
18
+ }
19
+ if (m === "DELETE") {
20
+ return "bg-red-50 text-red-600 dark:bg-red-500/15 dark:text-red-300";
21
+ }
22
+ return "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300";
9
23
  };
10
24
 
11
25
  export const ApiShowcaseSection = ({
@@ -18,15 +32,18 @@ export const ApiShowcaseSection = ({
18
32
 
19
33
  const browseHref = apiShowcase.browseAllHref ?? "/api";
20
34
  const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
35
+ const layout = apiShowcase.layout ?? "grid";
21
36
 
22
37
  return (
23
- <section className="border-y border-border bg-muted/30">
38
+ <section className="border-y border-border bg-background">
24
39
  <div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
25
40
  <div className="mb-8 flex flex-wrap items-end justify-between gap-5">
26
41
  <div>
27
- <p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
28
- {"// the catalog"}
29
- </p>
42
+ {layout === "grid" ? (
43
+ <p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
44
+ {"// the catalog"}
45
+ </p>
46
+ ) : null}
30
47
  <h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
31
48
  {apiShowcase.title ?? "Production APIs. One key."}
32
49
  </h2>
@@ -44,32 +61,70 @@ export const ApiShowcaseSection = ({
44
61
  </Button>
45
62
  </div>
46
63
 
47
- <div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
48
- {items.map((item) => (
49
- <Link
50
- key={`${item.name}-${item.path}`}
51
- to={browseHref}
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"
53
- >
54
- <div className="mb-3.5 flex items-center justify-between">
55
- <div className="grid size-9 place-items-center rounded-[9px] border border-border bg-muted/40 text-primary">
56
- <LandingIcon name={item.icon ?? "boxes"} size={18} />
57
- </div>
58
- {item.latency ? (
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}
64
+ {layout === "list" ? (
65
+ <div className="overflow-hidden rounded-[14px] border border-border bg-card">
66
+ {items.map((item, index) => {
67
+ const method =
68
+ "method" in item && typeof item.method === "string"
69
+ ? item.method
70
+ : undefined;
71
+ const description = item.summary ?? item.name;
72
+ return (
73
+ <div
74
+ key={`${item.name}-${item.path}`}
75
+ className={cn(
76
+ "flex flex-wrap items-center gap-3.5 px-5 py-3.5",
77
+ index < items.length - 1 && "border-b border-border",
78
+ )}
79
+ >
80
+ {method ? (
81
+ <span
82
+ className={cn(
83
+ "min-w-14 rounded-[5px] px-2 py-0.5 text-center font-mono text-[11px] font-bold",
84
+ methodTone(method),
85
+ )}
86
+ >
87
+ {method.toUpperCase()}
88
+ </span>
89
+ ) : null}
90
+ <code className="flex-1 font-mono text-[0.84375rem] text-foreground">
91
+ {item.path}
92
+ </code>
93
+ <span className="text-[0.8125rem] text-muted-foreground">
94
+ {description}
61
95
  </span>
62
- ) : null}
63
- </div>
64
- <h3 className="mb-1 text-[0.9375rem] font-semibold">
65
- {item.name}
66
- </h3>
67
- <p className="font-mono text-[0.71875rem] text-muted-foreground/70">
68
- {item.path}
69
- </p>
70
- </Link>
71
- ))}
72
- </div>
96
+ </div>
97
+ );
98
+ })}
99
+ </div>
100
+ ) : (
101
+ <div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
102
+ {items.map((item) => (
103
+ <Link
104
+ key={`${item.name}-${item.path}`}
105
+ to={browseHref}
106
+ 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"
107
+ >
108
+ <div className="mb-3.5 flex items-center justify-between">
109
+ <div className="grid size-9 place-items-center rounded-[9px] border border-border bg-muted/40 text-primary">
110
+ <LandingIcon name={item.icon ?? "boxes"} size={18} />
111
+ </div>
112
+ {item.latency ? (
113
+ <span className="rounded-md bg-primary/10 px-[0.4375rem] py-[0.1875rem] font-mono text-[0.6875rem] font-semibold text-primary">
114
+ {item.latency}
115
+ </span>
116
+ ) : null}
117
+ </div>
118
+ <h3 className="mb-1 text-[0.9375rem] font-semibold">
119
+ {item.name}
120
+ </h3>
121
+ <p className="font-mono text-[0.71875rem] text-muted-foreground/70">
122
+ {item.path}
123
+ </p>
124
+ </Link>
125
+ ))}
126
+ </div>
127
+ )}
73
128
  </div>
74
129
  </section>
75
130
  );
@@ -21,7 +21,7 @@ export const FeaturesSection = ({
21
21
  "Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.";
22
22
 
23
23
  return (
24
- <section className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
24
+ <section id="features" className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
25
25
  <div className="mb-11 max-w-[640px]">
26
26
  <p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
27
27
  {label}
@@ -98,8 +98,19 @@ export const FooterSection = ({ footer }: FooterSectionProps) => {
98
98
  />
99
99
  ) : (
100
100
  <span className="text-[1.125rem] font-bold tracking-[-0.02em]">
101
- <span className="text-primary">API</span>
102
- <span>ToGo</span>
101
+ {brandName.includes("API") && brandName !== "API ToGo" ? (
102
+ <>
103
+ {brandName.replace(/API$/, "")}
104
+ <span className="text-primary">API</span>
105
+ </>
106
+ ) : footer.brand?.name ? (
107
+ brandName
108
+ ) : (
109
+ <>
110
+ <span className="text-primary">API</span>
111
+ <span>ToGo</span>
112
+ </>
113
+ )}
103
114
  </span>
104
115
  )}
105
116
  </div>
@@ -4,18 +4,31 @@ import { ArrowRightIcon, BookOpenIcon, CheckIcon } from "lucide-react";
4
4
  import { CodeExampleCard } from "./CodeExampleCard.js";
5
5
  import { LandingButton } from "./LandingButtons.js";
6
6
 
7
+ type CodeExample = NonNullable<
8
+ NonNullable<ResolvedLandingContent["hero"]>["codeExample"]
9
+ >;
10
+
7
11
  export type HeroSectionProps = {
8
12
  hero: NonNullable<ResolvedLandingContent["hero"]>;
9
13
  primaryAction?: ResolvedLandingContent["primaryAction"];
10
14
  secondaryAction?: ResolvedLandingContent["secondaryAction"];
15
+ /** Optional stacked code cards (falls back to hero.codeExample). */
16
+ codeExamples?: CodeExample[];
11
17
  };
12
18
 
13
19
  export const HeroSection = ({
14
20
  hero,
15
21
  primaryAction,
16
22
  secondaryAction,
23
+ codeExamples,
17
24
  }: HeroSectionProps) => {
18
- const hasCodeExample = Boolean(hero.codeExample?.code);
25
+ const examples =
26
+ codeExamples && codeExamples.length > 0
27
+ ? codeExamples.filter((example) => Boolean(example.code))
28
+ : hero.codeExample?.code
29
+ ? [hero.codeExample]
30
+ : [];
31
+ const hasCodeExample = examples.length > 0;
19
32
  const highlights = hero.highlights ?? [];
20
33
 
21
34
  return (
@@ -106,9 +119,14 @@ export const HeroSection = ({
106
119
  ) : null}
107
120
  </div>
108
121
 
109
- {hasCodeExample && hero.codeExample ? (
110
- <div className="landing-fu">
111
- <CodeExampleCard example={hero.codeExample} />
122
+ {hasCodeExample ? (
123
+ <div className="landing-fu flex flex-col gap-3.5">
124
+ {examples.map((example) => (
125
+ <CodeExampleCard
126
+ key={`${example.filename ?? "example"}-${example.code.slice(0, 24)}`}
127
+ example={example}
128
+ />
129
+ ))}
112
130
  </div>
113
131
  ) : null}
114
132
  </div>
@@ -0,0 +1,76 @@
1
+ import { cn } from "@lukoweb/apitogo";
2
+ import { Link } from "@lukoweb/apitogo/components";
3
+ import { ArrowRightIcon } from "lucide-react";
4
+
5
+ export type PlansPreviewPlan = {
6
+ name: string;
7
+ price: string;
8
+ unit: string;
9
+ blurb: string;
10
+ href?: string;
11
+ featured?: boolean;
12
+ };
13
+
14
+ export type PlansPreviewSectionProps = {
15
+ title?: string;
16
+ seeAllHref?: string;
17
+ seeAllLabel?: string;
18
+ plans: PlansPreviewPlan[];
19
+ };
20
+
21
+ export const PlansPreviewSection = ({
22
+ title = "Simple, usage-based pricing",
23
+ seeAllHref = "/pricing",
24
+ seeAllLabel = "See full pricing →",
25
+ plans,
26
+ }: PlansPreviewSectionProps) => {
27
+ if (plans.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <section className="border-t border-border bg-muted/30">
33
+ <div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
34
+ <div className="mb-10 flex flex-wrap items-end justify-between gap-4">
35
+ <h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
36
+ {title}
37
+ </h2>
38
+ <Link
39
+ to={seeAllHref}
40
+ className="inline-flex items-center gap-1 text-sm font-semibold text-primary no-underline hover:underline"
41
+ >
42
+ {seeAllLabel}
43
+ {!seeAllLabel.includes("→") ? (
44
+ <ArrowRightIcon size={14} aria-hidden />
45
+ ) : null}
46
+ </Link>
47
+ </div>
48
+ <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
49
+ {plans.map((plan) => (
50
+ <article
51
+ key={plan.name}
52
+ className={cn(
53
+ "rounded-[14px] border border-border bg-card p-6",
54
+ plan.featured &&
55
+ "border-primary/50 ring-1 ring-primary/25 shadow-[0_10px_28px_rgba(13,148,136,0.18)]",
56
+ )}
57
+ >
58
+ <div className="text-sm font-bold uppercase tracking-[0.04em]">
59
+ {plan.name}
60
+ </div>
61
+ <div className="mt-2.5 text-[1.8rem] font-extrabold tracking-[-0.02em]">
62
+ {plan.price}
63
+ </div>
64
+ <div className="mb-3.5 text-[0.78125rem] text-muted-foreground">
65
+ {plan.unit}
66
+ </div>
67
+ <p className="text-[0.8125rem] leading-relaxed text-muted-foreground">
68
+ {plan.blurb}
69
+ </p>
70
+ </article>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ </section>
75
+ );
76
+ };
@@ -4,14 +4,54 @@ import { LandingButton } from "./LandingButtons.js";
4
4
 
5
5
  export type PricingCtaSectionProps = {
6
6
  pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
7
+ variant?: "default" | "banner";
7
8
  };
8
9
 
9
- export const PricingCtaSection = ({ pricingCta }: PricingCtaSectionProps) => {
10
+ export const PricingCtaSection = ({
11
+ pricingCta,
12
+ variant = "default",
13
+ }: PricingCtaSectionProps) => {
10
14
  const title = pricingCta.title ?? "Start free. Scale when you do.";
11
15
  const description =
12
16
  pricingCta.description ??
13
17
  "Generous free tier. Upgrade only when your app takes off.";
14
18
 
19
+ if (variant === "banner") {
20
+ return (
21
+ <section className="relative overflow-hidden bg-foreground text-background">
22
+ <div
23
+ aria-hidden
24
+ className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,color-mix(in_oklch,var(--primary)_30%,transparent),transparent_55%)]"
25
+ />
26
+ <div className="relative mx-auto max-w-[640px] px-6 py-[4.5rem] text-center">
27
+ <h2 className="mb-3.5 text-[2.125rem] leading-tight font-bold tracking-[-0.025em] text-balance">
28
+ {title}
29
+ </h2>
30
+ <p className="mx-auto mb-7 max-w-[520px] text-lg leading-relaxed text-background/70">
31
+ {description}
32
+ </p>
33
+ <div className="flex flex-wrap items-center justify-center gap-3">
34
+ {pricingCta.primaryAction ? (
35
+ <LandingButton
36
+ href={pricingCta.primaryAction.href}
37
+ label={pricingCta.primaryAction.label}
38
+ icon={ArrowRightIcon}
39
+ variant="primary"
40
+ />
41
+ ) : null}
42
+ {pricingCta.secondaryAction ? (
43
+ <LandingButton
44
+ href={pricingCta.secondaryAction.href}
45
+ label={pricingCta.secondaryAction.label}
46
+ variant="secondary"
47
+ />
48
+ ) : null}
49
+ </div>
50
+ </div>
51
+ </section>
52
+ );
53
+ }
54
+
15
55
  return (
16
56
  <section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
17
57
  <h2 className="mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
@@ -0,0 +1,42 @@
1
+ export type StepsSectionStep = {
2
+ step: string;
3
+ title: string;
4
+ description: string;
5
+ };
6
+
7
+ export type StepsSectionProps = {
8
+ title?: string;
9
+ steps: StepsSectionStep[];
10
+ };
11
+
12
+ export const StepsSection = ({
13
+ title = "Three steps to your first request",
14
+ steps,
15
+ }: StepsSectionProps) => {
16
+ if (steps.length === 0) {
17
+ return null;
18
+ }
19
+
20
+ return (
21
+ <section className="border-y border-border bg-muted/30">
22
+ <div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
23
+ <h2 className="mb-10 text-center text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
24
+ {title}
25
+ </h2>
26
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
27
+ {steps.map((item) => (
28
+ <div key={item.step}>
29
+ <div className="font-mono text-[2rem] font-bold text-primary">
30
+ {item.step}
31
+ </div>
32
+ <h3 className="mt-2 mb-1.5 text-lg font-semibold">{item.title}</h3>
33
+ <p className="text-sm leading-relaxed text-muted-foreground">
34
+ {item.description}
35
+ </p>
36
+ </div>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ </section>
41
+ );
42
+ };