create-ailk 0.19.1 → 0.21.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 +84 -7
- package/component-catalog.json +184 -6
- package/component-catalog.md +20 -9
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/fetch-template-snapshot.d.ts +21 -0
- package/dist/lib/fetch-template-snapshot.js +205 -0
- package/dist/lib/merge-three-way.d.ts +31 -0
- package/dist/lib/merge-three-way.js +65 -0
- package/dist/module-architecture.js +55 -2
- package/dist/programmatic.d.ts +7 -1
- package/dist/programmatic.js +76 -22
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +78 -2
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +123 -1
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/update.d.ts +53 -0
- package/dist/update.js +606 -0
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/.claude/cloudparity-repo-hooks.json +9 -0
- package/templates/.claude/hooks/architecture-check.sh +8 -0
- package/templates/.claude/rules/loop-contract.md +129 -0
- package/templates/.claude/settings.json +3 -2
- package/templates/.env.example +13 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/CONVENTIONS.md +3 -2
- package/templates/README.md +26 -25
- package/templates/_gitignore +162 -0
- package/templates/apps/api/CLAUDE.md +18 -10
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
- package/templates/apps/api/src/__tests__/setup.ts +7 -0
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
- package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
- package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
- package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
- package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
- package/templates/apps/api/src/middleware/auth.ts +8 -4
- package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/README.md +33 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
- package/templates/apps/api/src/server.ts +14 -2
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
- package/templates/apps/api/src/services/audit-log.ts +72 -10
- package/templates/apps/api/src/services/default-tenant.ts +12 -3
- package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
- package/templates/apps/api/vercel.json +1 -1
- package/templates/apps/mcp/CLAUDE.md +19 -7
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
- package/templates/apps/mcp/src/tools/proposals.ts +27 -0
- package/templates/apps/mcp/src/workspace-server.ts +16 -0
- package/templates/apps/web/CLAUDE.md +26 -1
- package/templates/apps/web/_gitignore +6 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
- package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +46 -4
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/robots.txt/route.ts +13 -10
- package/templates/apps/web/jest.config.cjs +4 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
- package/templates/apps/web/lib/account-config.ts +31 -16
- package/templates/apps/web/lib/consent-visitor.ts +103 -0
- package/templates/apps/web/lib/deck-page.ts +186 -0
- package/templates/apps/web/lib/relay.ts +20 -0
- package/templates/apps/web/messages/en.json +83 -9
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +48 -0
- package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
- package/templates/database/__tests__/person-notes.db.test.ts +249 -0
- package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
- package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
- package/templates/database/__tests__/seed.test.ts +39 -2
- package/templates/database/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +38 -12
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
- package/templates/database/ops/schema.prisma +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +29 -0
- package/templates/project.yaml +6 -3
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
// About page. Reads the AboutPage MDX frontmatter, renders sections via
|
|
2
2
|
// SectionRenderer, and emits a JSON-LD <script>.
|
|
3
|
+
//
|
|
4
|
+
// #8650 D4 — `/about` additionally lists every `Person` page in the request's
|
|
5
|
+
// locale as one generated `team` section (the `/alternatives` `withSpokes`
|
|
6
|
+
// precedent, #6878 D9), each member linking to its own `/about/<slug>` page.
|
|
7
|
+
// With zero `Person` pages the parsed sections are returned unchanged, so a
|
|
8
|
+
// fresh scaffold (which seeds no `Person` file) renders exactly as before.
|
|
3
9
|
import type {
|
|
10
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
4
11
|
getMdxSource as defaultGetMdxSource,
|
|
5
12
|
getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
|
|
6
13
|
import {
|
|
14
|
+
getMdxSlugs as loadMdxSlugs,
|
|
15
|
+
getMdxSource as loadMdxSource,
|
|
16
|
+
getPageSections as loadPageSections,
|
|
7
17
|
renderPage,
|
|
8
18
|
renderPageMetadata
|
|
9
19
|
} from "@working-theory/page-renderer";
|
|
10
20
|
import type { Metadata } from "next";
|
|
21
|
+
import { getTranslations } from "next-intl/server";
|
|
11
22
|
import type { ReactElement } from "react";
|
|
12
23
|
|
|
13
24
|
interface Props {
|
|
14
25
|
params: Promise<{ locale: string }>;
|
|
15
26
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
16
27
|
getPageSections?: typeof defaultGetPageSections;
|
|
28
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
17
29
|
}
|
|
18
30
|
|
|
31
|
+
type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
|
|
32
|
+
|
|
19
33
|
export async function generateMetadata({
|
|
20
34
|
params,
|
|
21
35
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
@@ -27,14 +41,82 @@ export default async function Page({
|
|
|
27
41
|
params,
|
|
28
42
|
getMdxSource,
|
|
29
43
|
getPageSections,
|
|
44
|
+
getMdxSlugs,
|
|
30
45
|
}: Props): Promise<ReactElement> {
|
|
31
46
|
const { locale } = await params;
|
|
47
|
+
const loadSections = getPageSections ?? loadPageSections;
|
|
48
|
+
const loadSlugs = getMdxSlugs ?? loadMdxSlugs;
|
|
49
|
+
const loadSource = getMdxSource ?? loadMdxSource;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Append one generated `team` section listing every `Person` page (C3) —
|
|
53
|
+
* the same "the visible list and the sections that feed JSON-LD always
|
|
54
|
+
* agree" property `withSpokes` gives `/alternatives` (#6878 D9), since
|
|
55
|
+
* `renderPage` builds both the body and the JSON-LD from these sections.
|
|
56
|
+
* Returns `declared` unchanged whenever there is nothing to list, so a
|
|
57
|
+
* site with no `Person` page renders byte-identical to today (AC-4, C5).
|
|
58
|
+
*/
|
|
59
|
+
async function withPeople(frontmatter: unknown): Promise<Section[]> {
|
|
60
|
+
const declared = await loadSections(frontmatter);
|
|
61
|
+
const slugs = [...(await loadSlugs("Person", locale))].sort();
|
|
62
|
+
if (slugs.length === 0) return declared;
|
|
63
|
+
|
|
64
|
+
const t = await getTranslations({ locale, namespace: "about" });
|
|
65
|
+
const label = t("viewProfile");
|
|
66
|
+
|
|
67
|
+
const sources = await Promise.all(
|
|
68
|
+
slugs.map((slug) => loadSource("Person", slug, locale)),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const members = sources.flatMap((source, i) => {
|
|
72
|
+
if (source === null) return [];
|
|
73
|
+
const slug = slugs[i]!;
|
|
74
|
+
const fm = source.frontmatter as {
|
|
75
|
+
title: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
image?: string;
|
|
78
|
+
schema?: { name?: string; jobTitle?: string };
|
|
79
|
+
};
|
|
80
|
+
const name =
|
|
81
|
+
typeof fm.schema?.name === "string" ? fm.schema.name : fm.title;
|
|
82
|
+
const role =
|
|
83
|
+
typeof fm.schema?.jobTitle === "string" ? fm.schema.jobTitle : "";
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
name,
|
|
87
|
+
role,
|
|
88
|
+
...(typeof fm.description === "string" && {
|
|
89
|
+
bio: fm.description,
|
|
90
|
+
}),
|
|
91
|
+
...(typeof fm.image === "string" && {
|
|
92
|
+
avatar: { src: fm.image, alt: name },
|
|
93
|
+
}),
|
|
94
|
+
links: [{ type: "page" as const, url: `/about/${slug}`, label }],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (members.length === 0) return declared;
|
|
100
|
+
|
|
101
|
+
const people: Section = {
|
|
102
|
+
type: "team" as const,
|
|
103
|
+
id: "people",
|
|
104
|
+
data: { variant: "with-large-images" as const, members },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const last = declared.at(-1);
|
|
108
|
+
if (last !== undefined && last.type === "cta") {
|
|
109
|
+
return [...declared.slice(0, -1), people, last];
|
|
110
|
+
}
|
|
111
|
+
return [...declared, people];
|
|
112
|
+
}
|
|
113
|
+
|
|
32
114
|
return renderPage({
|
|
33
115
|
type: "AboutPage",
|
|
34
116
|
slug: "about",
|
|
35
117
|
locale,
|
|
36
118
|
translationsNamespace: "about",
|
|
119
|
+
getPageSections: withPeople,
|
|
37
120
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
38
|
-
...(getPageSections !== undefined && { getPageSections }),
|
|
39
121
|
});
|
|
40
122
|
}
|
|
@@ -72,7 +72,7 @@ describe("careers/page — snapshot regression", () => {
|
|
|
72
72
|
});
|
|
73
73
|
const { container } = render(element);
|
|
74
74
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
75
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Careers at AILK</h1><p data-testid="careers-description" class="mt-block-tight text-lead">Join the team building the AX-first platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-
|
|
75
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Careers at AILK</h1><p data-testid="careers-description" class="mt-block-tight text-lead">Join the team building the AX-first platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
76
76
|
);
|
|
77
77
|
});
|
|
78
78
|
});
|
|
@@ -97,7 +97,7 @@ describe("careers/[slug]/page — snapshot regression", () => {
|
|
|
97
97
|
});
|
|
98
98
|
const { container } = render(element);
|
|
99
99
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
100
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Senior Engineer</h1><p data-testid="senior-engineer-description" class="mt-block-tight text-lead">Build the AILK platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-
|
|
100
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Senior Engineer</h1><p data-testid="senior-engineer-description" class="mt-block-tight text-lead">Build the AILK platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
101
101
|
);
|
|
102
102
|
});
|
|
103
103
|
|
package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -68,7 +68,7 @@ describe("case-studies/page — snapshot regression", () => {
|
|
|
68
68
|
});
|
|
69
69
|
const { container } = render(element);
|
|
70
70
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
71
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Case Studies</h1><p data-testid="case-studies-description" class="mt-block-tight text-lead">Real-world accounts of teams using AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-
|
|
71
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Case Studies</h1><p data-testid="case-studies-description" class="mt-block-tight text-lead">Real-world accounts of teams using AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
@@ -93,7 +93,7 @@ describe("case-studies/[slug]/page — snapshot regression", () => {
|
|
|
93
93
|
});
|
|
94
94
|
const { container } = render(element);
|
|
95
95
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
96
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description" class="mt-block-tight text-lead">How a team shipped AEO with AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-
|
|
96
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description" class="mt-block-tight text-lead">How a team shipped AEO with AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
97
97
|
);
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ConsentBanner — a thin client wrapper around `Banner` (#8438 D12).
|
|
5
|
+
*
|
|
6
|
+
* `layout.tsx` (a Server Component) cannot pass `acceptCookiesAction` /
|
|
7
|
+
* `declineCookiesAction` straight into `Banner`'s `onAccept`/`onReject`
|
|
8
|
+
* props: those are plain functions, not JSX, and a function crossing the
|
|
9
|
+
* RSC → Client boundary as a PROP (rather than being called from inside
|
|
10
|
+
* client code) is the exact class of crash `signed-in-nav-rsc-boundary.
|
|
11
|
+
* test.tsx` guards against (#7402's `renderFlowTrigger` idiom next door
|
|
12
|
+
* solves the same problem the same way). This component is itself
|
|
13
|
+
* "use client" and imports the two Server Actions directly, so nothing
|
|
14
|
+
* callable crosses from the server — only the serializable props below do.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Banner, type BannerProps } from "@working-theory/ui/chrome";
|
|
18
|
+
import type { ReactElement } from "react";
|
|
19
|
+
|
|
20
|
+
import { acceptCookiesAction, declineCookiesAction } from "./consent.actions";
|
|
21
|
+
|
|
22
|
+
export function ConsentBanner(
|
|
23
|
+
props: Omit<BannerProps, "onAccept" | "onReject">,
|
|
24
|
+
): ReactElement {
|
|
25
|
+
return (
|
|
26
|
+
<Banner
|
|
27
|
+
{...props}
|
|
28
|
+
onAccept={() => void acceptCookiesAction()}
|
|
29
|
+
onReject={() => void declineCookiesAction()}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* acceptCookiesAction / declineCookiesAction — the cookie banner's two
|
|
5
|
+
* Server Actions (#8438 D12, spec
|
|
6
|
+
* docs/specs/8438-consent-registry-and-ledger-step-spec.md).
|
|
7
|
+
*
|
|
8
|
+
* Bound to Banner's `onAccept`/`onReject` in `layout.tsx`. Each ensures the
|
|
9
|
+
* visitor id (`consent-visitor.ts`), then calls `consent.recordCookieChoice`
|
|
10
|
+
* through `@working-theory/api-client` with the site's own spoke key (added
|
|
11
|
+
* by `http.ts`) and the visitor-forward headers a spoke attaches today.
|
|
12
|
+
*
|
|
13
|
+
* Neither ever throws to the client — Banner's `onAccept`/`onReject` are
|
|
14
|
+
* plain `() => void`, and the banner island already hides itself locally;
|
|
15
|
+
* a failed write here is logged and nothing more. Neither writes
|
|
16
|
+
* `ailk-consent` — the island owns that cookie, unchanged (C3).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { consent, visitorForwardHeaders } from "@working-theory/api-client";
|
|
20
|
+
import { cookies, headers } from "next/headers";
|
|
21
|
+
import { getLocale } from "next-intl/server";
|
|
22
|
+
|
|
23
|
+
import { ensureConsentVisitor } from "../../lib/consent-visitor";
|
|
24
|
+
|
|
25
|
+
async function recordChoice(choice: "accepted" | "declined"): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
const cookieStore = await cookies();
|
|
28
|
+
const visitorId = ensureConsentVisitor(cookieStore);
|
|
29
|
+
|
|
30
|
+
let locale = "en";
|
|
31
|
+
try {
|
|
32
|
+
locale = await getLocale();
|
|
33
|
+
} catch {
|
|
34
|
+
// No request-scoped locale (e.g. outside next-intl's pipeline) — "en".
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let init: { headers: Record<string, string> } | undefined;
|
|
38
|
+
try {
|
|
39
|
+
const forwarded = visitorForwardHeaders(
|
|
40
|
+
await headers(),
|
|
41
|
+
process.env.AILK_VISITOR_FORWARD_KEY,
|
|
42
|
+
);
|
|
43
|
+
if (forwarded) init = { headers: forwarded };
|
|
44
|
+
} catch {
|
|
45
|
+
// No request scope to read — record with no forwarding headers.
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const body = { visitorId, choice, locale };
|
|
49
|
+
const res = init
|
|
50
|
+
? await consent.recordCookieChoice(body, init)
|
|
51
|
+
: await consent.recordCookieChoice(body);
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
console.error("[consent.actions] record cookie choice refused", {
|
|
54
|
+
outcome: res.status,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Never attach the visitor id or any request detail to the log line.
|
|
59
|
+
console.error("[consent.actions] record cookie choice failed", {
|
|
60
|
+
outcome: err instanceof Error ? err.name : "unknown",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function acceptCookiesAction(): Promise<void> {
|
|
66
|
+
await recordChoice("accepted");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function declineCookiesAction(): Promise<void> {
|
|
70
|
+
await recordChoice("declined");
|
|
71
|
+
}
|
|
@@ -200,6 +200,13 @@ export function ConversationalIntakeWrapper({
|
|
|
200
200
|
action={captureLeadAction}
|
|
201
201
|
text={contactFormText(t)}
|
|
202
202
|
termsHref={localeHref(locale, "/terms")}
|
|
203
|
+
// #8661 D7 — the same three policy links /contact passes, so the
|
|
204
|
+
// catalog's three-link statement resolves here too.
|
|
205
|
+
policies={{
|
|
206
|
+
terms: localeHref(locale, "/terms"),
|
|
207
|
+
privacy: localeHref(locale, "/privacy"),
|
|
208
|
+
working_agreement: localeHref(locale, "/working-agreement"),
|
|
209
|
+
}}
|
|
203
210
|
initialValues={initialValues}
|
|
204
211
|
/>
|
|
205
212
|
</Container>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contactFormFromConfig (#8661 D6) — a Contact form magnet's config mapped
|
|
3
|
+
* onto the shared `LeadCaptureForm` block's fields, statement and label.
|
|
4
|
+
*
|
|
5
|
+
* AC-3: the seeded config maps to the five-field form in order (the terms box
|
|
6
|
+
* is the statement, not a field); a relabelled subject and a sixth option
|
|
7
|
+
* render; a config the action cannot submit (no `name`, a non-field first
|
|
8
|
+
* step) is `null`; `address` and a second `consent` field are not rendered;
|
|
9
|
+
* copy resolves per locale. AC-5's fallback is the same mapping over the
|
|
10
|
+
* built-in config, which is never `null`.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
CONTACT_FORM_SUBJECT_OPTIONS,
|
|
14
|
+
CONTACT_FORM_TERMS_STATEMENT,
|
|
15
|
+
contactFormFlowConfig,
|
|
16
|
+
type FlowConfig,
|
|
17
|
+
} from "@working-theory/validation";
|
|
18
|
+
|
|
19
|
+
import { contactFormFromConfig } from "../contact-form-magnet";
|
|
20
|
+
|
|
21
|
+
type FieldStep = Extract<FlowConfig["steps"][number], { type: "field" }>;
|
|
22
|
+
|
|
23
|
+
/** A deep copy of the seeded config, so a case can edit it freely. */
|
|
24
|
+
function seeded(): FlowConfig {
|
|
25
|
+
return JSON.parse(JSON.stringify(contactFormFlowConfig)) as FlowConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function firstStep(config: FlowConfig): FieldStep {
|
|
29
|
+
return config.steps[0] as FieldStep;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("contactFormFromConfig — the seeded config (AC-3)", () => {
|
|
33
|
+
const view = contactFormFromConfig(contactFormFlowConfig, "en");
|
|
34
|
+
|
|
35
|
+
it("is never null", () => {
|
|
36
|
+
expect(view).not.toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("maps name, email, subject and message, in order", () => {
|
|
40
|
+
expect(view!.fields.map((f) => f.name)).toEqual(["name", "email", "subject", "message"]);
|
|
41
|
+
expect(view!.fields.map((f) => f.required)).toEqual([true, true, false, false]);
|
|
42
|
+
expect(view!.fields.map((f) => f.type)).toEqual(["text", "email", "select", "textarea"]);
|
|
43
|
+
expect(view!.fields.map((f) => f.label)).toEqual(["Name", "Email", "Subject", "Message"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("pairs name and email, and runs every other field full width", () => {
|
|
47
|
+
expect(view!.fields.map((f) => f.colSpan)).toEqual([1, 1, 2, 2]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("gives the subject its five options as { value, label } pairs and its placeholder", () => {
|
|
51
|
+
const subject = view!.fields.find((f) => f.name === "subject")!;
|
|
52
|
+
expect(subject.options).toEqual(
|
|
53
|
+
CONTACT_FORM_SUBJECT_OPTIONS.map((value) => ({ value, label: value })),
|
|
54
|
+
);
|
|
55
|
+
expect(subject.placeholder).toBe("Choose a subject");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("takes the terms statement and the step's cta", () => {
|
|
59
|
+
expect(view!.consentStatement).toBe(CONTACT_FORM_TERMS_STATEMENT);
|
|
60
|
+
expect(view!.submitLabel).toBe("Send message");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("contactFormFromConfig — an owner's edit (AC-3)", () => {
|
|
65
|
+
it("renders a relabelled subject and a sixth option", () => {
|
|
66
|
+
const config = seeded();
|
|
67
|
+
const subject = firstStep(config).fields.find((f) => f.name === "subject")!;
|
|
68
|
+
subject.copy.label = { en: "Topic" };
|
|
69
|
+
subject.options = [...subject.options!, { value: "Podcast", label: { en: "Podcast" } }];
|
|
70
|
+
|
|
71
|
+
const view = contactFormFromConfig(config, "en")!;
|
|
72
|
+
const rendered = view.fields.find((f) => f.name === "subject")!;
|
|
73
|
+
expect(rendered.label).toBe("Topic");
|
|
74
|
+
expect(rendered.options).toHaveLength(6);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("is null when the step has no name field", () => {
|
|
78
|
+
const config = seeded();
|
|
79
|
+
const step = firstStep(config);
|
|
80
|
+
step.fields = step.fields.filter((f) => f.name !== "name");
|
|
81
|
+
expect(contactFormFromConfig(config, "en")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is null when the step has no email field", () => {
|
|
85
|
+
const config = seeded();
|
|
86
|
+
const step = firstStep(config);
|
|
87
|
+
step.fields = step.fields.filter((f) => f.name !== "email");
|
|
88
|
+
expect(contactFormFromConfig(config, "en")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("is null when the first step is not a field step", () => {
|
|
92
|
+
const config = seeded();
|
|
93
|
+
config.steps = [
|
|
94
|
+
{
|
|
95
|
+
key: "segment",
|
|
96
|
+
type: "segmentation",
|
|
97
|
+
copy: { title: { en: "Who are you?" } },
|
|
98
|
+
questions: [{ name: "role", options: [{ value: "a", label: { en: "A" } }] }],
|
|
99
|
+
},
|
|
100
|
+
...config.steps,
|
|
101
|
+
];
|
|
102
|
+
expect(contactFormFromConfig(config, "en")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders neither an address field nor a second consent field", () => {
|
|
106
|
+
const config = seeded();
|
|
107
|
+
const step = firstStep(config);
|
|
108
|
+
step.fields = [
|
|
109
|
+
...step.fields,
|
|
110
|
+
{ name: "where", inputType: "address", copy: { label: { en: "Address" } } },
|
|
111
|
+
{
|
|
112
|
+
name: "marketing",
|
|
113
|
+
inputType: "consent",
|
|
114
|
+
purposes: ["marketing"],
|
|
115
|
+
copy: { label: { en: "Send me news." } },
|
|
116
|
+
},
|
|
117
|
+
] as FieldStep["fields"];
|
|
118
|
+
|
|
119
|
+
const view = contactFormFromConfig(config, "en")!;
|
|
120
|
+
expect(view.fields.map((f) => f.name)).toEqual(["name", "email", "subject", "message"]);
|
|
121
|
+
expect(view.consentStatement).toBe(CONTACT_FORM_TERMS_STATEMENT);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("gives no statement when the step has no consent field", () => {
|
|
125
|
+
const config = seeded();
|
|
126
|
+
const step = firstStep(config);
|
|
127
|
+
step.fields = step.fields.filter((f) => f.inputType !== "consent");
|
|
128
|
+
expect(contactFormFromConfig(config, "en")!.consentStatement).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolves copy for the active locale", () => {
|
|
132
|
+
const config = seeded();
|
|
133
|
+
firstStep(config).fields[0]!.copy.label = { en: "Name", fr: "Nom" };
|
|
134
|
+
|
|
135
|
+
expect(contactFormFromConfig(config, "fr")!.fields[0]!.label).toBe("Nom");
|
|
136
|
+
expect(contactFormFromConfig(config, "en")!.fields[0]!.label).toBe("Name");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* the block to fill.
|
|
5
5
|
*/
|
|
6
6
|
import messages from "../../../../messages/en.json";
|
|
7
|
+
import { CONTACT_FORM_TERMS_STATEMENT } from "@working-theory/validation";
|
|
8
|
+
|
|
7
9
|
import { contactFormText } from "../contact-form-text";
|
|
8
10
|
|
|
9
11
|
/** A translator over the English catalog that interpolates `{name}` values, as ICU does. */
|
|
@@ -24,9 +26,24 @@ describe("contactFormText", () => {
|
|
|
24
26
|
success: messages.contact.successMessage,
|
|
25
27
|
genericError: messages.contact.genericError,
|
|
26
28
|
consentStatement: messages.contact.consentStatement,
|
|
29
|
+
consentRequired: "Please accept the terms to send your message.",
|
|
27
30
|
unavailableHeading: messages.contact.unavailableHeading,
|
|
28
31
|
unavailableBody: "Email us at {email} and we'll reply.",
|
|
29
32
|
unavailableNoEmail: messages.contact.unavailableNoEmail,
|
|
30
33
|
});
|
|
31
34
|
});
|
|
32
35
|
});
|
|
36
|
+
|
|
37
|
+
describe("the contact catalog (#8661 D9)", () => {
|
|
38
|
+
it("states the three-link terms statement the Contact form magnet seeds", () => {
|
|
39
|
+
expect(messages.contact.consentStatement).toBe(CONTACT_FORM_TERMS_STATEMENT);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("carries a workingAgreement namespace in the terms namespace's shape", () => {
|
|
43
|
+
expect(messages.workingAgreement).toEqual({
|
|
44
|
+
heading: "Working Agreement",
|
|
45
|
+
emptyState: "Working agreement content coming soon.",
|
|
46
|
+
});
|
|
47
|
+
expect(Object.keys(messages.workingAgreement)).toEqual(Object.keys(messages.terms));
|
|
48
|
+
});
|
|
49
|
+
});
|