fimo 2.1.0 → 2.2.0-staging.10
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/README.md +3 -2
- package/assets/agent-templates/_default/config.yaml +1 -0
- package/assets/agent-templates/broken-link-sweeper/config.yaml +1 -0
- package/assets/agent-templates/content-translator/config.yaml +1 -0
- package/assets/agent-templates/content-translator/scripts/translate-entries.ts +1 -1
- package/assets/agent-templates/post-deploy-check/config.yaml +1 -0
- package/assets/agent-templates/post-deploy-check/scripts/check-logs.ts +1 -1
- package/assets/agent-templates/seo-audit/config.yaml +1 -0
- package/assets/agent-templates/seo-audit/scripts/list-routes.ts +3 -3
- package/assets/skills/fimo-cli/SKILL.md +5 -5
- package/assets/skills/fimo-cli/references/agents.md +2 -1
- package/assets/skills/fimo-cli/references/assets.md +4 -1
- package/assets/skills/fimo-cli/references/branches.md +1 -1
- package/assets/skills/fimo-cli/references/content.md +3 -3
- package/assets/skills/fimo-cli/references/deploy.md +2 -2
- package/assets/skills/fimo-cli/references/forms.md +3 -1
- package/assets/skills/fimo-cli/references/history.md +1 -0
- package/assets/skills/fimo-cli/references/init.md +30 -39
- package/assets/skills/fimo-cli/references/preview.md +5 -3
- package/assets/skills/fimo-cli/references/schedules.md +4 -2
- package/assets/skills/fimo-cli/references/webhooks.md +2 -0
- package/assets/skills/fimo-code/references/frameworks/next.md +4 -1
- package/assets/skills/fimo-code/references/frameworks/react-router.md +4 -3
- package/assets/skills/fimo-migration/SKILL.md +3 -3
- package/assets/skills/fimo-studio/references/preview-and-responsive.md +4 -1
- package/assets/skills/fimo-studio/references/visual-editing.md +9 -1
- package/dist/build/astro/config.js +2 -1
- package/dist/build/vite/plugins/preview-health.d.ts +4 -0
- package/dist/build/vite/plugins/preview-health.js +40 -0
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +136 -137
- package/dist/runtime/astro/server-runtime.js +7 -3
- package/dist/runtime/content/client/collection-core.js +3 -2
- package/dist/runtime/content/client/core-types.d.ts +4 -3
- package/dist/runtime/content/client/internal.d.ts +2 -0
- package/dist/runtime/content/client/internal.js +54 -25
- package/dist/runtime/content/client/singleton-core.js +2 -1
- package/dist/runtime/content/index.d.ts +1 -1
- package/dist/runtime/content/index.js +1 -1
- package/dist/runtime/paths/get-fimo-paths.js +7 -3
- package/dist/runtime/primitives/lib/index.d.ts +1 -1
- package/dist/runtime/primitives/lib/index.js +1 -1
- package/dist/runtime/primitives/lib/primitives.d.ts +24 -5
- package/dist/runtime/primitives/lib/primitives.js +69 -16
- package/dist/runtime/primitives/lib/template.js +2 -2
- package/dist/runtime/shared/branch-identity.d.ts +56 -0
- package/dist/runtime/shared/{env-identity.js → branch-identity.js} +40 -27
- package/dist/runtime/shared/local-runtime-env.d.ts +2 -2
- package/dist/runtime/shared/local-runtime-env.js +12 -10
- package/dist/runtime/templates.d.ts +8 -2
- package/dist/runtime/templates.js +34 -8
- package/package.json +13 -9
- package/release.json +2 -2
- package/templates/astro/package.json +2 -2
- package/templates/nextjs/AGENTS.md +1 -1
- package/templates/nextjs/package.json +4 -4
- package/templates/react-router/.fimo/config.json +1 -2
- package/templates/react-router/AGENTS.md +1 -1
- package/templates/react-router/package.json +4 -4
- package/templates/sveltekit/package.json +2 -2
- package/templates/vite/package.json +4 -4
- package/assets/starters/fimo-site/manifest.json +0 -64
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/GOAL.md +0 -8
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/capabilities.yaml +0 -1
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/config.yaml +0 -9
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/GOAL.md +0 -14
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/capabilities.yaml +0 -3
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/config.yaml +0 -9
- package/assets/starters/fimo-site/template/src/forms/contact.json +0 -14
- package/assets/starters/fimo-site/template/src/forms/contact.ts +0 -46
- package/assets/starters/fimo-site/template/src/pages/Contact.tsx +0 -193
- package/assets/starters/fimo-site/template/src/pages/Index.tsx +0 -27
- package/assets/starters/fimo-site/template/src/pages/SiteContent.tsx +0 -191
- package/assets/starters/fimo-site/template/src/schemas/HomePage.json +0 -28
- package/assets/starters/fimo-site/template/src/schemas/HomePage.ts +0 -29
- package/assets/starters/fimo-site/template/src/schemas/HomePage.types.ts +0 -64
- package/dist/runtime/shared/env-identity.d.ts +0 -43
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "contact",
|
|
3
|
-
"label": "Start a conversation",
|
|
4
|
-
"fields": {
|
|
5
|
-
"name": { "type": "text", "label": "Your name", "required": true },
|
|
6
|
-
"email": { "type": "email", "label": "Email address", "required": true },
|
|
7
|
-
"message": {
|
|
8
|
-
"type": "textarea",
|
|
9
|
-
"label": "What are you building?",
|
|
10
|
-
"required": true,
|
|
11
|
-
"validation": { "minLength": 10, "maxLength": 2000 }
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED ⚠️ — do not edit by hand
|
|
2
|
-
// Source: src/forms/contact.json
|
|
3
|
-
// The CLI replaces the placeholder form id while seeding the starter. `fimo
|
|
4
|
-
// validate` regenerates this file from the JSON spec on later edits.
|
|
5
|
-
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
|
|
8
|
-
export const FORM_ID = '__FIMO_FORM_ID__';
|
|
9
|
-
|
|
10
|
-
export const ContactSchema = z.object({
|
|
11
|
-
name: z.string().min(1, 'Your name is required'),
|
|
12
|
-
email: z.string().min(1, 'Email address is required').email('Invalid email address'),
|
|
13
|
-
message: z.string().min(1, 'What are you building? is required').min(10).max(2000),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export type ContactData = z.infer<typeof ContactSchema>;
|
|
17
|
-
|
|
18
|
-
// @ts-ignore - meta.env is a vite feature
|
|
19
|
-
const base = import.meta.env.VITE_API_URL;
|
|
20
|
-
|
|
21
|
-
export async function submitContact(data: ContactData): Promise<{
|
|
22
|
-
success: boolean;
|
|
23
|
-
errors?: Record<string, string>;
|
|
24
|
-
}> {
|
|
25
|
-
let response: Response;
|
|
26
|
-
try {
|
|
27
|
-
response = await fetch(`${base}/forms/${FORM_ID}/submit`, {
|
|
28
|
-
method: 'POST',
|
|
29
|
-
headers: { 'Content-Type': 'application/json' },
|
|
30
|
-
body: JSON.stringify(data),
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
return { success: false };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (response.ok) {
|
|
37
|
-
return { success: true };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (response.status === 422) {
|
|
41
|
-
const json = (await response.json()) as { errors?: Record<string, string> };
|
|
42
|
-
return { success: false, errors: json.errors };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { success: false };
|
|
46
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { type FimoString, Text, useLabels } from 'fimo-react';
|
|
2
|
-
import { ArrowUpRight, Check } from 'lucide-react';
|
|
3
|
-
import { type FormEvent, type ReactNode, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
import { Button } from '@/components/ui/button';
|
|
6
|
-
import { Input } from '@/components/ui/input';
|
|
7
|
-
import { Label } from '@/components/ui/label';
|
|
8
|
-
import { Spinner } from '@/components/ui/spinner';
|
|
9
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
10
|
-
import { ContactSchema, submitContact } from '@/forms/contact';
|
|
11
|
-
import { cn } from '@/lib/utils';
|
|
12
|
-
import type { HomePage } from '@/schemas/HomePage';
|
|
13
|
-
|
|
14
|
-
import { Bar } from './SiteContent';
|
|
15
|
-
|
|
16
|
-
export function Contact({ page, loading }: { page?: HomePage; loading: boolean }) {
|
|
17
|
-
return (
|
|
18
|
-
<section id="contact" className="dark bg-background text-foreground scroll-mt-16">
|
|
19
|
-
<div className="mx-auto grid w-full max-w-6xl gap-12 px-6 py-20 lg:grid-cols-12 lg:gap-16 lg:px-10 lg:py-28">
|
|
20
|
-
<div className="lg:col-span-5">
|
|
21
|
-
{page ? (
|
|
22
|
-
<>
|
|
23
|
-
<Text
|
|
24
|
-
value={page.contactHeading}
|
|
25
|
-
as="h2"
|
|
26
|
-
className="text-4xl font-semibold tracking-[-0.03em] text-balance lg:text-5xl"
|
|
27
|
-
/>
|
|
28
|
-
<Text value={page.contactIntro} as="p" className="text-muted-foreground mt-6 max-w-md leading-relaxed" />
|
|
29
|
-
</>
|
|
30
|
-
) : (
|
|
31
|
-
<div aria-hidden>
|
|
32
|
-
<div className="grid gap-3">
|
|
33
|
-
<Bar loading={loading} className="h-10 w-4/5" />
|
|
34
|
-
<Bar loading={loading} className="h-10 w-2/5" />
|
|
35
|
-
</div>
|
|
36
|
-
<div className="mt-8 grid gap-2">
|
|
37
|
-
<Bar loading={loading} className="h-4 w-full max-w-md" />
|
|
38
|
-
<Bar loading={loading} className="h-4 w-11/12 max-w-md" />
|
|
39
|
-
<Bar loading={loading} className="h-4 w-2/3 max-w-md" />
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<div className="lg:col-span-7">
|
|
46
|
-
<ContactForm />
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</section>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type SubmitState = 'idle' | 'submitting' | 'sent' | 'invalid' | 'failed';
|
|
54
|
-
|
|
55
|
-
function ContactForm() {
|
|
56
|
-
const { t } = useLabels();
|
|
57
|
-
const [state, setState] = useState<SubmitState>('idle');
|
|
58
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
59
|
-
|
|
60
|
-
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
61
|
-
event.preventDefault();
|
|
62
|
-
// `currentTarget` is cleared once the handler yields, so keep the node.
|
|
63
|
-
const form = event.currentTarget;
|
|
64
|
-
setErrors({});
|
|
65
|
-
|
|
66
|
-
const parsed = ContactSchema.safeParse(Object.fromEntries(new FormData(form)));
|
|
67
|
-
if (!parsed.success) {
|
|
68
|
-
const fieldErrors: Record<string, string> = {};
|
|
69
|
-
for (const issue of parsed.error.issues) {
|
|
70
|
-
fieldErrors[issue.path[0] as string] = issue.message;
|
|
71
|
-
}
|
|
72
|
-
setErrors(fieldErrors);
|
|
73
|
-
setState('invalid');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
setState('submitting');
|
|
78
|
-
const result = await submitContact(parsed.data);
|
|
79
|
-
if (result.success) {
|
|
80
|
-
form.reset();
|
|
81
|
-
setState('sent');
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// A rejected submission comes back with per-field messages; a network or
|
|
86
|
-
// server failure has nothing to attach to a field, so it needs its own.
|
|
87
|
-
const fieldErrors = result.errors ?? {};
|
|
88
|
-
setErrors(fieldErrors);
|
|
89
|
-
setState(Object.keys(fieldErrors).length > 0 ? 'invalid' : 'failed');
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const busy = state === 'submitting';
|
|
93
|
-
const sent = state === 'sent';
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<form
|
|
97
|
-
onSubmit={handleSubmit}
|
|
98
|
-
noValidate
|
|
99
|
-
aria-busy={busy}
|
|
100
|
-
className="border-border bg-card rounded-xl border p-6 sm:p-8"
|
|
101
|
-
>
|
|
102
|
-
<div className="grid gap-6 sm:grid-cols-2">
|
|
103
|
-
<FormField id="contact-name" label={t('contact.name')} error={errors.name}>
|
|
104
|
-
<Input
|
|
105
|
-
id="contact-name"
|
|
106
|
-
name="name"
|
|
107
|
-
type="text"
|
|
108
|
-
autoComplete="name"
|
|
109
|
-
disabled={busy || sent}
|
|
110
|
-
aria-invalid={Boolean(errors.name)}
|
|
111
|
-
aria-describedby={errors.name ? 'contact-name-error' : undefined}
|
|
112
|
-
/>
|
|
113
|
-
</FormField>
|
|
114
|
-
|
|
115
|
-
<FormField id="contact-email" label={t('contact.email')} error={errors.email}>
|
|
116
|
-
<Input
|
|
117
|
-
id="contact-email"
|
|
118
|
-
name="email"
|
|
119
|
-
type="email"
|
|
120
|
-
autoComplete="email"
|
|
121
|
-
disabled={busy || sent}
|
|
122
|
-
aria-invalid={Boolean(errors.email)}
|
|
123
|
-
aria-describedby={errors.email ? 'contact-email-error' : undefined}
|
|
124
|
-
/>
|
|
125
|
-
</FormField>
|
|
126
|
-
|
|
127
|
-
<div className="sm:col-span-2">
|
|
128
|
-
<FormField id="contact-message" label={t('contact.message')} error={errors.message}>
|
|
129
|
-
<Textarea
|
|
130
|
-
id="contact-message"
|
|
131
|
-
name="message"
|
|
132
|
-
rows={5}
|
|
133
|
-
disabled={busy || sent}
|
|
134
|
-
aria-invalid={Boolean(errors.message)}
|
|
135
|
-
aria-describedby={errors.message ? 'contact-message-error' : undefined}
|
|
136
|
-
/>
|
|
137
|
-
</FormField>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div className="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
142
|
-
<Button
|
|
143
|
-
type="submit"
|
|
144
|
-
size="lg"
|
|
145
|
-
disabled={busy || sent}
|
|
146
|
-
className={cn('group', state === 'invalid' && 'ring-destructive/40 ring-[3px]')}
|
|
147
|
-
>
|
|
148
|
-
{busy ? <Spinner /> : null}
|
|
149
|
-
{sent ? <Check /> : null}
|
|
150
|
-
<Text value={t('contact.submit')} />
|
|
151
|
-
{!busy && !sent ? (
|
|
152
|
-
<ArrowUpRight className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
153
|
-
) : null}
|
|
154
|
-
</Button>
|
|
155
|
-
|
|
156
|
-
{/* Both regions stay mounted and empty: a live region inserted at the same
|
|
157
|
-
moment as its text is announced unreliably. */}
|
|
158
|
-
<p role="status" className="text-muted-foreground text-sm">
|
|
159
|
-
{sent ? <Text value={t('contact.sent')} /> : null}
|
|
160
|
-
</p>
|
|
161
|
-
<p role="alert" className="text-destructive text-sm">
|
|
162
|
-
{state === 'failed' ? <Text value={t('contact.failed')} /> : null}
|
|
163
|
-
</p>
|
|
164
|
-
</div>
|
|
165
|
-
</form>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function FormField({
|
|
170
|
-
id,
|
|
171
|
-
label,
|
|
172
|
-
error,
|
|
173
|
-
children,
|
|
174
|
-
}: {
|
|
175
|
-
id: string;
|
|
176
|
-
label: FimoString;
|
|
177
|
-
error?: string;
|
|
178
|
-
children: ReactNode;
|
|
179
|
-
}) {
|
|
180
|
-
return (
|
|
181
|
-
<div className="grid gap-2">
|
|
182
|
-
<Label htmlFor={id} className="text-muted-foreground text-[11px] font-medium tracking-[0.16em] uppercase">
|
|
183
|
-
<Text value={label} />
|
|
184
|
-
</Label>
|
|
185
|
-
{children}
|
|
186
|
-
{error ? (
|
|
187
|
-
<p id={`${id}-error`} role="alert" className="text-destructive text-xs">
|
|
188
|
-
{error}
|
|
189
|
-
</p>
|
|
190
|
-
) : null}
|
|
191
|
-
</div>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import HomePageContent from '@/schemas/HomePage';
|
|
2
|
-
|
|
3
|
-
import { Contact } from './Contact';
|
|
4
|
-
import { ContentFallback, Hero, SiteHeader, Stories } from './SiteContent';
|
|
5
|
-
|
|
6
|
-
const Index = () => {
|
|
7
|
-
const { data: page, isLoading } = HomePageContent.useGet();
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<div className="bg-background text-foreground min-h-screen">
|
|
11
|
-
<SiteHeader />
|
|
12
|
-
<main>
|
|
13
|
-
{page ? (
|
|
14
|
-
<>
|
|
15
|
-
<Hero page={page} />
|
|
16
|
-
<Stories items={page.featured ?? []} />
|
|
17
|
-
</>
|
|
18
|
-
) : (
|
|
19
|
-
<ContentFallback loading={isLoading} />
|
|
20
|
-
)}
|
|
21
|
-
<Contact page={page} loading={isLoading} />
|
|
22
|
-
</main>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default Index;
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { RichText, Text, useLabels } from 'fimo-react';
|
|
2
|
-
import { ArrowDown, ArrowUpRight } from 'lucide-react';
|
|
3
|
-
|
|
4
|
-
import { Button } from '@/components/ui/button';
|
|
5
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
6
|
-
import { cn } from '@/lib/utils';
|
|
7
|
-
import type { HomePage } from '@/schemas/HomePage';
|
|
8
|
-
|
|
9
|
-
export function SiteHeader() {
|
|
10
|
-
const { t } = useLabels();
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<header className="border-border/70 bg-background/85 sticky top-0 z-30 border-b backdrop-blur">
|
|
14
|
-
<div className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between gap-6 px-6 lg:px-10">
|
|
15
|
-
<Mark />
|
|
16
|
-
<nav className="flex min-w-0 items-center gap-5">
|
|
17
|
-
<a
|
|
18
|
-
href="#stories"
|
|
19
|
-
className="text-muted-foreground hover:text-foreground hidden truncate text-sm transition-colors sm:inline"
|
|
20
|
-
>
|
|
21
|
-
<Text value={t('nav.stories')} />
|
|
22
|
-
</a>
|
|
23
|
-
<Button asChild size="sm" variant="outline" className="min-w-0 shrink">
|
|
24
|
-
<a href="#contact">
|
|
25
|
-
<Text value={t('nav.contact')} className="truncate" />
|
|
26
|
-
</a>
|
|
27
|
-
</Button>
|
|
28
|
-
</nav>
|
|
29
|
-
</div>
|
|
30
|
-
</header>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function Hero({ page }: { page: HomePage }) {
|
|
35
|
-
const { t } = useLabels();
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<section className="border-border/70 border-b">
|
|
39
|
-
<div className="mx-auto w-full max-w-6xl px-6 py-20 lg:px-10 lg:py-32">
|
|
40
|
-
<p className="text-muted-foreground flex items-center gap-4 text-[11px] font-semibold tracking-[0.24em] uppercase">
|
|
41
|
-
<span aria-hidden className="bg-primary h-px w-10 shrink-0" />
|
|
42
|
-
<Text value={page.eyebrow} />
|
|
43
|
-
</p>
|
|
44
|
-
|
|
45
|
-
<div className="mt-10 grid gap-12 lg:mt-14 lg:grid-cols-12 lg:gap-16">
|
|
46
|
-
<Text
|
|
47
|
-
value={page.title}
|
|
48
|
-
as="h1"
|
|
49
|
-
className="text-5xl leading-[0.95] font-semibold tracking-[-0.04em] text-balance sm:text-6xl lg:col-span-7 lg:text-7xl"
|
|
50
|
-
/>
|
|
51
|
-
|
|
52
|
-
<div className="lg:col-span-5 lg:pt-3">
|
|
53
|
-
<RichText
|
|
54
|
-
value={page.intro}
|
|
55
|
-
className="text-muted-foreground max-w-md text-lg leading-relaxed [&>p+p]:mt-4"
|
|
56
|
-
/>
|
|
57
|
-
|
|
58
|
-
<div className="mt-10 flex flex-wrap items-center gap-x-8 gap-y-4">
|
|
59
|
-
<Button asChild size="lg" className="group">
|
|
60
|
-
<a href={String(page.primaryCtaHref)}>
|
|
61
|
-
<Text value={page.primaryCtaLabel} />
|
|
62
|
-
<ArrowUpRight className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
63
|
-
</a>
|
|
64
|
-
</Button>
|
|
65
|
-
<a
|
|
66
|
-
href="#stories"
|
|
67
|
-
className="text-muted-foreground hover:text-foreground group inline-flex items-center gap-2 text-sm transition-colors"
|
|
68
|
-
>
|
|
69
|
-
<Text value={t('hero.scroll')} />
|
|
70
|
-
<ArrowDown className="size-4 transition-transform group-hover:translate-y-0.5" />
|
|
71
|
-
</a>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</section>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function Stories({ items }: { items: HomePage['featured'] }) {
|
|
81
|
-
const { t } = useLabels();
|
|
82
|
-
|
|
83
|
-
if (items.length === 0) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<section id="stories" aria-labelledby="stories-title" className="border-border/70 scroll-mt-16 border-b">
|
|
89
|
-
<div className="mx-auto w-full max-w-6xl px-6 lg:px-10">
|
|
90
|
-
<Text
|
|
91
|
-
value={t('nav.stories')}
|
|
92
|
-
as="h2"
|
|
93
|
-
id="stories-title"
|
|
94
|
-
className="text-muted-foreground pt-16 text-[11px] font-semibold tracking-[0.24em] uppercase lg:pt-24"
|
|
95
|
-
/>
|
|
96
|
-
<ul className="divide-border/70 border-border/70 mt-8 divide-y border-t">
|
|
97
|
-
{items.map((item, index) => (
|
|
98
|
-
<li key={item._key}>
|
|
99
|
-
<a
|
|
100
|
-
href={item.href}
|
|
101
|
-
className="focus-visible:ring-ring/50 group grid gap-5 rounded-md py-10 outline-none focus-visible:ring-[3px] md:grid-cols-12 md:gap-8 md:py-14"
|
|
102
|
-
>
|
|
103
|
-
<div className="flex items-baseline gap-4 md:col-span-4">
|
|
104
|
-
<span className="text-primary font-mono text-xs tabular-nums">
|
|
105
|
-
{String(index + 1).padStart(2, '0')}
|
|
106
|
-
</span>
|
|
107
|
-
<Text value={item.label} className="text-sm font-medium" />
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div className="flex items-start gap-6 md:col-span-8">
|
|
111
|
-
<div className="min-w-0 transition-transform duration-300 group-hover:translate-x-1">
|
|
112
|
-
<Text
|
|
113
|
-
value={item.title}
|
|
114
|
-
as="h3"
|
|
115
|
-
className="text-2xl font-semibold tracking-[-0.02em] text-balance md:text-3xl"
|
|
116
|
-
/>
|
|
117
|
-
<Text
|
|
118
|
-
value={item.summary}
|
|
119
|
-
as="p"
|
|
120
|
-
className="text-muted-foreground mt-3 max-w-xl leading-relaxed whitespace-pre-line"
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
<ArrowUpRight className="text-muted-foreground/40 group-hover:text-foreground ml-auto size-5 shrink-0 transition-all duration-300 group-hover:translate-x-1 group-hover:-translate-y-1" />
|
|
124
|
-
</div>
|
|
125
|
-
</a>
|
|
126
|
-
</li>
|
|
127
|
-
))}
|
|
128
|
-
</ul>
|
|
129
|
-
</div>
|
|
130
|
-
</section>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function ContentFallback({ loading }: { loading: boolean }) {
|
|
135
|
-
return (
|
|
136
|
-
<>
|
|
137
|
-
<section aria-hidden className="border-border/70 border-b">
|
|
138
|
-
<div className="mx-auto w-full max-w-6xl px-6 py-20 lg:px-10 lg:py-32">
|
|
139
|
-
<Bar loading={loading} className="h-3 w-40" />
|
|
140
|
-
<div className="mt-10 grid gap-12 lg:mt-14 lg:grid-cols-12 lg:gap-16">
|
|
141
|
-
<div className="grid gap-4 lg:col-span-7">
|
|
142
|
-
<Bar loading={loading} className="h-14 w-full lg:h-16" />
|
|
143
|
-
<Bar loading={loading} className="h-14 w-5/6 lg:h-16" />
|
|
144
|
-
<Bar loading={loading} className="h-14 w-2/3 lg:h-16" />
|
|
145
|
-
</div>
|
|
146
|
-
<div className="grid gap-3 lg:col-span-5 lg:pt-3">
|
|
147
|
-
<Bar loading={loading} className="h-4 w-full max-w-md" />
|
|
148
|
-
<Bar loading={loading} className="h-4 w-11/12 max-w-md" />
|
|
149
|
-
<Bar loading={loading} className="h-4 w-3/4 max-w-md" />
|
|
150
|
-
<Bar loading={loading} className="mt-7 h-10 w-48" />
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
</section>
|
|
155
|
-
|
|
156
|
-
<section aria-hidden className="border-border/70 border-b">
|
|
157
|
-
<div className="mx-auto w-full max-w-6xl px-6 lg:px-10">
|
|
158
|
-
<Bar loading={loading} className="mt-16 h-3 w-28 lg:mt-24" />
|
|
159
|
-
<ul className="divide-border/70 border-border/70 mt-8 divide-y border-t">
|
|
160
|
-
{[0, 1, 2].map((row) => (
|
|
161
|
-
<li key={row} className="grid gap-5 py-10 md:grid-cols-12 md:gap-8 md:py-14">
|
|
162
|
-
<Bar loading={loading} className="h-4 w-32 md:col-span-4" />
|
|
163
|
-
<div className="grid gap-3 md:col-span-8">
|
|
164
|
-
<Bar loading={loading} className="h-8 w-3/4" />
|
|
165
|
-
<Bar loading={loading} className="h-4 w-full max-w-xl" />
|
|
166
|
-
<Bar loading={loading} className="h-4 w-2/3 max-w-xl" />
|
|
167
|
-
</div>
|
|
168
|
-
</li>
|
|
169
|
-
))}
|
|
170
|
-
</ul>
|
|
171
|
-
</div>
|
|
172
|
-
</section>
|
|
173
|
-
</>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Pulses while the request is in flight; settles into a static block once it is not. */
|
|
178
|
-
export function Bar({ loading, className }: { loading: boolean; className: string }) {
|
|
179
|
-
return loading ? <Skeleton className={className} /> : <div className={cn('bg-muted rounded-md', className)} />;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function Mark() {
|
|
183
|
-
return (
|
|
184
|
-
<span aria-hidden className="grid size-5 shrink-0 grid-cols-2 gap-[3px]">
|
|
185
|
-
<span className="bg-foreground rounded-[2px]" />
|
|
186
|
-
<span className="bg-foreground/25 rounded-[2px]" />
|
|
187
|
-
<span className="bg-foreground/25 rounded-[2px]" />
|
|
188
|
-
<span className="bg-primary rounded-[2px]" />
|
|
189
|
-
</span>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"uid": "HomePage",
|
|
3
|
-
"name": "Home page",
|
|
4
|
-
"isSingleton": true,
|
|
5
|
-
"i18n": { "enabled": false },
|
|
6
|
-
"fields": {
|
|
7
|
-
"eyebrow": { "type": "string", "required": true },
|
|
8
|
-
"title": { "type": "string", "required": true },
|
|
9
|
-
"intro": { "type": "richtext", "required": true },
|
|
10
|
-
"primaryCtaLabel": { "type": "string", "required": true },
|
|
11
|
-
"primaryCtaHref": { "type": "string", "required": true },
|
|
12
|
-
"featured": {
|
|
13
|
-
"type": "blocks",
|
|
14
|
-
"min": 1,
|
|
15
|
-
"max": 3,
|
|
16
|
-
"schema": {
|
|
17
|
-
"fields": {
|
|
18
|
-
"label": { "type": "string", "required": true },
|
|
19
|
-
"title": { "type": "string", "required": true },
|
|
20
|
-
"summary": { "type": "text", "required": true },
|
|
21
|
-
"href": { "type": "string", "required": true }
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"contactHeading": { "type": "string", "required": true },
|
|
26
|
-
"contactIntro": { "type": "text", "required": true }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by `fimo` from src/schemas/HomePage.json - DO NOT EDIT.
|
|
2
|
-
// Change content fields in the schema, then run `fimo validate`.
|
|
3
|
-
// Runtime implementation lives in the project-local fimo-react package.
|
|
4
|
-
import { createSingletonClient } from 'fimo-react';
|
|
5
|
-
import type { ContentSchema, SingletonContentClient } from 'fimo-react';
|
|
6
|
-
|
|
7
|
-
import schema from './HomePage.json';
|
|
8
|
-
import type { HomePage, HomePageInput } from './HomePage.types';
|
|
9
|
-
|
|
10
|
-
export type {
|
|
11
|
-
HomePage,
|
|
12
|
-
HomePageFields,
|
|
13
|
-
HomePageFimoMeta,
|
|
14
|
-
HomePageInput,
|
|
15
|
-
HomePagePopulate,
|
|
16
|
-
HomePageQuery,
|
|
17
|
-
HomePageReferences,
|
|
18
|
-
HomePageResult,
|
|
19
|
-
HomePageSort,
|
|
20
|
-
HomePageWhere,
|
|
21
|
-
} from './HomePage.types';
|
|
22
|
-
|
|
23
|
-
const client: SingletonContentClient<HomePage, HomePageInput> = createSingletonClient<HomePage, HomePageInput>({
|
|
24
|
-
schema: schema as unknown as ContentSchema,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export const { get, update, useGet, useSuspenseGet, useUpdate, wrapWithSource, meta } = client;
|
|
28
|
-
|
|
29
|
-
export default client;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED ⚠️ – do not edit by hand
|
|
2
|
-
import {
|
|
3
|
-
type Fields,
|
|
4
|
-
type FimoRichText,
|
|
5
|
-
type FimoRichTextContent,
|
|
6
|
-
type FimoString,
|
|
7
|
-
type Populate,
|
|
8
|
-
type PopulatedResult,
|
|
9
|
-
type ProjectedWithPopulate,
|
|
10
|
-
type Query,
|
|
11
|
-
type Sort,
|
|
12
|
-
type Where,
|
|
13
|
-
type WithReferences,
|
|
14
|
-
} from 'fimo-react';
|
|
15
|
-
|
|
16
|
-
export interface HomePageFimoMeta {
|
|
17
|
-
contentType: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface HomePage extends WithReferences<HomePageReferences> {
|
|
21
|
-
id: string;
|
|
22
|
-
entryId: string;
|
|
23
|
-
documentId: string;
|
|
24
|
-
slug: FimoString;
|
|
25
|
-
locale?: string;
|
|
26
|
-
createdAt: string;
|
|
27
|
-
updatedAt: string;
|
|
28
|
-
__fimo: HomePageFimoMeta;
|
|
29
|
-
eyebrow: FimoString;
|
|
30
|
-
title: FimoString;
|
|
31
|
-
intro: FimoRichText;
|
|
32
|
-
primaryCtaLabel: FimoString;
|
|
33
|
-
primaryCtaHref: FimoString;
|
|
34
|
-
featured: { _key: string; label: string; title: string; summary: string; href: string }[];
|
|
35
|
-
contactHeading: FimoString;
|
|
36
|
-
contactIntro: FimoString;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface HomePageInput {
|
|
40
|
-
slug?: string;
|
|
41
|
-
eyebrow: string;
|
|
42
|
-
title: string;
|
|
43
|
-
intro: FimoRichTextContent;
|
|
44
|
-
primaryCtaLabel: string;
|
|
45
|
-
primaryCtaHref: string;
|
|
46
|
-
featured: { _key?: string; label: string; title: string; summary: string; href: string }[];
|
|
47
|
-
contactHeading: string;
|
|
48
|
-
contactIntro: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface HomePageReferences {}
|
|
52
|
-
|
|
53
|
-
export type HomePagePopulate = Populate<HomePage>;
|
|
54
|
-
export type HomePageWhere = Where<HomePage>;
|
|
55
|
-
export type HomePageSort = Sort<HomePage>;
|
|
56
|
-
export type HomePageFields = Fields<HomePage>;
|
|
57
|
-
export type HomePageQuery<
|
|
58
|
-
TFields extends HomePageFields | undefined = HomePageFields | undefined,
|
|
59
|
-
TPopulate extends HomePagePopulate | undefined = HomePagePopulate | undefined,
|
|
60
|
-
> = Query<HomePage, TFields, TPopulate>;
|
|
61
|
-
export type HomePageResult<
|
|
62
|
-
TFields extends HomePageFields | undefined = undefined,
|
|
63
|
-
TPopulate extends HomePagePopulate | undefined = undefined,
|
|
64
|
-
> = ProjectedWithPopulate<PopulatedResult<HomePage, TPopulate>, TFields, TPopulate>;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How a local checkout names and addresses a Fimo env — runtime-safe copy.
|
|
3
|
-
*
|
|
4
|
-
* TRAP: this looks like a duplicate of `@fimo/api-contracts` and it is, on
|
|
5
|
-
* purpose. `dist/runtime/` and `dist/build/` ship UNBUNDLED inside the `fimo`
|
|
6
|
-
* npm package and can only import published dependencies; `@fimo/api-contracts`
|
|
7
|
-
* is a private workspace package, so a runtime import of it breaks every user
|
|
8
|
-
* project (`ERR_MODULE_NOT_FOUND` from `vite.config.ts`). The CLI binary is
|
|
9
|
-
* bundled and may import contracts freely — the runtime may not.
|
|
10
|
-
*
|
|
11
|
-
* The two copies must agree byte-for-byte or a checkout computes a different
|
|
12
|
-
* tenant host than the API hands out. `env-identity.test.ts` pins them
|
|
13
|
-
* together against a corpus; change one, and that test tells you to change the
|
|
14
|
-
* other.
|
|
15
|
-
*/
|
|
16
|
-
export declare const DEFAULT_ENV = "main";
|
|
17
|
-
export declare const PROD_ENV_PREFIX = "release/";
|
|
18
|
-
/** Mirror of `isValidEnv` in `@fimo/api-contracts`. */
|
|
19
|
-
export declare function isValidEnv(env: string): boolean;
|
|
20
|
-
/** Mirror of `envResourceKey` in `@fimo/api-contracts`. */
|
|
21
|
-
export declare function envResourceKey(env: string): string;
|
|
22
|
-
/** Mirror of `buildTenantApiUrl` in `@fimo/api-contracts`. */
|
|
23
|
-
export declare function buildTenantApiUrl(projectId: string, env: string, tenantApiBaseUrl: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* Content-API aliases a local checkout exposes — one per framework family
|
|
26
|
-
* that has a Fimo startup hook (Vite/React Router, Next, Astro, SvelteKit).
|
|
27
|
-
* Add a key when its framework's hook lands, not before: an alias no runtime
|
|
28
|
-
* reads is one more line to reason about when a site talks to the wrong env.
|
|
29
|
-
*/
|
|
30
|
-
export declare const LOCAL_CONTENT_API_ENV_KEYS: readonly ['FIMO_CONTENT_API_URL', 'VITE_API_URL', 'NEXT_PUBLIC_FIMO_API_URL', 'PUBLIC_FIMO_API_URL'];
|
|
31
|
-
/** Env selectors a runtime turns into the `X-Fimo-Env` request header. */
|
|
32
|
-
export declare const LOCAL_ENV_SELECTOR_KEYS: readonly ['FIMO_ENV', 'NEXT_PUBLIC_FIMO_ENV'];
|
|
33
|
-
/**
|
|
34
|
-
* Both halves move together. The tenant URL selects the env for content reads
|
|
35
|
-
* (each env has its own host); the selectors select it for every service that
|
|
36
|
-
* reads `X-Fimo-Env`. Setting one without the other points a checkout at two
|
|
37
|
-
* different envs.
|
|
38
|
-
*/
|
|
39
|
-
export declare function buildLocalRuntimeEnv(options: {
|
|
40
|
-
readonly tenantApiUrl: string;
|
|
41
|
-
readonly env: string;
|
|
42
|
-
}): Record<string, string>;
|
|
43
|
-
//# sourceMappingURL=env-identity.d.ts.map
|