create-ailk 0.7.0 → 0.8.0
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 +44 -10
- package/component-catalog.json +520 -5
- package/component-catalog.md +9 -6
- package/dist/cli.js +22 -24
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/parse-args.d.ts +78 -10
- package/dist/parse-args.js +204 -14
- package/dist/programmatic.d.ts +39 -9
- package/dist/programmatic.js +163 -25
- package/dist/site-roster.js +11 -6
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.js +102 -43
- package/dist/template-manifest.d.ts +36 -17
- package/dist/template-manifest.js +121 -367
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +3 -3
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/standard/DESIGN.md +5 -0
- package/site-templates/marketing-site/DESIGN.md +5 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
- package/site-templates/marketing-site-free/DESIGN.md +5 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/site-templates/saas-product/DESIGN.md +5 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-page/references/page-types.md +4 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/DESIGN.md +5 -0
- package/templates/README.md +46 -38
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +7 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/layout.tsx +37 -5
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
- package/templates/apps/web/lib/nav-config.ts +48 -36
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/messages/en.json +28 -2
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/ContactPage/contact.mdx +14 -44
- package/templates/content/en/HomePage/home.mdx +8 -16
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +33 -33
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "Service"
|
|
3
|
-
slug: "seasonal-cleanup"
|
|
4
|
-
title: "{Seasonal cleanup}"
|
|
5
|
-
description: "{One line on what this job is and who it suits. Replace before you launch.}"
|
|
6
|
-
price: 289
|
|
7
|
-
priceCurrency: "USD"
|
|
8
|
-
provider:
|
|
9
|
-
name: "{Your Company}"
|
|
10
|
-
schema:
|
|
11
|
-
serviceType: "{Seasonal cleanup}"
|
|
12
|
-
areaServed: "{Your service area}"
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# \{Seasonal cleanup\}
|
|
16
|
-
|
|
17
|
-
\{Open with the situation a reader is in when they come looking for this. Two or
|
|
18
|
-
three sentences, no more — the detail belongs under the headings below.\}
|
|
19
|
-
|
|
20
|
-
## What it covers
|
|
21
|
-
|
|
22
|
-
- \{The first thing you actually do\}
|
|
23
|
-
- \{The second thing you actually do\}
|
|
24
|
-
- \{The third thing you actually do\}
|
|
25
|
-
|
|
26
|
-
## How it runs
|
|
27
|
-
|
|
28
|
-
\{One paragraph on the shape of the visit: how it is scheduled, how long it
|
|
29
|
-
takes, who turns up, and what you need from the customer beforehand.\}
|
|
30
|
-
|
|
31
|
-
## What you end up with
|
|
32
|
-
|
|
33
|
-
\{One paragraph on the result. Be concrete — describe the yard when the crew
|
|
34
|
-
drives away.\}
|
|
35
|
-
|
|
36
|
-
## What it costs
|
|
37
|
-
|
|
38
|
-
\{The price in this page's frontmatter is a PLACEHOLDER. It is what the site
|
|
39
|
-
publishes as a schema.org `Offer`, so an agent reading this page quotes it —
|
|
40
|
-
replace it with your real number before you launch.\}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ContactForm — client component that wires the contact form to
|
|
5
|
-
* captureLeadAction via useFormState.
|
|
6
|
-
*
|
|
7
|
-
* Validation errors render inline next to each field.
|
|
8
|
-
* Success shows a confirmation message.
|
|
9
|
-
* Server errors show a generic fallback message.
|
|
10
|
-
*
|
|
11
|
-
* Honeypot + rate-limit enforcement lives in apps/api; no duplication here.
|
|
12
|
-
*
|
|
13
|
-
* `initialValues` (#4534, epic #4521's integration leg): optional prefill
|
|
14
|
-
* for name/email/message, set via `defaultValue` (the fields stay
|
|
15
|
-
* uncontrolled — unchanged for the no-prop call site). Lets
|
|
16
|
-
* `ConversationalIntakeWrapper` carry over whatever the visitor already
|
|
17
|
-
* typed into the conversation before Skip (or a failed auto-submit)
|
|
18
|
-
* dropped them here, instead of asking them to retype it.
|
|
19
|
-
*
|
|
20
|
-
* Consent (#6466): a real checkbox named by a `ConsentStatement` whose
|
|
21
|
-
* "Terms" links to the locale-aware `/terms` route. It starts unticked and
|
|
22
|
-
* the submit button stays disabled until it is ticked — the form is
|
|
23
|
-
* `noValidate`, so `required` alone would gate nothing. The statement is
|
|
24
|
-
* wired by `aria-labelledby`, never a wrapping `<label>`, so clicking the
|
|
25
|
-
* link follows it instead of toggling the box (the ConsentGroup rule).
|
|
26
|
-
*
|
|
27
|
-
* Composition (#6514): every control and message is a `@working-theory/ui`
|
|
28
|
-
* primitive — `Label`/`Input`/`Textarea` for the fields, `Alert` for the
|
|
29
|
-
* success and form-level error, `InlineMessage` for the per-field messages
|
|
30
|
-
* (its `error` variant carries `role="alert"`), `Button` for submit. The honeypot hides behind
|
|
31
|
-
* the `hidden` attribute on its wrapper rather than an inline style object.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
Alert,
|
|
36
|
-
AlertDescription,
|
|
37
|
-
Button,
|
|
38
|
-
Checkbox,
|
|
39
|
-
type CheckboxProps,
|
|
40
|
-
ConsentStatement,
|
|
41
|
-
InlineMessage,
|
|
42
|
-
Input,
|
|
43
|
-
Label,
|
|
44
|
-
localeHref,
|
|
45
|
-
Textarea,
|
|
46
|
-
} from "@working-theory/ui";
|
|
47
|
-
import { useLocale, useTranslations } from "next-intl";
|
|
48
|
-
import { useCallback, useState } from "react";
|
|
49
|
-
import { useFormState, useFormStatus } from "react-dom";
|
|
50
|
-
|
|
51
|
-
import { captureLeadAction } from "./contact.actions";
|
|
52
|
-
|
|
53
|
-
export interface ContactFormInitialValues {
|
|
54
|
-
name?: string;
|
|
55
|
-
email?: string;
|
|
56
|
-
message?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type CheckedState = Parameters<NonNullable<CheckboxProps["onCheckedChange"]>>[0];
|
|
60
|
-
|
|
61
|
-
interface ContactFormProps {
|
|
62
|
-
initialValues?: ContactFormInitialValues;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function SubmitButton({ disabled }: { disabled: boolean }) {
|
|
66
|
-
const t = useTranslations("contact");
|
|
67
|
-
const { pending } = useFormStatus();
|
|
68
|
-
return (
|
|
69
|
-
<Button
|
|
70
|
-
type="submit"
|
|
71
|
-
disabled={pending || disabled}
|
|
72
|
-
data-testid="contact-submit"
|
|
73
|
-
>
|
|
74
|
-
{pending ? t("sending") : t("sendMessage")}
|
|
75
|
-
</Button>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function ContactForm({ initialValues }: ContactFormProps = {}) {
|
|
80
|
-
const t = useTranslations("contact");
|
|
81
|
-
const locale = useLocale();
|
|
82
|
-
const [state, formAction] = useFormState(captureLeadAction, null);
|
|
83
|
-
const [consented, setConsented] = useState(false);
|
|
84
|
-
const handleConsentChange = useCallback((checked: CheckedState) => {
|
|
85
|
-
setConsented(checked === true);
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
if (state?.kind === "success") {
|
|
89
|
-
return (
|
|
90
|
-
<Alert variant="success" data-testid="contact-success">
|
|
91
|
-
<AlertDescription>{t("successMessage")}</AlertDescription>
|
|
92
|
-
</Alert>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const fieldErrors = state?.kind === "error" ? (state.fieldErrors ?? {}) : {};
|
|
97
|
-
const formError =
|
|
98
|
-
state?.kind === "error" && !state.fieldErrors ? state.message : undefined;
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<form action={formAction} noValidate>
|
|
102
|
-
{formError && (
|
|
103
|
-
<Alert variant="error" data-testid="contact-form-error">
|
|
104
|
-
<AlertDescription>{formError}</AlertDescription>
|
|
105
|
-
</Alert>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{/* Honeypot — hidden from users; bots fill it; API rejects non-empty */}
|
|
109
|
-
<div hidden>
|
|
110
|
-
<Input
|
|
111
|
-
type="text"
|
|
112
|
-
name="website"
|
|
113
|
-
tabIndex={-1}
|
|
114
|
-
aria-hidden="true"
|
|
115
|
-
data-testid="contact-honeypot"
|
|
116
|
-
/>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div>
|
|
120
|
-
<Label htmlFor="contact-name">{t("nameLabel")}</Label>
|
|
121
|
-
<Input
|
|
122
|
-
id="contact-name"
|
|
123
|
-
name="name"
|
|
124
|
-
type="text"
|
|
125
|
-
autoComplete="name"
|
|
126
|
-
data-testid="contact-name-input"
|
|
127
|
-
required
|
|
128
|
-
defaultValue={initialValues?.name}
|
|
129
|
-
aria-describedby={fieldErrors.name ? "contact-name-error" : undefined}
|
|
130
|
-
/>
|
|
131
|
-
{fieldErrors.name && (
|
|
132
|
-
<InlineMessage
|
|
133
|
-
variant="error"
|
|
134
|
-
id="contact-name-error"
|
|
135
|
-
data-testid="contact-name-error"
|
|
136
|
-
>
|
|
137
|
-
{fieldErrors.name}
|
|
138
|
-
</InlineMessage>
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<div>
|
|
143
|
-
<Label htmlFor="contact-email">{t("emailLabel")}</Label>
|
|
144
|
-
<Input
|
|
145
|
-
id="contact-email"
|
|
146
|
-
name="email"
|
|
147
|
-
type="email"
|
|
148
|
-
autoComplete="email"
|
|
149
|
-
data-testid="contact-email-input"
|
|
150
|
-
required
|
|
151
|
-
defaultValue={initialValues?.email}
|
|
152
|
-
aria-describedby={
|
|
153
|
-
fieldErrors.email ? "contact-email-error" : undefined
|
|
154
|
-
}
|
|
155
|
-
/>
|
|
156
|
-
{fieldErrors.email && (
|
|
157
|
-
<InlineMessage
|
|
158
|
-
variant="error"
|
|
159
|
-
id="contact-email-error"
|
|
160
|
-
data-testid="contact-email-error"
|
|
161
|
-
>
|
|
162
|
-
{fieldErrors.email}
|
|
163
|
-
</InlineMessage>
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<div>
|
|
168
|
-
<Label htmlFor="contact-message">{t("messageLabel")}</Label>
|
|
169
|
-
<Textarea
|
|
170
|
-
id="contact-message"
|
|
171
|
-
name="message"
|
|
172
|
-
rows={5}
|
|
173
|
-
data-testid="contact-message-input"
|
|
174
|
-
defaultValue={initialValues?.message}
|
|
175
|
-
aria-describedby={
|
|
176
|
-
fieldErrors.message ? "contact-message-error" : undefined
|
|
177
|
-
}
|
|
178
|
-
/>
|
|
179
|
-
{fieldErrors.message && (
|
|
180
|
-
<InlineMessage
|
|
181
|
-
variant="error"
|
|
182
|
-
id="contact-message-error"
|
|
183
|
-
data-testid="contact-message-error"
|
|
184
|
-
>
|
|
185
|
-
{fieldErrors.message}
|
|
186
|
-
</InlineMessage>
|
|
187
|
-
)}
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
<div className="flex items-start gap-3">
|
|
191
|
-
<Checkbox
|
|
192
|
-
id="contact-consent"
|
|
193
|
-
name="consent"
|
|
194
|
-
value="true"
|
|
195
|
-
checked={consented}
|
|
196
|
-
onCheckedChange={handleConsentChange}
|
|
197
|
-
aria-labelledby="contact-consent-statement"
|
|
198
|
-
className="mt-0.5"
|
|
199
|
-
data-testid="contact-consent"
|
|
200
|
-
/>
|
|
201
|
-
<ConsentStatement
|
|
202
|
-
id="contact-consent-statement"
|
|
203
|
-
statement={t("consentStatement")}
|
|
204
|
-
policies={{ terms: localeHref(locale, "/terms") }}
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<SubmitButton disabled={!consented} />
|
|
209
|
-
</form>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for ContactForm's consent row (#6466, spec AC-2 / AC-3).
|
|
3
|
-
*
|
|
4
|
-
* ContactForm is the only form a /contact visitor can submit — the fallback
|
|
5
|
-
* `ConversationalIntakeWrapper` swaps in on Skip or a failed auto-submit. Its
|
|
6
|
-
* consent line must be a real control: a visible, unticked checkbox named by
|
|
7
|
-
* a statement whose "Terms" is a link, and a submit button that stays
|
|
8
|
-
* disabled until the box is ticked (the form is `noValidate`, so `required`
|
|
9
|
-
* alone would gate nothing).
|
|
10
|
-
*
|
|
11
|
-
* jsdom lacks `useFormState`/`useFormStatus` (see
|
|
12
|
-
* page-renderer-migration.test.tsx), so `react-dom` is extended with inert
|
|
13
|
-
* stand-ins; the outermost network boundary (`@working-theory/api-client`)
|
|
14
|
-
* is mocked so the real server action module loads without a network.
|
|
15
|
-
*/
|
|
16
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
17
|
-
import { localeHref } from "@working-theory/ui";
|
|
18
|
-
|
|
19
|
-
const MESSAGES: Record<string, string> = {
|
|
20
|
-
consentStatement: "I accept the [Terms](terms).",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
jest.mock("next-intl", () => ({
|
|
24
|
-
useTranslations: () => (key: string) => MESSAGES[key] ?? key,
|
|
25
|
-
useLocale: () => "en",
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
jest.mock("react-dom", () => ({
|
|
29
|
-
...jest.requireActual("react-dom"),
|
|
30
|
-
useFormState: (_action: unknown, initial: unknown) => [initial, jest.fn()],
|
|
31
|
-
useFormStatus: () => ({ pending: false }),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
35
|
-
leads: { submit: jest.fn() },
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
import { ContactForm } from "../ContactForm.js";
|
|
39
|
-
|
|
40
|
-
// jsdom does not implement ResizeObserver — the Radix Checkbox renders a
|
|
41
|
-
// hidden form-bubble input that observes size. Polyfill with a no-op.
|
|
42
|
-
beforeAll(() => {
|
|
43
|
-
global.ResizeObserver = class ResizeObserver {
|
|
44
|
-
observe() {}
|
|
45
|
-
unobserve() {}
|
|
46
|
-
disconnect() {}
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("ContactForm consent row (#6466)", () => {
|
|
51
|
-
it("renders a visible, unticked checkbox (AC-2 i)", () => {
|
|
52
|
-
render(<ContactForm />);
|
|
53
|
-
const checkbox = screen.getByRole("checkbox");
|
|
54
|
-
expect(checkbox).toBeVisible();
|
|
55
|
-
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("links 'Terms' to the locale-aware /terms route (AC-2 ii)", () => {
|
|
59
|
-
render(<ContactForm />);
|
|
60
|
-
const link = screen.getByRole("link", { name: "Terms" });
|
|
61
|
-
expect(link).toHaveAttribute("href", localeHref("en", "/terms"));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("names the checkbox by the consent statement (AC-2 iii)", () => {
|
|
65
|
-
render(<ContactForm />);
|
|
66
|
-
// jsdom's accname pads the inline link with a space ("Terms .").
|
|
67
|
-
expect(screen.getByRole("checkbox")).toHaveAccessibleName(
|
|
68
|
-
/^I accept the Terms ?\.$/,
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("clicking the Terms link does not tick the box (AC-2 iv)", () => {
|
|
73
|
-
render(<ContactForm />);
|
|
74
|
-
const link = screen.getByRole("link", { name: "Terms" });
|
|
75
|
-
// A wrapping <label> is what would forward the click to the box.
|
|
76
|
-
expect(link.closest("label")).toBeNull();
|
|
77
|
-
link.addEventListener("click", (event) => event.preventDefault());
|
|
78
|
-
fireEvent.click(link);
|
|
79
|
-
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
80
|
-
"aria-checked",
|
|
81
|
-
"false",
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("keeps submit disabled until the box is ticked (AC-3)", () => {
|
|
86
|
-
render(<ContactForm />);
|
|
87
|
-
const submit = screen.getByTestId("contact-submit");
|
|
88
|
-
expect(submit).toBeDisabled();
|
|
89
|
-
fireEvent.click(screen.getByRole("checkbox"));
|
|
90
|
-
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
91
|
-
"aria-checked",
|
|
92
|
-
"true",
|
|
93
|
-
);
|
|
94
|
-
expect(submit).toBeEnabled();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "SoftwareProduct"
|
|
3
|
-
slug: "product"
|
|
4
|
-
title: "{Your product name}"
|
|
5
|
-
description: "{One sentence on what your product does and who it is for}"
|
|
6
|
-
placeholder: true
|
|
7
|
-
sections:
|
|
8
|
-
[
|
|
9
|
-
{
|
|
10
|
-
"id": "hero",
|
|
11
|
-
"type": "hero",
|
|
12
|
-
"data":
|
|
13
|
-
{
|
|
14
|
-
"eyebrow": "Product",
|
|
15
|
-
"headline": "{Your product name}",
|
|
16
|
-
"subheadline": "{One sentence on what your product does and who it is for}",
|
|
17
|
-
"primaryCta": { "label": "{Primary CTA}", "href": "/contact" },
|
|
18
|
-
"secondaryCta":
|
|
19
|
-
{
|
|
20
|
-
"label": "{Secondary CTA}",
|
|
21
|
-
"href": "/pricing",
|
|
22
|
-
"variant": "outline",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"id": "core-features",
|
|
28
|
-
"type": "feature",
|
|
29
|
-
"data":
|
|
30
|
-
{
|
|
31
|
-
"variant": "default",
|
|
32
|
-
"itemStyle": "plain",
|
|
33
|
-
"eyebrow": "{Section label}",
|
|
34
|
-
"headline": "{Section heading: the outcome this section promises}",
|
|
35
|
-
"description": "{Two sentences on what these features let your customers do}",
|
|
36
|
-
"features":
|
|
37
|
-
[
|
|
38
|
-
{
|
|
39
|
-
"title": "{Feature name}",
|
|
40
|
-
"description": "{Explain what this feature lets your customers do, in plain English with no jargon}",
|
|
41
|
-
"icon": "rocket",
|
|
42
|
-
"link":
|
|
43
|
-
{
|
|
44
|
-
"label": "Learn More",
|
|
45
|
-
"href": "#how-it-works",
|
|
46
|
-
"variant": "link",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"title": "{Feature name}",
|
|
51
|
-
"description": "{Explain what this feature lets your customers do, in plain English with no jargon}",
|
|
52
|
-
"icon": "zap",
|
|
53
|
-
"link":
|
|
54
|
-
{
|
|
55
|
-
"label": "Learn More",
|
|
56
|
-
"href": "#how-it-works",
|
|
57
|
-
"variant": "link",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"title": "{Feature name}",
|
|
62
|
-
"description": "{Explain what this feature lets your customers do, in plain English with no jargon}",
|
|
63
|
-
"icon": "sparkles",
|
|
64
|
-
"link":
|
|
65
|
-
{
|
|
66
|
-
"label": "Learn More",
|
|
67
|
-
"href": "#how-it-works",
|
|
68
|
-
"variant": "link",
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"title": "{Feature name}",
|
|
73
|
-
"description": "{Explain what this feature lets your customers do, in plain English with no jargon}",
|
|
74
|
-
"icon": "gauge",
|
|
75
|
-
"link":
|
|
76
|
-
{
|
|
77
|
-
"label": "Learn More",
|
|
78
|
-
"href": "#how-it-works",
|
|
79
|
-
"variant": "link",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"title": "{Feature name}",
|
|
84
|
-
"description": "{Explain what this feature lets your customers do, in plain English with no jargon}",
|
|
85
|
-
"icon": "shield-check",
|
|
86
|
-
"link":
|
|
87
|
-
{
|
|
88
|
-
"label": "Learn More",
|
|
89
|
-
"href": "#how-it-works",
|
|
90
|
-
"variant": "link",
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"id": "how-it-works",
|
|
98
|
-
"type": "feature",
|
|
99
|
-
"data":
|
|
100
|
-
{
|
|
101
|
-
"variant": "steps",
|
|
102
|
-
"numbered": true,
|
|
103
|
-
"eyebrow": "How it works",
|
|
104
|
-
"headline": "{Get set up in number easy steps}",
|
|
105
|
-
"description": "{Summarize the setup process briefly, so it reads as approachable and quick}",
|
|
106
|
-
"features":
|
|
107
|
-
[
|
|
108
|
-
{
|
|
109
|
-
"title": "{The first thing they do to get the outcome they want}",
|
|
110
|
-
"description": "{Two sentences on what the customer does in this step and what they get}",
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
"title": "{The second thing they do to get the outcome they want}",
|
|
114
|
-
"description": "{Two sentences on what the customer does in this step and what they get}",
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
"title": "{The third thing they do to get the outcome they want}",
|
|
118
|
-
"description": "{Two sentences on what the customer does in this step and what they get}",
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"id": "integrations",
|
|
125
|
-
"type": "cta",
|
|
126
|
-
"data":
|
|
127
|
-
{
|
|
128
|
-
"variant": "minimal",
|
|
129
|
-
"eyebrow": "Integrations",
|
|
130
|
-
"headline": "Direct integrations with your favorite tools",
|
|
131
|
-
"description": "{Describe what these integrations help your customers do, with one impressive fact about how they work}",
|
|
132
|
-
"primaryCta":
|
|
133
|
-
{
|
|
134
|
-
"label": "Get Started",
|
|
135
|
-
"href": "/contact",
|
|
136
|
-
"variant": "outline",
|
|
137
|
-
},
|
|
138
|
-
"secondaryCta":
|
|
139
|
-
{
|
|
140
|
-
"label": "See all Integrations",
|
|
141
|
-
"href": "#integrations-media",
|
|
142
|
-
"variant": "link",
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
"id": "integrations-media",
|
|
148
|
-
"type": "media-band",
|
|
149
|
-
"data": { "imagerySlot": true },
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
"id": "switch",
|
|
153
|
-
"type": "cta",
|
|
154
|
-
"data":
|
|
155
|
-
{
|
|
156
|
-
"variant": "box",
|
|
157
|
-
"headline": "{Switch to your product in time amount or less}",
|
|
158
|
-
"description": "{You shouldn't have to experience a painful problem. With your product, you can finally achieve a value outcome or get a big improvement.}",
|
|
159
|
-
"checklist":
|
|
160
|
-
["No credit card required", "Cancel any time", "Live chat support"],
|
|
161
|
-
"imagerySlot": true,
|
|
162
|
-
"primaryCta": { "label": "{Primary CTA}", "href": "/contact" },
|
|
163
|
-
"secondaryCta":
|
|
164
|
-
{
|
|
165
|
-
"label": "{Secondary CTA}",
|
|
166
|
-
"href": "/pricing",
|
|
167
|
-
"variant": "outline",
|
|
168
|
-
},
|
|
169
|
-
"closingLine": "{Social Proof goes here}",
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
]
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
{/* Your product's own page, served at /software/product. Its JSON-LD is a WebPage whose main entity is a SoftwareApplication named by this page's title, and the feature titles below become the site's featureList. Placeholder copy — replace every {brace} with your own words before you launch. */}
|