create-ailk 0.4.0 → 0.5.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/component-catalog.json +11939 -0
- package/component-catalog.md +176 -64
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/module-architecture.d.ts +59 -8
- package/dist/module-architecture.js +334 -115
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +107 -32
- package/dist/template-manifest.d.ts +55 -3
- package/dist/template-manifest.js +181 -12
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +9 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
- package/site-templates/minimalism-site/ailk-template.json +21 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +32 -7
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/CLAUDE.md +4 -1
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/package.json +6 -2
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +8 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/server.ts +82 -6
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/default-tenant.ts +261 -0
- package/templates/apps/api/src/services/lead-promotion.ts +161 -0
- package/templates/apps/api/src/services/leads-inbox.ts +263 -0
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/mcp/CLAUDE.md +1 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/package.json +6 -2
- package/templates/apps/mcp/src/server.ts +2 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/index.ts +2 -2
- package/templates/apps/web/.env.example +3 -3
- package/templates/apps/web/CLAUDE.md +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +214 -17
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +7 -15
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
- package/templates/apps/web/messages/en.json +29 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +6 -2
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -25,8 +25,10 @@ import "../globals.css";
|
|
|
25
25
|
import { Telemetry, VerificationMeta } from "@working-theory/analytics";
|
|
26
26
|
import { MarketingShell } from "@working-theory/templates/shells";
|
|
27
27
|
import { themeToCssVarsBothModes } from "@working-theory/theme";
|
|
28
|
-
import { isRtl } from "@working-theory/ui";
|
|
28
|
+
import { Link, isRtl, localeHref } from "@working-theory/ui";
|
|
29
|
+
import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
|
|
29
30
|
import {
|
|
31
|
+
Banner,
|
|
30
32
|
Footer,
|
|
31
33
|
Nav,
|
|
32
34
|
type NavAction,
|
|
@@ -49,6 +51,7 @@ import { SignOutTrigger } from "../../components/sign-out-trigger";
|
|
|
49
51
|
import { navItems } from "../../lib/nav-config";
|
|
50
52
|
import { getOgImageUrl } from "../../lib/og";
|
|
51
53
|
import { SITE_NAME, siteLogo } from "../../lib/site-brand";
|
|
54
|
+
import { siteChrome } from "../../lib/site-chrome";
|
|
52
55
|
import { loadSiteBrandTokens } from "../../lib/site-theme";
|
|
53
56
|
import { resolveBaseUrl } from "../../src/lib/routes";
|
|
54
57
|
|
|
@@ -57,6 +60,12 @@ import { resolveBaseUrl } from "../../src/lib/routes";
|
|
|
57
60
|
const DEFAULT_LOCALE = "en";
|
|
58
61
|
const THEME_COOKIE_NAME =
|
|
59
62
|
process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme";
|
|
63
|
+
/**
|
|
64
|
+
* The cookie the consent island persists the visitor's choice under (#5557).
|
|
65
|
+
* Passed to Banner explicitly so the server-side read below and the island's
|
|
66
|
+
* client-side write name the same cookie by construction.
|
|
67
|
+
*/
|
|
68
|
+
const CONSENT_COOKIE_NAME = "ailk-consent";
|
|
60
69
|
|
|
61
70
|
type ThemeMode = "light" | "dark";
|
|
62
71
|
|
|
@@ -73,7 +82,9 @@ type ThemeMode = "light" | "dark";
|
|
|
73
82
|
* has to satisfy the type: one minimal fields-step and a dead `action`.
|
|
74
83
|
*/
|
|
75
84
|
const NAV_CLIENT_ACTION_FLOW: NavActionFlow["flow"] = {
|
|
76
|
-
steps: [
|
|
85
|
+
steps: [
|
|
86
|
+
{ id: "noop", fields: [{ name: "noop", label: "noop", type: "text" }] },
|
|
87
|
+
],
|
|
77
88
|
action: "#",
|
|
78
89
|
};
|
|
79
90
|
|
|
@@ -100,6 +111,26 @@ async function resolveThemeMode(): Promise<ThemeMode> {
|
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Whether this visitor has already accepted or declined cookies (#5557).
|
|
116
|
+
*
|
|
117
|
+
* Read on the server, not left to the island alone: `BannerConsentIsland`
|
|
118
|
+
* initialises its `consented` state from `document.cookie`, which makes the
|
|
119
|
+
* CLIENT render null while the SERVER (no `document`) has already streamed
|
|
120
|
+
* the banner — React then leaves that server markup in place, un-hydrated, so
|
|
121
|
+
* a returning visitor sees a dead banner whose buttons do nothing. Deciding
|
|
122
|
+
* here means a decided visitor gets no banner markup at all, and a fresh one
|
|
123
|
+
* gets the live island that hides itself on Accept / Reject.
|
|
124
|
+
*/
|
|
125
|
+
async function hasConsentChoice(): Promise<boolean> {
|
|
126
|
+
try {
|
|
127
|
+
const cookieStore = await cookies();
|
|
128
|
+
return cookieStore.get(CONSENT_COOKIE_NAME)?.value !== undefined;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
103
134
|
/**
|
|
104
135
|
* Blocking inline script that runs synchronously before any styled element.
|
|
105
136
|
*
|
|
@@ -177,6 +208,7 @@ export default async function LocaleLayout({
|
|
|
177
208
|
const { locale } = await params;
|
|
178
209
|
const messages = await getMessages();
|
|
179
210
|
const themeMode = await resolveThemeMode();
|
|
211
|
+
const consentDecided = await hasConsentChoice();
|
|
180
212
|
// Server-side session read (#4984) — the same `auth.api.getSession` idiom
|
|
181
213
|
// the `(authed)` segment guard uses. Null when signed out or auth is unset.
|
|
182
214
|
const session = await auth.api.getSession({ headers: await headers() });
|
|
@@ -187,12 +219,26 @@ export default async function LocaleLayout({
|
|
|
187
219
|
const navLabels = {
|
|
188
220
|
items: {
|
|
189
221
|
home: tNav("home"),
|
|
222
|
+
// The Free marketing-site chrome (#5469 D5): the Product flyout's three
|
|
223
|
+
// entries + descriptions, the top-level items, and the Get started CTA.
|
|
224
|
+
product: tNav("product"),
|
|
225
|
+
howItWorks: tNav("howItWorks"),
|
|
226
|
+
howItWorksDesc: tNav("howItWorksDesc"),
|
|
227
|
+
features: tNav("features"),
|
|
228
|
+
featuresDesc: tNav("featuresDesc"),
|
|
229
|
+
faq: tNav("faq"),
|
|
230
|
+
faqDesc: tNav("faqDesc"),
|
|
231
|
+
pricing: tNav("pricing"),
|
|
232
|
+
about: tNav("about"),
|
|
233
|
+
getStarted: tNav("getStarted"),
|
|
190
234
|
docs: tNav("docs"),
|
|
191
235
|
blog: tNav("blog"),
|
|
192
|
-
// The scheduling module's booking page (#4633
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// `
|
|
236
|
+
// The scheduling module's booking page, /meet (#4633; renamed from the
|
|
237
|
+
// schedule/ route by #5563 — the label key itself keeps its name, D1
|
|
238
|
+
// R2: it names copy, not a route). Present in the map unconditionally,
|
|
239
|
+
// like `docs` and `blog`: the map is a label lookup, and an entry for
|
|
240
|
+
// a surface this site did not scaffold is never reached — `nav-config.ts`
|
|
241
|
+
// only lists surfaces the declared architecture carries.
|
|
196
242
|
schedule: tNav("schedule"),
|
|
197
243
|
// Session-aware action labels (#4984). `account` carries the signed-in
|
|
198
244
|
// user's display name (name, else email) so the Nav shows WHO is signed
|
|
@@ -207,8 +253,13 @@ export default async function LocaleLayout({
|
|
|
207
253
|
closeMenu: tNav("closeMenu"),
|
|
208
254
|
};
|
|
209
255
|
const navOverflowLabel = tNav("moreLinks");
|
|
256
|
+
// Cookie-consent copy (#5557). The `consent` namespace ships in every
|
|
257
|
+
// scaffold's messages file; the Banner below is UI-only — OSS ships no
|
|
258
|
+
// enforcement, the callbacks are the hook for @working-theory/consent.
|
|
259
|
+
const tConsent = await getTranslations({ locale, namespace: "consent" });
|
|
210
260
|
|
|
211
|
-
// Right-side action cluster (#4984). Signed out →
|
|
261
|
+
// Right-side action cluster (#4984). Signed out → the Free template's pair
|
|
262
|
+
// (#5469 D5): a primary "Get started" → /contact beside the outline-strong
|
|
212
263
|
// "Sign in" link (the operator's auth-pair idiom, #4370, MINUS Sign up —
|
|
213
264
|
// this site offers no sign-up flow). Signed in → the user's name (a
|
|
214
265
|
// non-navigating label) + an outline-strong "Sign out"; both authored as
|
|
@@ -224,7 +275,10 @@ export default async function LocaleLayout({
|
|
|
224
275
|
variant: "outline-strong",
|
|
225
276
|
},
|
|
226
277
|
]
|
|
227
|
-
: [
|
|
278
|
+
: [
|
|
279
|
+
{ labelKey: "getStarted", path: "/contact", variant: "primary" },
|
|
280
|
+
{ labelKey: "signIn", path: "/sign-in", variant: "outline-strong" },
|
|
281
|
+
];
|
|
228
282
|
|
|
229
283
|
// Only compose the flow seam when there is a flow action to compose. This
|
|
230
284
|
// layout is a Server Component and `renderFlowTrigger` is a function, so
|
|
@@ -237,8 +291,64 @@ export default async function LocaleLayout({
|
|
|
237
291
|
// this is the caller half of that contract.
|
|
238
292
|
const hasFlowAction = navActions.some((action) => action.kind === "flow");
|
|
239
293
|
|
|
240
|
-
// Shared brand mark injected into both Nav and Footer chrome slots.
|
|
294
|
+
// Shared brand mark injected into both Nav and Footer chrome slots. The
|
|
295
|
+
// mark comes from site-brand.tsx (the light/dark pair the /sign-in card
|
|
296
|
+
// also renders); the Nav adds the shipped wordmark beside it so BrandLogo
|
|
297
|
+
// composes mark + wordmark on desktop and mark-only below `md` (#5469 D5).
|
|
241
298
|
const brandLogo = siteLogo();
|
|
299
|
+
const brandWordmark = {
|
|
300
|
+
src: "/wordmark.svg",
|
|
301
|
+
alt: SITE_NAME,
|
|
302
|
+
width: 140,
|
|
303
|
+
height: 16,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// The locales the footer's language switcher offers — the same list the
|
|
307
|
+
// middleware and hreflang alternates read (NEXT_PUBLIC_ENABLED_LOCALES).
|
|
308
|
+
const enabledLocales = readLocales();
|
|
309
|
+
|
|
310
|
+
// The announcement bar is HOME-ONLY (#5469 D5): its route gate matches the
|
|
311
|
+
// pathname literally, and under next-intl's `as-needed` prefixing the home
|
|
312
|
+
// path is `/` for the default locale and `/<locale>` for every other one.
|
|
313
|
+
const homePath = locale === DEFAULT_LOCALE ? "/" : `/${locale}`;
|
|
314
|
+
|
|
315
|
+
// Deck home (#5612 D3): `siteChrome.home === "deck"` only ever applies
|
|
316
|
+
// `snap="mandatory"` + `deckHero` on the locale-root route — every other
|
|
317
|
+
// path renders byte-identically regardless of the knob. `x-pathname` is the
|
|
318
|
+
// same header `generateMetadata` above reads (forwarded by middleware.ts);
|
|
319
|
+
// Next.js's `headers()` is request-scoped and cheap to call again here.
|
|
320
|
+
const pathname = (await headers()).get("x-pathname") ?? homePath;
|
|
321
|
+
const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
|
|
322
|
+
|
|
323
|
+
// Placeholder social handles + copyright (#5469 D5): `{{COMPANY_HANDLE}}` /
|
|
324
|
+
// `{{COMPANY_NAME}}` are create-ailk markers substituted at scaffold time
|
|
325
|
+
// (packages/create-ailk/src/programmatic.ts), so a customer's site never
|
|
326
|
+
// carries the kit vendor's own identity here. The year is computed, never
|
|
327
|
+
// typed, so the placeholder does not go stale.
|
|
328
|
+
const socialLinks = [
|
|
329
|
+
{ platform: "facebook", href: "https://facebook.com/{{COMPANY_HANDLE}}" },
|
|
330
|
+
{ platform: "instagram", href: "https://instagram.com/{{COMPANY_HANDLE}}" },
|
|
331
|
+
{ platform: "x", href: "https://x.com/{{COMPANY_HANDLE}}" },
|
|
332
|
+
{
|
|
333
|
+
platform: "linkedin",
|
|
334
|
+
href: "https://linkedin.com/company/{{COMPANY_HANDLE}}",
|
|
335
|
+
},
|
|
336
|
+
{ platform: "youtube", href: "https://youtube.com/@{{COMPANY_HANDLE}}" },
|
|
337
|
+
] as const;
|
|
338
|
+
const copyrightText = `© ${new Date().getFullYear()} {{COMPANY_NAME}}`;
|
|
339
|
+
|
|
340
|
+
// Footer legal links (#5612 D3). The centered variant folds in FAQ — the
|
|
341
|
+
// roster's third always-scaffolded core route (`/faq`, alongside `/privacy`
|
|
342
|
+
// and `/terms`) — since a mark-only nav (its usual pairing, #5589) has
|
|
343
|
+
// nowhere else on the page linking it. The default `columns` footer keeps
|
|
344
|
+
// exactly today's two-entry list, byte-identical.
|
|
345
|
+
const legalLinks = [
|
|
346
|
+
{ label: "Privacy Policy", href: "/privacy" },
|
|
347
|
+
{ label: "Terms of Service", href: "/terms" },
|
|
348
|
+
...(siteChrome.footer === "centered"
|
|
349
|
+
? [{ label: navLabels.items.faq, href: "/faq" }]
|
|
350
|
+
: []),
|
|
351
|
+
];
|
|
242
352
|
|
|
243
353
|
// The site's own declared colours, read from `content/_site.mdx`'s `theme:`
|
|
244
354
|
// block and translated from that customer-facing vocabulary onto a token
|
|
@@ -257,7 +367,11 @@ export default async function LocaleLayout({
|
|
|
257
367
|
);
|
|
258
368
|
|
|
259
369
|
return (
|
|
260
|
-
<html
|
|
370
|
+
<html
|
|
371
|
+
lang={locale}
|
|
372
|
+
dir={isRtl(locale) ? "rtl" : "ltr"}
|
|
373
|
+
data-theme={themeMode}
|
|
374
|
+
>
|
|
261
375
|
<head>
|
|
262
376
|
{/* Flash-prevention: runs synchronously before any styled element */}
|
|
263
377
|
<script
|
|
@@ -271,10 +385,24 @@ export default async function LocaleLayout({
|
|
|
271
385
|
/>
|
|
272
386
|
{/* Favicons — single brand mark (theme-independent), PNG fallbacks, ICO legacy */}
|
|
273
387
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
274
|
-
<link
|
|
275
|
-
|
|
388
|
+
<link
|
|
389
|
+
rel="icon"
|
|
390
|
+
type="image/png"
|
|
391
|
+
sizes="32x32"
|
|
392
|
+
href="/favicon-32x32.png"
|
|
393
|
+
/>
|
|
394
|
+
<link
|
|
395
|
+
rel="icon"
|
|
396
|
+
type="image/png"
|
|
397
|
+
sizes="16x16"
|
|
398
|
+
href="/favicon-16x16.png"
|
|
399
|
+
/>
|
|
276
400
|
<link rel="shortcut icon" href="/favicon.ico" />
|
|
277
|
-
<link
|
|
401
|
+
<link
|
|
402
|
+
rel="apple-touch-icon"
|
|
403
|
+
sizes="180x180"
|
|
404
|
+
href="/apple-touch-icon.png"
|
|
405
|
+
/>
|
|
278
406
|
<link rel="manifest" href="/site.webmanifest" />
|
|
279
407
|
<GoogleAnalytics />
|
|
280
408
|
<VerificationMeta />
|
|
@@ -294,13 +422,35 @@ export default async function LocaleLayout({
|
|
|
294
422
|
*/}
|
|
295
423
|
<MarketingShell
|
|
296
424
|
contentOwnsMain
|
|
425
|
+
gutters="plain"
|
|
426
|
+
bannerPlacement="above-nav"
|
|
427
|
+
{...(siteChrome.announcementBanner
|
|
428
|
+
? {
|
|
429
|
+
banner: (
|
|
430
|
+
<Banner
|
|
431
|
+
variant="contrast"
|
|
432
|
+
placement="above-nav"
|
|
433
|
+
message="Medium length banner heading with link goes here"
|
|
434
|
+
action={{ label: "Link", href: "/pricing", kind: "link" }}
|
|
435
|
+
routes={{ include: [homePath] }}
|
|
436
|
+
dismissible={false}
|
|
437
|
+
locale={locale}
|
|
438
|
+
/>
|
|
439
|
+
),
|
|
440
|
+
}
|
|
441
|
+
: {})}
|
|
442
|
+
{...(isDeckHome
|
|
443
|
+
? { snap: "mandatory" as const, deckHero: true }
|
|
444
|
+
: {})}
|
|
297
445
|
nav={
|
|
298
446
|
<Nav
|
|
299
447
|
locale={locale}
|
|
300
448
|
labels={navLabels}
|
|
301
|
-
navItems={navItems}
|
|
449
|
+
navItems={siteChrome.nav === "mark-only" ? [] : navItems}
|
|
302
450
|
siteName={SITE_NAME}
|
|
451
|
+
showThemeToggle={false}
|
|
303
452
|
logo={brandLogo}
|
|
453
|
+
wordmark={brandWordmark}
|
|
304
454
|
overflowLabel={navOverflowLabel}
|
|
305
455
|
actions={navActions}
|
|
306
456
|
{...(hasFlowAction
|
|
@@ -327,8 +477,22 @@ export default async function LocaleLayout({
|
|
|
327
477
|
navItems={navItems}
|
|
328
478
|
siteName={SITE_NAME}
|
|
329
479
|
logo={brandLogo}
|
|
330
|
-
|
|
331
|
-
|
|
480
|
+
social={[...socialLinks]}
|
|
481
|
+
{...(siteChrome.footer === "centered"
|
|
482
|
+
? { layout: "centered" as const }
|
|
483
|
+
: {})}
|
|
484
|
+
showThemeToggle
|
|
485
|
+
copyrightText={copyrightText}
|
|
486
|
+
legalLinks={legalLinks}
|
|
487
|
+
languageSwitcher={
|
|
488
|
+
<LocaleSwitcherNav
|
|
489
|
+
current={locale}
|
|
490
|
+
locales={enabledLocales}
|
|
491
|
+
pathname={pathname}
|
|
492
|
+
/>
|
|
493
|
+
}
|
|
494
|
+
// #4157 — deliberately left as a literal "Built by Working
|
|
495
|
+
// Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
|
|
332
496
|
// the OSS TEMPLATE this site is built with (a "powered by"
|
|
333
497
|
// link, unconditional in OSS forks per the comment above),
|
|
334
498
|
// never the customer's own identity, so it isn't the vendor-
|
|
@@ -336,7 +500,7 @@ export default async function LocaleLayout({
|
|
|
336
500
|
// docs/specs/4157-scaffold-brand-defaults-spec.md § Design
|
|
337
501
|
// Decisions for the fuller reasoning.
|
|
338
502
|
attribution={{
|
|
339
|
-
label: "Built
|
|
503
|
+
label: "Built by Working Theory Labs",
|
|
340
504
|
href: "https://ailaunchkit.ai",
|
|
341
505
|
}}
|
|
342
506
|
/>
|
|
@@ -344,6 +508,39 @@ export default async function LocaleLayout({
|
|
|
344
508
|
>
|
|
345
509
|
{children}
|
|
346
510
|
</MarketingShell>
|
|
511
|
+
{/*
|
|
512
|
+
* Cookie consent (#5557). Bottom-pinned `info` Banner in consent
|
|
513
|
+
* mode: `secondaryAction` makes Banner compose BannerConsentIsland,
|
|
514
|
+
* which reads/writes the `ailk-consent` cookie itself — a fresh
|
|
515
|
+
* visitor sees it, an accepted/declined one does not, on every
|
|
516
|
+
* page. Out of the shell's flow (placement `bottom` is a fixed
|
|
517
|
+
* overlay), so accepting causes no layout shift.
|
|
518
|
+
*/}
|
|
519
|
+
{consentDecided ? null : (
|
|
520
|
+
<Banner
|
|
521
|
+
variant="info"
|
|
522
|
+
placement="bottom"
|
|
523
|
+
consentCookieName={CONSENT_COOKIE_NAME}
|
|
524
|
+
title={tConsent("title")}
|
|
525
|
+
message={
|
|
526
|
+
<>
|
|
527
|
+
{tConsent("body")}{" "}
|
|
528
|
+
<Link
|
|
529
|
+
href={localeHref(locale, "/privacy")}
|
|
530
|
+
variant="unstyled"
|
|
531
|
+
className="font-semibold text-current underline underline-offset-4"
|
|
532
|
+
>
|
|
533
|
+
{tConsent("policyLinkLabel")}
|
|
534
|
+
</Link>
|
|
535
|
+
</>
|
|
536
|
+
}
|
|
537
|
+
dismissible={false}
|
|
538
|
+
acceptLabel={tConsent("acceptLabel")}
|
|
539
|
+
secondaryAction={{ label: tConsent("rejectLabel") }}
|
|
540
|
+
rejectLabel={tConsent("rejectLabel")}
|
|
541
|
+
locale={locale}
|
|
542
|
+
/>
|
|
543
|
+
)}
|
|
347
544
|
<Telemetry />
|
|
348
545
|
</NextIntlClientProvider>
|
|
349
546
|
</body>
|
|
@@ -12,11 +12,19 @@
|
|
|
12
12
|
import Script from "next/script";
|
|
13
13
|
import type { ReactElement } from "react";
|
|
14
14
|
|
|
15
|
+
import { TOKEN_QUERY_PARAMS } from "../lib/telemetry-scrub";
|
|
16
|
+
|
|
15
17
|
export function GoogleAnalytics(): ReactElement | null {
|
|
16
18
|
const tagId = process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
|
|
17
19
|
if (!tagId) {
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
22
|
+
// The init script below runs in the browser as raw inline JS, outside any
|
|
23
|
+
// module graph, so it can't `import` telemetry-scrub.ts — it gets its own
|
|
24
|
+
// copy of scrubTelemetryUrl, driven by the shared param list, so the
|
|
25
|
+
// report-grant token (?g=<id>, #5562) never reaches GA's page_location
|
|
26
|
+
// default (document.location.href) (#5613).
|
|
27
|
+
const scrubParamsJson = JSON.stringify(TOKEN_QUERY_PARAMS);
|
|
20
28
|
return (
|
|
21
29
|
<>
|
|
22
30
|
<Script
|
|
@@ -27,7 +35,16 @@ export function GoogleAnalytics(): ReactElement | null {
|
|
|
27
35
|
{`window.dataLayer = window.dataLayer || [];
|
|
28
36
|
function gtag(){dataLayer.push(arguments);}
|
|
29
37
|
gtag('js', new Date());
|
|
30
|
-
|
|
38
|
+
function scrubTelemetryUrl(url) {
|
|
39
|
+
try {
|
|
40
|
+
var parsed = new URL(url);
|
|
41
|
+
${scrubParamsJson}.forEach(function (param) { parsed.searchParams.delete(param); });
|
|
42
|
+
return parsed.toString();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
gtag('config', '${tagId}', { page_location: scrubTelemetryUrl(window.location.href) });`}
|
|
31
48
|
</Script>
|
|
32
49
|
</>
|
|
33
50
|
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Self-contained ESLint flat config for this scaffolded package.
|
|
2
|
+
//
|
|
3
|
+
// It deliberately does NOT depend on @working-theory/eslint-config — that
|
|
4
|
+
// package is internal to the AI Launch Kit monorepo and is not published, so
|
|
5
|
+
// it cannot be a dependency of a scaffolded starter. Everything below comes
|
|
6
|
+
// from public npm packages already pinned as devDependencies (see
|
|
7
|
+
// package.json): @eslint/js, globals and typescript-eslint.
|
|
8
|
+
//
|
|
9
|
+
// Source file name (#5556): this file is checked in as `_eslint.config.mjs`,
|
|
10
|
+
// not `eslint.config.mjs` — build-templates.mjs renames it at snapshot time
|
|
11
|
+
// (renameScaffoldLintConfigs()). A file literally named `eslint.config.mjs`
|
|
12
|
+
// anywhere under this monorepo (even one this repo's own root eslint.config.ts
|
|
13
|
+
// lists in its `ignores`) is auto-discovered by ESLint's flat-config nested
|
|
14
|
+
// resolution as a competing config root, which breaks the monorepo's own
|
|
15
|
+
// lint runs (verified live: typescript-eslint's "multiple candidate
|
|
16
|
+
// TSConfigRootDirs" parser error across unrelated packages/create-ailk
|
|
17
|
+
// files). The underscore prefix keeps it invisible to that discovery while
|
|
18
|
+
// it lives in this checkout; only the snapshot in templates/ — which ships
|
|
19
|
+
// to the customer, not this repo — gets the real filename.
|
|
20
|
+
import js from "@eslint/js";
|
|
21
|
+
import globals from "globals";
|
|
22
|
+
import tseslint from "typescript-eslint";
|
|
23
|
+
|
|
24
|
+
export default tseslint.config(
|
|
25
|
+
{
|
|
26
|
+
ignores: [
|
|
27
|
+
"**/.next/**",
|
|
28
|
+
"**/dist/**",
|
|
29
|
+
"**/generated/**",
|
|
30
|
+
"**/node_modules/**",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
js.configs.recommended,
|
|
34
|
+
...tseslint.configs.recommended,
|
|
35
|
+
{
|
|
36
|
+
languageOptions: {
|
|
37
|
+
globals: {
|
|
38
|
+
...globals.node,
|
|
39
|
+
...globals.browser,
|
|
40
|
+
...globals.jest,
|
|
41
|
+
},
|
|
42
|
+
// Explicit, self-contained root — this scaffold config is never meant
|
|
43
|
+
// to combine with any sibling scaffold package's config (each ships
|
|
44
|
+
// standalone), and leaving it unset makes typescript-eslint's
|
|
45
|
+
// candidate-root disambiguation ambiguous the moment more than one
|
|
46
|
+
// scaffold package's config resolves in the same ESLint process (e.g.
|
|
47
|
+
// this monorepo's own `templates/` build, where all four exist
|
|
48
|
+
// side by side; a real customer scaffold never has that problem,
|
|
49
|
+
// since it only ever ships and lints ONE of these four at a time).
|
|
50
|
+
parserOptions: {
|
|
51
|
+
tsconfigRootDir: import.meta.dirname,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
rules: {
|
|
55
|
+
// Downgraded rather than off, so both stay visible without failing a
|
|
56
|
+
// fresh scaffold's lint:
|
|
57
|
+
// - unused-vars: most of the source follows the underscore-prefix
|
|
58
|
+
// convention for intentionally-unused bindings, which this rule's
|
|
59
|
+
// default options don't recognize; a handful of test fixtures also
|
|
60
|
+
// carry genuinely-unused locals this minimal config doesn't try to
|
|
61
|
+
// triage file-by-file.
|
|
62
|
+
// - require-imports: CommonJS require() is the correct form in Jest
|
|
63
|
+
// config/mock files and in a few test-only dynamic re-require
|
|
64
|
+
// patterns.
|
|
65
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
66
|
+
"@typescript-eslint/no-require-imports": "warn",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
@@ -58,9 +58,15 @@ module.exports = {
|
|
|
58
58
|
"<rootDir>/node_modules/@working-theory/schema-validator/dist/index.js",
|
|
59
59
|
"^@working-theory/sections-registry$":
|
|
60
60
|
"<rootDir>/node_modules/@working-theory/sections-registry/dist/index.js",
|
|
61
|
-
|
|
62
61
|
"^@working-theory/templates/shells$":
|
|
63
62
|
"<rootDir>/node_modules/@working-theory/templates/dist/shells/index.js",
|
|
63
|
+
// Pure-data routes-map subpath (#5572 U2) — mapped directly to source so
|
|
64
|
+
// a consumer of the lightweight `./routes` export (routes.ts,
|
|
65
|
+
// next.config.mjs, this suite) never pulls in the templates package's
|
|
66
|
+
// shells (React/lucide-react) chain, mirroring the same entry in the
|
|
67
|
+
// root and create-ailk jest configs.
|
|
68
|
+
"^@working-theory/templates/routes$":
|
|
69
|
+
"<rootDir>/node_modules/@working-theory/templates/dist/routes-map.js",
|
|
64
70
|
"^@working-theory/templates$": "<rootDir>/node_modules/@working-theory/templates/dist/index.js",
|
|
65
71
|
"^@working-theory/ui$": "<rootDir>/node_modules/@working-theory/ui/dist/index.js",
|
|
66
72
|
"^@working-theory/ui/blocks$": "<rootDir>/node_modules/@working-theory/ui/dist/blocks/index.js",
|
|
@@ -69,20 +75,6 @@ module.exports = {
|
|
|
69
75
|
"<rootDir>/node_modules/@working-theory/ui/dist/primitives/index.js",
|
|
70
76
|
"^@working-theory/ui/panels$": "<rootDir>/node_modules/@working-theory/ui/dist/panels/index.js",
|
|
71
77
|
"^@working-theory/ui/sections$": "<rootDir>/node_modules/@working-theory/ui/dist/sections/index.js",
|
|
72
|
-
// #5214: the scheduling slice's renderer-registration subpath. Mapped to
|
|
73
|
-
// source like every sibling above — packages/ui/dist isn't built in this
|
|
74
|
-
// workspace pre-`pnpm build`. The target strips with the slice on the public
|
|
75
|
-
// tree, where nothing imports it, so the unused mapping is inert there.
|
|
76
|
-
"^@working-theory/ui/slices/scheduling$":
|
|
77
|
-
"<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/scheduling.js",
|
|
78
|
-
// #5366: the signal-collectors slice's re-export subpath. Same reasoning
|
|
79
|
-
// as the scheduling mapping above.
|
|
80
|
-
"^@working-theory/ui/slices/signal-collectors$":
|
|
81
|
-
"<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/signal-collectors.js",
|
|
82
|
-
// #5364: the commerce slice's renderer-registration + CheckoutPriceCard
|
|
83
|
-
// re-export subpath. Same reasoning as the scheduling mapping above.
|
|
84
|
-
"^@working-theory/ui/slices/commerce$":
|
|
85
|
-
"<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/commerce.js",
|
|
86
78
|
"^@working-theory/theme$": "<rootDir>/node_modules/@working-theory/theme/dist/index.js",
|
|
87
79
|
// site-config publishes ESM in dist/; map to source so ts-jest compiles it
|
|
88
80
|
// through the same transformer as every sibling above. Only the
|
|
@@ -310,7 +310,7 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
310
310
|
reason: "interactive flow engine, API-backed",
|
|
311
311
|
subtree: true,
|
|
312
312
|
},
|
|
313
|
-
{ route: "
|
|
313
|
+
{ route: "meet", reason: "booking surface, API-backed", subtree: true },
|
|
314
314
|
{ route: "sign-in", reason: "auth surface, not a content page" },
|
|
315
315
|
];
|
|
316
316
|
|
|
@@ -345,14 +345,36 @@ function exemption(route: string): string | undefined {
|
|
|
345
345
|
)?.reason;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
// The page identity a route renders — the
|
|
349
|
-
//
|
|
350
|
-
// slug
|
|
351
|
-
// (
|
|
348
|
+
// The page identity a route renders — the three shapes
|
|
349
|
+
// tools/lint-check-content-renders.ts recognises: `renderPage({ type, slug })`
|
|
350
|
+
// with a literal slug, the same with a slug taken from params, and a direct
|
|
351
|
+
// `getMdxSource("<Type>", slug, …)` for a route that composes its own body.
|
|
352
|
+
// This file previously mirrored only the first two and so drifted from the
|
|
353
|
+
// tool its comment names (whose own `GET_MDX_SOURCE_RE` has carried the third
|
|
354
|
+
// arm all along); #5562 restored the parity. A route that reads its identity
|
|
355
|
+
// some other way (e.g. a page-type manifest) is still unrecognised and must
|
|
356
|
+
// be declared.
|
|
352
357
|
const RENDER_PAGE_EXACT_RE =
|
|
353
358
|
/renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*slug\s*:\s*["']([^"']+)["']/s;
|
|
354
359
|
const RENDER_PAGE_DYNAMIC_RE =
|
|
355
360
|
/renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*\bslug\s*(?!:\s*["'])/s;
|
|
361
|
+
// A route that composes its own body instead of calling `renderPage` — it
|
|
362
|
+
// resolves content with `getMdxSource(<Type>, slug, locale)` and emits the
|
|
363
|
+
// graph itself via `pageToJsonLd` + `<JsonLdScript>`. That is not a route
|
|
364
|
+
// hiding from this check; it is the SAME declaration one level down, since
|
|
365
|
+
// `renderPage` resolves its own content with exactly this call. `/reports/
|
|
366
|
+
// [slug]` (#5562) is the first: it owns a full-bleed `<main>` and a header
|
|
367
|
+
// section that already renders the title, so `renderPage`'s own `<h1>`/`<p>`
|
|
368
|
+
// would duplicate it.
|
|
369
|
+
//
|
|
370
|
+
// Recognising the shape is deliberately better than exempting the route:
|
|
371
|
+
// an exemption declares "nothing is known to emit JSON-LD here" — false for
|
|
372
|
+
// these routes — and would silence the check for a page that DOES emit a
|
|
373
|
+
// graph. Recognised, the route is rendered and validated like any other.
|
|
374
|
+
// Matched only after both `renderPage` shapes above, so a route that uses
|
|
375
|
+
// `renderPage` is still read through that (richer, slug-bearing) form.
|
|
376
|
+
// Mirrors `GET_MDX_SOURCE_RE` in tools/lint-check-content-renders.ts.
|
|
377
|
+
const GET_MDX_SOURCE_RE = /getMdxSource\s*\(\s*["']([A-Za-z]+)["']/s;
|
|
356
378
|
|
|
357
379
|
function routeIdentity(
|
|
358
380
|
file: string,
|
|
@@ -362,6 +384,11 @@ function routeIdentity(
|
|
|
362
384
|
if (exact) return { type: exact[1] as string, slug: exact[2] as string };
|
|
363
385
|
const dynamic = RENDER_PAGE_DYNAMIC_RE.exec(src);
|
|
364
386
|
if (dynamic) return { type: dynamic[1] as string, slug: null };
|
|
387
|
+
// Self-composing route (see GET_MDX_SOURCE_RE). Its slug always comes from
|
|
388
|
+
// the route params — a self-composing FIXED route would name its slug as a
|
|
389
|
+
// literal to `getMdxSource`, and none exists — so the dynamic path applies.
|
|
390
|
+
const ownBody = GET_MDX_SOURCE_RE.exec(src);
|
|
391
|
+
if (ownBody) return { type: ownBody[1] as string, slug: null };
|
|
365
392
|
return null;
|
|
366
393
|
}
|
|
367
394
|
|
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
// nav-config.ts — Nav item registry for apps/web
|
|
1
|
+
// nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469).
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
// Paths are locale-agnostic; Nav
|
|
3
|
+
// The Free marketing-site template's nav (the approved chrome, #5469 D5):
|
|
4
|
+
// a Product flyout (How it works · Features · FAQ), then How it works,
|
|
5
|
+
// Pricing and About as top-level items. Paths are locale-agnostic; Nav
|
|
6
|
+
// prefixes them with the active locale at render.
|
|
7
|
+
//
|
|
8
|
+
// In a SCAFFOLD this file is regenerated by create-ailk from the declared
|
|
9
|
+
// route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
|
|
10
|
+
// so the nav never links a page the scaffold does not carry — keep the two in
|
|
11
|
+
// step when the base item set changes.
|
|
6
12
|
|
|
7
13
|
import type { NavItem } from "@working-theory/ui";
|
|
14
|
+
import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
|
|
8
15
|
|
|
9
16
|
export const navItems: NavItem[] = [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
{
|
|
18
|
+
labelKey: "product",
|
|
19
|
+
flyout: {
|
|
20
|
+
presentation: "list",
|
|
21
|
+
width: "trigger",
|
|
22
|
+
features: [
|
|
23
|
+
{ labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },
|
|
24
|
+
{ labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },
|
|
25
|
+
{ labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
|
|
26
|
+
],
|
|
27
|
+
actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{ labelKey: "howItWorks", path: "/how-it-works" },
|
|
31
|
+
{ labelKey: "pricing", path: "/pricing" },
|
|
32
|
+
{ labelKey: "about", path: "/about" },
|
|
13
33
|
];
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* hand-rolled `fetch`. This module supplies exactly the two things that
|
|
18
18
|
* namespace doesn't own: the cookie/origin-forwarding `init` (B0's additive
|
|
19
19
|
* `init?: { headers }` on `http.get`) and the envelope→`ApiError` unwrap
|
|
20
|
-
* (mirrors `apps-paid/
|
|
20
|
+
* (mirrors `apps-paid/admin/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
|
|
21
21
|
* the `@working-theory/api-client` `ApiResult<T>` shape B0 already returns
|
|
22
22
|
* rather than re-parsing the wire envelope by hand).
|
|
23
23
|
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// site-chrome.ts — resolved template layout knobs.
|
|
2
|
+
//
|
|
3
|
+
// GENERATED by create-ailk from the declared template's `layout` block
|
|
4
|
+
// (#5612). A scaffold with no template, or a template with no `layout`
|
|
5
|
+
// block, gets these defaults — byte-identical to every scaffold before this
|
|
6
|
+
// file existed. `apps/web/app/[locale]/layout.tsx` reads this to decide:
|
|
7
|
+
// - nav: "mark-only" → Nav renders the brand mark + right-hand controls
|
|
8
|
+
// only (no nav links).
|
|
9
|
+
// - footer: "centered" → Footer renders its existing centered variant.
|
|
10
|
+
// - announcementBanner: false → the home-only announcement Banner is not
|
|
11
|
+
// mounted (the cookie-consent Banner is unaffected — it is not an
|
|
12
|
+
// announcement).
|
|
13
|
+
// - home: "deck" → the locale-root route mounts MarketingShell with
|
|
14
|
+
// snap="mandatory" + deckHero (spec 5426 D7); every other route is
|
|
15
|
+
// unaffected.
|
|
16
|
+
|
|
17
|
+
export interface SiteChrome {
|
|
18
|
+
nav: "full" | "mark-only";
|
|
19
|
+
footer: "columns" | "centered";
|
|
20
|
+
announcementBanner: boolean;
|
|
21
|
+
home: "default" | "deck";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const siteChrome: SiteChrome = {
|
|
25
|
+
nav: "full",
|
|
26
|
+
footer: "columns",
|
|
27
|
+
announcementBanner: true,
|
|
28
|
+
home: "default",
|
|
29
|
+
};
|