@lukoweb/apitogo-module-landing 0.2.23 → 0.2.25

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.
@@ -1,161 +1,161 @@
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 ?? "API ToGo";
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
- {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
- )}
114
- </span>
115
- )}
116
- </div>
117
- {tagline ? (
118
- <p className="max-w-[280px] text-[0.875rem] leading-relaxed text-muted-foreground">
119
- {tagline}
120
- </p>
121
- ) : null}
122
- </div>
123
-
124
- {columns.map((column) => (
125
- <div key={column.title}>
126
- <h3 className="mb-4 text-sm font-semibold">{column.title}</h3>
127
- <ul className="space-y-2.5">
128
- {column.links.map((link) => (
129
- <li key={`${column.title}-${link.label}`}>
130
- <a
131
- href={link.href}
132
- className="text-sm text-muted-foreground transition-colors hover:text-foreground"
133
- >
134
- {link.label}
135
- </a>
136
- </li>
137
- ))}
138
- </ul>
139
- </div>
140
- ))}
141
- </div>
142
-
143
- <div className="mt-10 flex flex-col items-center justify-between gap-4 border-t border-border pt-6 sm:flex-row">
144
- <p className="text-sm text-muted-foreground/70">{copyright}</p>
145
- {social.length > 0 ? (
146
- <div className="flex items-center gap-4">
147
- {social.map((item) => (
148
- <FooterSocialLink
149
- key={`${item.icon}-${item.href}`}
150
- icon={item.icon}
151
- href={item.href}
152
- label={item.label}
153
- />
154
- ))}
155
- </div>
156
- ) : null}
157
- </div>
158
- </div>
159
- </footer>
160
- );
161
- };
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 ?? "API ToGo";
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
+ {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
+ )}
114
+ </span>
115
+ )}
116
+ </div>
117
+ {tagline ? (
118
+ <p className="max-w-[280px] text-[0.875rem] leading-relaxed text-muted-foreground">
119
+ {tagline}
120
+ </p>
121
+ ) : null}
122
+ </div>
123
+
124
+ {columns.map((column) => (
125
+ <div key={column.title}>
126
+ <h3 className="mb-4 text-sm font-semibold">{column.title}</h3>
127
+ <ul className="space-y-2.5">
128
+ {column.links.map((link) => (
129
+ <li key={`${column.title}-${link.label}`}>
130
+ <a
131
+ href={link.href}
132
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
133
+ >
134
+ {link.label}
135
+ </a>
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ </div>
140
+ ))}
141
+ </div>
142
+
143
+ <div className="mt-10 flex flex-col items-center justify-between gap-4 border-t border-border pt-6 sm:flex-row">
144
+ <p className="text-sm text-muted-foreground/70">{copyright}</p>
145
+ {social.length > 0 ? (
146
+ <div className="flex items-center gap-4">
147
+ {social.map((item) => (
148
+ <FooterSocialLink
149
+ key={`${item.icon}-${item.href}`}
150
+ icon={item.icon}
151
+ href={item.href}
152
+ label={item.label}
153
+ />
154
+ ))}
155
+ </div>
156
+ ) : null}
157
+ </div>
158
+ </div>
159
+ </footer>
160
+ );
161
+ };
@@ -1,135 +1,135 @@
1
- import type { ResolvedLandingContent } from "@lukoweb/apitogo";
2
- import { cn } from "@lukoweb/apitogo";
3
- import { ArrowRightIcon, BookOpenIcon, CheckIcon } from "lucide-react";
4
- import { CodeExampleCard } from "./CodeExampleCard.js";
5
- import { LandingButton } from "./LandingButtons.js";
6
-
7
- type CodeExample = NonNullable<
8
- NonNullable<ResolvedLandingContent["hero"]>["codeExample"]
9
- >;
10
-
11
- export type HeroSectionProps = {
12
- hero: NonNullable<ResolvedLandingContent["hero"]>;
13
- primaryAction?: ResolvedLandingContent["primaryAction"];
14
- secondaryAction?: ResolvedLandingContent["secondaryAction"];
15
- /** Optional stacked code cards (falls back to hero.codeExample). */
16
- codeExamples?: CodeExample[];
17
- };
18
-
19
- export const HeroSection = ({
20
- hero,
21
- primaryAction,
22
- secondaryAction,
23
- codeExamples,
24
- }: HeroSectionProps) => {
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;
32
- const highlights = hero.highlights ?? [];
33
-
34
- return (
35
- <section className="relative overflow-hidden border-b border-border">
36
- <div
37
- aria-hidden
38
- 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%)]"
39
- />
40
-
41
- <div
42
- className={
43
- hasCodeExample
44
- ? "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"
45
- : "relative mx-auto flex max-w-[1200px] flex-col gap-10 px-6 py-16 lg:px-6 lg:py-24"
46
- }
47
- >
48
- <div
49
- className={cn(
50
- "landing-fu flex flex-col",
51
- !hasCodeExample && "mx-auto max-w-3xl items-center text-center",
52
- )}
53
- >
54
- {(hero.badge || hero.subtitle) && (
55
- <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">
56
- {hero.badge ? (
57
- <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">
58
- <span
59
- className="size-1.5 rounded-full bg-primary"
60
- aria-hidden
61
- />
62
- {hero.badge}
63
- </span>
64
- ) : null}
65
- {hero.subtitle ? <span>{hero.subtitle}</span> : null}
66
- </div>
67
- )}
68
-
69
- <h1 className="mb-5 text-[2.5rem] leading-[1.02] font-bold tracking-[-0.03em] text-balance sm:text-5xl lg:text-[3.5rem]">
70
- {hero.title}
71
- {hero.titleAccent ? (
72
- <>
73
- <br />
74
- <span className="text-primary">{hero.titleAccent}</span>
75
- </>
76
- ) : null}
77
- </h1>
78
-
79
- {hero.description ? (
80
- <p className="mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty text-muted-foreground">
81
- {hero.description}
82
- </p>
83
- ) : null}
84
-
85
- {(primaryAction || secondaryAction) && (
86
- <div className="flex flex-wrap items-center gap-3">
87
- {primaryAction ? (
88
- <LandingButton
89
- href={primaryAction.href}
90
- label={primaryAction.label}
91
- icon={ArrowRightIcon}
92
- variant="primary"
93
- />
94
- ) : null}
95
- {secondaryAction ? (
96
- <LandingButton
97
- href={secondaryAction.href}
98
- label={secondaryAction.label}
99
- icon={BookOpenIcon}
100
- iconPosition="start"
101
- variant="secondary"
102
- />
103
- ) : null}
104
- </div>
105
- )}
106
-
107
- {highlights.length > 0 ? (
108
- <ul className="mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem] text-muted-foreground/70">
109
- {highlights.map((item) => (
110
- <li
111
- key={item}
112
- className="inline-flex items-center gap-[0.4375rem]"
113
- >
114
- <CheckIcon size={15} className="text-primary" aria-hidden />
115
- {item}
116
- </li>
117
- ))}
118
- </ul>
119
- ) : null}
120
- </div>
121
-
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
- ))}
130
- </div>
131
- ) : null}
132
- </div>
133
- </section>
134
- );
135
- };
1
+ import type { ResolvedLandingContent } from "@lukoweb/apitogo";
2
+ import { cn } from "@lukoweb/apitogo";
3
+ import { ArrowRightIcon, BookOpenIcon, CheckIcon } from "lucide-react";
4
+ import { CodeExampleCard } from "./CodeExampleCard.js";
5
+ import { LandingButton } from "./LandingButtons.js";
6
+
7
+ type CodeExample = NonNullable<
8
+ NonNullable<ResolvedLandingContent["hero"]>["codeExample"]
9
+ >;
10
+
11
+ export type HeroSectionProps = {
12
+ hero: NonNullable<ResolvedLandingContent["hero"]>;
13
+ primaryAction?: ResolvedLandingContent["primaryAction"];
14
+ secondaryAction?: ResolvedLandingContent["secondaryAction"];
15
+ /** Optional stacked code cards (falls back to hero.codeExample). */
16
+ codeExamples?: CodeExample[];
17
+ };
18
+
19
+ export const HeroSection = ({
20
+ hero,
21
+ primaryAction,
22
+ secondaryAction,
23
+ codeExamples,
24
+ }: HeroSectionProps) => {
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;
32
+ const highlights = hero.highlights ?? [];
33
+
34
+ return (
35
+ <section className="relative overflow-hidden border-b border-border">
36
+ <div
37
+ aria-hidden
38
+ 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%)]"
39
+ />
40
+
41
+ <div
42
+ className={
43
+ hasCodeExample
44
+ ? "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"
45
+ : "relative mx-auto flex max-w-[1200px] flex-col gap-10 px-6 py-16 lg:px-6 lg:py-24"
46
+ }
47
+ >
48
+ <div
49
+ className={cn(
50
+ "landing-fu flex flex-col",
51
+ !hasCodeExample && "mx-auto max-w-3xl items-center text-center",
52
+ )}
53
+ >
54
+ {(hero.badge || hero.subtitle) && (
55
+ <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">
56
+ {hero.badge ? (
57
+ <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">
58
+ <span
59
+ className="size-1.5 rounded-full bg-primary"
60
+ aria-hidden
61
+ />
62
+ {hero.badge}
63
+ </span>
64
+ ) : null}
65
+ {hero.subtitle ? <span>{hero.subtitle}</span> : null}
66
+ </div>
67
+ )}
68
+
69
+ <h1 className="mb-5 text-[2.5rem] leading-[1.02] font-bold tracking-[-0.03em] text-balance sm:text-5xl lg:text-[3.5rem]">
70
+ {hero.title}
71
+ {hero.titleAccent ? (
72
+ <>
73
+ <br />
74
+ <span className="text-primary">{hero.titleAccent}</span>
75
+ </>
76
+ ) : null}
77
+ </h1>
78
+
79
+ {hero.description ? (
80
+ <p className="mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty text-muted-foreground">
81
+ {hero.description}
82
+ </p>
83
+ ) : null}
84
+
85
+ {(primaryAction || secondaryAction) && (
86
+ <div className="flex flex-wrap items-center gap-3">
87
+ {primaryAction ? (
88
+ <LandingButton
89
+ href={primaryAction.href}
90
+ label={primaryAction.label}
91
+ icon={ArrowRightIcon}
92
+ variant="primary"
93
+ />
94
+ ) : null}
95
+ {secondaryAction ? (
96
+ <LandingButton
97
+ href={secondaryAction.href}
98
+ label={secondaryAction.label}
99
+ icon={BookOpenIcon}
100
+ iconPosition="start"
101
+ variant="secondary"
102
+ />
103
+ ) : null}
104
+ </div>
105
+ )}
106
+
107
+ {highlights.length > 0 ? (
108
+ <ul className="mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem] text-muted-foreground/70">
109
+ {highlights.map((item) => (
110
+ <li
111
+ key={item}
112
+ className="inline-flex items-center gap-[0.4375rem]"
113
+ >
114
+ <CheckIcon size={15} className="text-primary" aria-hidden />
115
+ {item}
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ ) : null}
120
+ </div>
121
+
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
+ ))}
130
+ </div>
131
+ ) : null}
132
+ </div>
133
+ </section>
134
+ );
135
+ };