create-ailk 0.7.0 → 0.9.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 +112 -10
- package/component-catalog.json +798 -49
- package/component-catalog.md +9 -6
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +114 -28
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +89 -0
- package/dist/lib/module-license-gate.js +171 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +64 -21
- package/dist/parse-args.d.ts +88 -10
- package/dist/parse-args.js +272 -21
- package/dist/programmatic.d.ts +66 -12
- package/dist/programmatic.js +361 -34
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +149 -11
- package/dist/starter-grid.d.ts +232 -0
- package/dist/starter-grid.js +545 -0
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +268 -92
- package/dist/template-manifest.d.ts +65 -29
- package/dist/template-manifest.js +161 -382
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +5 -4
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
- package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/designs/standard/DESIGN.md +103 -6
- package/site-templates/marketing-site/DESIGN.md +103 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
- package/site-templates/marketing-site-free/DESIGN.md +103 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/saas-product/DESIGN.md +103 -6
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/rules/architecture.md +7 -0
- package/templates/.claude/skills/README.md +5 -3
- package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +85 -72
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +23 -0
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +103 -6
- package/templates/README.md +63 -43
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +35 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +65 -17
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +50 -37
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +45 -5
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +43 -34
- package/templates/content/en/HomePage/home.mdx +10 -22
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
- /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
|
@@ -20,16 +20,21 @@
|
|
|
20
20
|
* true of any declared palette. Deleting the `theme:` block still fails, on the
|
|
21
21
|
* `toBeDefined()` below.
|
|
22
22
|
*/
|
|
23
|
-
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { tmpdir } from "node:os";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
|
|
27
|
-
import { parse as parseYaml } from "yaml";
|
|
27
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
28
28
|
|
|
29
|
-
import { PartialColorPaletteSchema } from "@working-theory/site-config/schema";
|
|
30
|
-
import {
|
|
29
|
+
import { PartialColorPaletteSchema, SiteThemeSchema } from "@working-theory/site-config/schema";
|
|
30
|
+
import {
|
|
31
|
+
siteColorTokenMap,
|
|
32
|
+
siteThemeShapeCss,
|
|
33
|
+
siteThemeToBrandTokens,
|
|
34
|
+
themeToCssVarsBothModes,
|
|
35
|
+
} from "@working-theory/theme";
|
|
31
36
|
|
|
32
|
-
import { loadSiteBrandTokens } from "../site-theme";
|
|
37
|
+
import { loadSiteBrandTokens, loadSiteShapeCss } from "../site-theme";
|
|
33
38
|
|
|
34
39
|
const REPO_CONTENT_ROOT = join(__dirname, "..", "..", "..", "..", "content");
|
|
35
40
|
|
|
@@ -46,19 +51,28 @@ function darkBlock(css: string): string {
|
|
|
46
51
|
/** The two modes, as the palette and the emitted CSS both name them. */
|
|
47
52
|
const MODES = ["light", "dark"] as const;
|
|
48
53
|
|
|
54
|
+
type ModeColors = Partial<Record<(typeof MODES)[number], Record<string, string>>>;
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
57
|
+
* The colours the `_site.mdx` under test declares — `theme.colors` with any
|
|
58
|
+
* `brand.colors` (the brand triad, #6820) layered over it per mode — as raw
|
|
59
|
+
* SLOT names (`text`, `primary`), not token keys. Read with the same
|
|
52
60
|
* frontmatter parse `site-theme.ts` uses, so the test cannot pass against a
|
|
53
|
-
* file the implementation reads differently
|
|
61
|
+
* file the implementation reads differently, and derived from the layered
|
|
62
|
+
* result so a scaffold whose owner sets a triad stays green.
|
|
54
63
|
*/
|
|
55
|
-
function declaredColors():
|
|
64
|
+
function declaredColors(): ModeColors {
|
|
56
65
|
const raw = readFileSync(join(REPO_CONTENT_ROOT, "_site.mdx"), "utf8");
|
|
57
66
|
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
58
67
|
const parsed = parseYaml(frontmatter?.[1] ?? "") as
|
|
59
|
-
| { theme?: { colors?:
|
|
68
|
+
| { theme?: { colors?: ModeColors }; brand?: { colors?: ModeColors } }
|
|
60
69
|
| null;
|
|
61
|
-
|
|
70
|
+
const out: ModeColors = {};
|
|
71
|
+
for (const mode of MODES) {
|
|
72
|
+
const layered = { ...parsed?.theme?.colors?.[mode], ...parsed?.brand?.colors?.[mode] };
|
|
73
|
+
if (Object.keys(layered).length > 0) out[mode] = layered;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
/**
|
|
@@ -210,6 +224,85 @@ describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
|
|
|
210
224
|
});
|
|
211
225
|
});
|
|
212
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The brand triad (#6820) — `brand.colors` / `brand.fonts` layered over the
|
|
229
|
+
* theme. The golden: a site with a theme and no triad emits exactly the CSS
|
|
230
|
+
* the pre-triad formula emitted; a site with a triad carries its fonts and
|
|
231
|
+
* primary into both mode blocks.
|
|
232
|
+
*/
|
|
233
|
+
describe("loadSiteBrandTokens — the brand triad", () => {
|
|
234
|
+
const originalRoot = process.env.AILK_CONTENT_ROOT;
|
|
235
|
+
const tempRoots: string[] = [];
|
|
236
|
+
|
|
237
|
+
afterEach(() => {
|
|
238
|
+
if (originalRoot === undefined) delete process.env.AILK_CONTENT_ROOT;
|
|
239
|
+
else process.env.AILK_CONTENT_ROOT = originalRoot;
|
|
240
|
+
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** The repo's own `theme:` block, as YAML text and as parsed. */
|
|
244
|
+
function repoTheme(): { yaml: string; parsed: unknown } {
|
|
245
|
+
const raw = readFileSync(join(REPO_CONTENT_ROOT, "_site.mdx"), "utf8");
|
|
246
|
+
const parsed = (
|
|
247
|
+
parseYaml(/^---\r?\n([\s\S]*?)\r?\n---/.exec(raw)?.[1] ?? "") as { theme?: unknown }
|
|
248
|
+
).theme;
|
|
249
|
+
return { yaml: stringifyYaml({ theme: parsed }), parsed };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function useSiteMdx(frontmatter: string): void {
|
|
253
|
+
const root = mkdtempSync(join(tmpdir(), "ailk-site-triad-"));
|
|
254
|
+
tempRoots.push(root);
|
|
255
|
+
writeFileSync(join(root, "_site.mdx"), `---\nsiteName: Test\n${frontmatter}---\n`, "utf8");
|
|
256
|
+
process.env.AILK_CONTENT_ROOT = root;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The layout's emission, verbatim (`app/[locale]/layout.tsx`). */
|
|
260
|
+
function layoutCss(): string {
|
|
261
|
+
const brand = loadSiteBrandTokens();
|
|
262
|
+
return themeToCssVarsBothModes(brand ? { brand } : {}) + loadSiteShapeCss();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
it("golden: the repo's theme with no triad emits the pre-triad CSS byte for byte", () => {
|
|
266
|
+
const { yaml, parsed } = repoTheme();
|
|
267
|
+
useSiteMdx(`${yaml}brand:\n logomark: { light: /logomark.svg, dark: /logomark.svg, alt: Test }\n`);
|
|
268
|
+
|
|
269
|
+
const theme = SiteThemeSchema.parse(parsed);
|
|
270
|
+
const before = siteThemeToBrandTokens(theme);
|
|
271
|
+
expect(layoutCss()).toBe(
|
|
272
|
+
themeToCssVarsBothModes(before ? { brand: before } : {}) + siteThemeShapeCss(theme),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("golden: no _site.mdx emits the default CSS", () => {
|
|
277
|
+
process.env.AILK_CONTENT_ROOT = join(__dirname, "no-such-content-root");
|
|
278
|
+
expect(layoutCss()).toBe(themeToCssVarsBothModes({}));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("a declared triad reaches --font-display / --font-body and --color-brand in both blocks", () => {
|
|
282
|
+
const { yaml } = repoTheme();
|
|
283
|
+
useSiteMdx(
|
|
284
|
+
`${yaml}brand:\n` +
|
|
285
|
+
` colors:\n light: { primary: "#0a3d62" }\n dark: { primary: "#82ccdd" }\n` +
|
|
286
|
+
` fonts:\n display: "'Fraunces', Georgia, serif"\n body: "Inter, sans-serif"\n`,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const css = layoutCss();
|
|
290
|
+
for (const [mode, block] of [
|
|
291
|
+
["light", lightBlock(css)],
|
|
292
|
+
["dark", darkBlock(css)],
|
|
293
|
+
] as const) {
|
|
294
|
+
expect(block).toContain("--font-display: 'Fraunces', Georgia, serif;");
|
|
295
|
+
expect(block).toContain("--font-body: Inter, sans-serif;");
|
|
296
|
+
expect(block).toContain(`--color-brand: ${mode === "light" ? "#0a3d62" : "#82ccdd"};`);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("refuses an unknown brand key by name", () => {
|
|
301
|
+
useSiteMdx("brand:\n corners: square\n");
|
|
302
|
+
expect(() => loadSiteBrandTokens()).toThrow(/corners/);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
213
306
|
/**
|
|
214
307
|
* The twelve customer-facing colour slots live in `@working-theory/site-config`
|
|
215
308
|
* and the map from them to token names lives in `@working-theory/theme`;
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
type SchemaOrgType,
|
|
54
54
|
type ValidationError,
|
|
55
55
|
} from "@working-theory/schema-validator";
|
|
56
|
+
|
|
57
|
+
import { MEET_PLACEHOLDER_HUB } from "../meet-placeholder";
|
|
56
58
|
import type { ReactElement } from "react";
|
|
57
59
|
import { parse as parseYaml } from "yaml";
|
|
58
60
|
|
|
@@ -447,19 +449,31 @@ function routeIdentity(
|
|
|
447
449
|
|
|
448
450
|
type PageModule = {
|
|
449
451
|
default: (props: {
|
|
450
|
-
params: Promise<{ locale: string; slug?: string }>;
|
|
452
|
+
params: Promise<{ locale: string; slug?: string; detail?: string }>;
|
|
451
453
|
}) => Promise<ReactElement>;
|
|
452
454
|
};
|
|
453
455
|
|
|
454
456
|
async function renderRoute(
|
|
455
457
|
file: string,
|
|
458
|
+
route: string,
|
|
456
459
|
slug: string | undefined,
|
|
457
460
|
): Promise<HTMLElement> {
|
|
458
461
|
// Resolved at test time from the walk above — a static import would dangle
|
|
459
462
|
// in a scaffold that gated the surface off.
|
|
460
463
|
const mod = require(file) as PageModule;
|
|
461
|
-
const params: { locale: string; slug?: string } = {
|
|
462
|
-
|
|
464
|
+
const params: { locale: string; slug?: string; detail?: string } = {
|
|
465
|
+
locale: DEFAULT_LOCALE,
|
|
466
|
+
};
|
|
467
|
+
// #6897 D2 — params are built from the route's OWN dynamic segments: the
|
|
468
|
+
// `[slug]/[detail]` placeholder route's `slug` param is fixed to the meet
|
|
469
|
+
// hub, and its content slug rides `detail` instead — never a copied
|
|
470
|
+
// literal for the hub name.
|
|
471
|
+
if (route === "[slug]/[detail]") {
|
|
472
|
+
params.slug = MEET_PLACEHOLDER_HUB;
|
|
473
|
+
if (slug !== undefined) params.detail = slug;
|
|
474
|
+
} else if (slug !== undefined) {
|
|
475
|
+
params.slug = slug;
|
|
476
|
+
}
|
|
463
477
|
const element = await mod.default({ params: Promise.resolve(params) });
|
|
464
478
|
return render(element).container;
|
|
465
479
|
}
|
|
@@ -531,12 +545,15 @@ describe("structured data — every route emits a valid JSON-LD script", () => {
|
|
|
531
545
|
for (const slug of slugs) {
|
|
532
546
|
const container = await renderRoute(
|
|
533
547
|
file,
|
|
548
|
+
route,
|
|
534
549
|
identity.slug === null ? slug : undefined,
|
|
535
550
|
);
|
|
536
551
|
const blocks = extractJsonLdBlocks(container);
|
|
537
552
|
const pageLabel =
|
|
538
553
|
identity.slug === null
|
|
539
|
-
?
|
|
554
|
+
? route === "[slug]/[detail]"
|
|
555
|
+
? `/${DEFAULT_LOCALE}/${MEET_PLACEHOLDER_HUB}/${slug}`
|
|
556
|
+
: `${label.replace(/\[slug\]$/, "")}${slug}`
|
|
540
557
|
: label;
|
|
541
558
|
if (blocks.length === 0) {
|
|
542
559
|
failures.push(
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `[locale]` layout's first check (#6107 F5).
|
|
3
|
+
*
|
|
4
|
+
* A dotted path that is not a file — the `/.well-known` directory, `/foo.bar`
|
|
5
|
+
* — skips the middleware matcher and reaches `app/[locale]` with that segment
|
|
6
|
+
* as `locale`. Anything outside the enabled-locale list (the same list the
|
|
7
|
+
* footer switcher offers) is a 404, not the whole site rendered under a bogus
|
|
8
|
+
* locale.
|
|
9
|
+
*/
|
|
10
|
+
import { readLocales } from "@working-theory/web-i18n";
|
|
11
|
+
import { notFound } from "next/navigation";
|
|
12
|
+
|
|
13
|
+
export function assertEnabledLocale(
|
|
14
|
+
locale: string,
|
|
15
|
+
locales: readonly string[] = readLocales(),
|
|
16
|
+
): void {
|
|
17
|
+
if (!locales.includes(locale)) notFound();
|
|
18
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The Free `/meet/<slug>` placeholder hub (#6897 D2). `MEET_PLACEHOLDER_HUB`
|
|
2
|
+
// is the one fixed top-level slug the `[slug]/[detail]` route answers under —
|
|
3
|
+
// never a copied literal, so the route and the structured-data walk
|
|
4
|
+
// (`apps/web/lib/__tests__/structured-data.test.tsx`) can never drift.
|
|
5
|
+
export const MEET_PLACEHOLDER_HUB = "meet";
|
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
// nav-config.ts —
|
|
2
|
-
// #6508).
|
|
1
|
+
// nav-config.ts — site-nav registry.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// How it works, Pricing and About as top-level items. Paths are
|
|
8
|
-
// locale-agnostic; Nav prefixes them with the active locale at render.
|
|
9
|
-
// `footerColumns` carries the footer's column groups (#6468), keys only — the
|
|
10
|
-
// layout resolves the labels.
|
|
3
|
+
// GENERATED by create-ailk from the declared route surfaces. Every entry here
|
|
4
|
+
// points at a route this site actually carries — the nav is regenerated with
|
|
5
|
+
// the surface set so it can never link a page that was not scaffolded.
|
|
11
6
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// SOFT_LOCK_OK: #6508 — Product's flyout is `width: "panel"` (D4/D5): a fixed
|
|
18
|
-
// 640px, two-column grid, replacing the prior "list"/"trigger" one-column
|
|
19
|
-
// menu. `presentation` is omitted — it defaults to "grid" (D5). The `docs`
|
|
20
|
-
// action is dropped (it duplicated the first feature's /how-it-works
|
|
21
|
-
// destination, § 1.4). This note lives here, not inside the `navItems` array
|
|
22
|
-
// below, so this file's `navItems`/`footerColumns` blocks stay byte-identical
|
|
23
|
-
// to `renderNavConfig`'s generated output — the mirror test (#6468) diffs the
|
|
24
|
-
// arrays verbatim.
|
|
25
|
-
|
|
7
|
+
// Adding a link: scaffold the page (the `add-page` skill), then add an
|
|
8
|
+
// entry below with a `labelKey` that exists in the `nav` namespace of
|
|
9
|
+
// `apps/web/messages/<locale>.json`. Paths are locale-agnostic — Nav prefixes
|
|
10
|
+
// them with the active locale at render.
|
|
26
11
|
import type { NavItem } from "@working-theory/ui";
|
|
27
|
-
import { HelpCircle, Layers, Sparkles } from "lucide-react";
|
|
28
12
|
|
|
29
13
|
export const navItems: NavItem[] = [
|
|
30
14
|
{
|
|
@@ -32,19 +16,41 @@ export const navItems: NavItem[] = [
|
|
|
32
16
|
flyout: {
|
|
33
17
|
width: "panel",
|
|
34
18
|
features: [
|
|
35
|
-
{ labelKey: "
|
|
36
|
-
{ labelKey: "
|
|
37
|
-
{ labelKey: "
|
|
19
|
+
{ labelKey: "feature1", path: "/features/feature-1" },
|
|
20
|
+
{ labelKey: "feature2", path: "/features/feature-2" },
|
|
21
|
+
{ labelKey: "feature3", path: "/features/feature-3" },
|
|
22
|
+
{ labelKey: "customer1", path: "/for/persona-1" },
|
|
23
|
+
{ labelKey: "customer2", path: "/for/persona-2" },
|
|
24
|
+
{ labelKey: "customer3", path: "/for/persona-3" },
|
|
25
|
+
],
|
|
26
|
+
groups: [
|
|
27
|
+
{
|
|
28
|
+
headingKey: "flyoutFeaturesHeading",
|
|
29
|
+
items: [
|
|
30
|
+
{ labelKey: "feature1", path: "/features/feature-1" },
|
|
31
|
+
{ labelKey: "feature2", path: "/features/feature-2" },
|
|
32
|
+
{ labelKey: "feature3", path: "/features/feature-3" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
headingKey: "flyoutCustomersHeading",
|
|
37
|
+
items: [
|
|
38
|
+
{ labelKey: "customer1", path: "/for/persona-1" },
|
|
39
|
+
{ labelKey: "customer2", path: "/for/persona-2" },
|
|
40
|
+
{ labelKey: "customer3", path: "/for/persona-3" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
38
43
|
],
|
|
39
44
|
actions: [],
|
|
40
45
|
},
|
|
41
46
|
},
|
|
42
|
-
{ labelKey: "useCases", path: "/use-cases" },
|
|
43
47
|
{ labelKey: "blog", path: "/blog" },
|
|
44
48
|
{ labelKey: "pricing", path: "/pricing" },
|
|
45
49
|
{ labelKey: "about", path: "/about" },
|
|
46
50
|
];
|
|
47
51
|
|
|
52
|
+
// Footer column groups. Keys only — the layout resolves each `headingKey`
|
|
53
|
+
// and `labelKey` through the same `nav` labels as the header.
|
|
48
54
|
export interface FooterColumnConfig {
|
|
49
55
|
headingKey: string;
|
|
50
56
|
items: { labelKey: string; path: string }[];
|
|
@@ -54,23 +60,30 @@ export const footerColumns: FooterColumnConfig[] = [
|
|
|
54
60
|
{
|
|
55
61
|
headingKey: "footerSitePagesHeading",
|
|
56
62
|
items: [
|
|
57
|
-
{ labelKey: "features", path: "/
|
|
63
|
+
{ labelKey: "features", path: "/features" },
|
|
58
64
|
{ labelKey: "pricing", path: "/pricing" },
|
|
59
|
-
{ labelKey: "
|
|
65
|
+
{ labelKey: "portfolio", path: "/portfolio" },
|
|
60
66
|
{ labelKey: "about", path: "/about" },
|
|
61
67
|
{ labelKey: "contact", path: "/contact" },
|
|
62
|
-
{ labelKey: "bookACall", path: "/book-a-call" },
|
|
68
|
+
{ labelKey: "bookACall", path: "/meet/book-a-call" },
|
|
63
69
|
],
|
|
64
70
|
},
|
|
65
71
|
{
|
|
66
72
|
headingKey: "footerFeaturesHeading",
|
|
67
73
|
items: [
|
|
68
|
-
{ labelKey: "feature1", path: "/
|
|
69
|
-
{ labelKey: "feature2", path: "/
|
|
70
|
-
{ labelKey: "feature3", path: "/
|
|
71
|
-
{ labelKey: "
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
{ labelKey: "feature1", path: "/features/feature-1" },
|
|
75
|
+
{ labelKey: "feature2", path: "/features/feature-2" },
|
|
76
|
+
{ labelKey: "feature3", path: "/features/feature-3" },
|
|
77
|
+
{ labelKey: "allFeatures", path: "/features" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
headingKey: "footerCustomersHeading",
|
|
82
|
+
items: [
|
|
83
|
+
{ labelKey: "customer1", path: "/for/persona-1" },
|
|
84
|
+
{ labelKey: "customer2", path: "/for/persona-2" },
|
|
85
|
+
{ labelKey: "customer3", path: "/for/persona-3" },
|
|
86
|
+
{ labelKey: "allCustomers", path: "/for" },
|
|
74
87
|
],
|
|
75
88
|
},
|
|
76
89
|
{
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* rather than a dependency on `@working-theory/site-config`'s `loadSiteConfig`
|
|
21
21
|
* (`packages/site-config/src/server/load.ts`): that package is async (this
|
|
22
22
|
* module's exports are plain constants, read at many synchronous call
|
|
23
|
-
* sites — Nav/Footer props, `siteLogo()
|
|
23
|
+
* sites — Nav/Footer props, the raw logomark `siteLogo()` in `site-logo.tsx` renders, edge-safe route defaults) and
|
|
24
24
|
* brings gray-matter + Zod validation + optimistic-locking writes built for
|
|
25
25
|
* the agent-operator editing workflow — disproportionate for reading two
|
|
26
26
|
* fields. See docs/specs/4157-scaffold-brand-defaults-spec.md § Design
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
import { existsSync, readFileSync } from "node:fs";
|
|
31
31
|
import { join } from "node:path";
|
|
32
32
|
|
|
33
|
-
import type { ReactElement } from "react";
|
|
34
33
|
import { parse as parseYaml } from "yaml";
|
|
35
34
|
|
|
36
35
|
/** Neutral, non-vendor fallback — see module doc comment point 3. */
|
|
@@ -46,6 +45,8 @@ interface SiteFrontmatter {
|
|
|
46
45
|
// `undefined` (a plain `?: string` would forbid that explicit assignment).
|
|
47
46
|
siteName?: string | undefined;
|
|
48
47
|
baseUrl?: string | undefined;
|
|
48
|
+
/** `brand.logomark`, raw — validated by `siteLogo()` when it renders. */
|
|
49
|
+
logomark?: unknown;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -83,10 +84,14 @@ function readSiteFrontmatter(): SiteFrontmatter | null {
|
|
|
83
84
|
const parsed: unknown = parseYaml(match[1] ?? "");
|
|
84
85
|
if (!parsed || typeof parsed !== "object") return null;
|
|
85
86
|
|
|
86
|
-
const { siteName, baseUrl } = parsed as Record<string, unknown>;
|
|
87
|
+
const { siteName, baseUrl, brand } = parsed as Record<string, unknown>;
|
|
87
88
|
return {
|
|
88
89
|
siteName: typeof siteName === "string" ? siteName : undefined,
|
|
89
90
|
baseUrl: typeof baseUrl === "string" ? baseUrl : undefined,
|
|
91
|
+
logomark:
|
|
92
|
+
brand && typeof brand === "object"
|
|
93
|
+
? ((brand as Record<string, unknown>).logomark ?? undefined)
|
|
94
|
+
: undefined,
|
|
90
95
|
};
|
|
91
96
|
} catch {
|
|
92
97
|
return null;
|
|
@@ -108,10 +113,9 @@ export const SITE_BASE_URL =
|
|
|
108
113
|
process.env.NEXT_PUBLIC_BASE_URL ?? siteFrontmatter?.baseUrl ?? FALLBACK_BASE_URL;
|
|
109
114
|
|
|
110
115
|
/**
|
|
111
|
-
*
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
116
|
+
* The raw `brand.logomark` from `_site.mdx`, or `undefined` when none is
|
|
117
|
+
* declared. Unvalidated: `siteLogo()` (`site-logo.tsx`) parses it on the
|
|
118
|
+
* render that uses it, so a malformed mark never breaks `SITE_NAME` /
|
|
119
|
+
* `SITE_BASE_URL` for the routes that read only those.
|
|
114
120
|
*/
|
|
115
|
-
export
|
|
116
|
-
return <img src="/logomark.svg" alt={SITE_NAME} width={28} height={28} className="block" />;
|
|
117
|
-
}
|
|
121
|
+
export const SITE_LOGOMARK: unknown = siteFrontmatter?.logomark;
|
|
@@ -85,6 +85,20 @@ function readFeatureList(contentRoot: string, locale: string): string[] {
|
|
|
85
85
|
const frontmatter = readFrontmatter(join(productDir, name));
|
|
86
86
|
if (frontmatter !== undefined) return featuresOf(frontmatter);
|
|
87
87
|
}
|
|
88
|
+
// #6721 — the Free roster's feature PAGES (one `Feature` seed per feature,
|
|
89
|
+
// #6721) are the featureList when the site carries no product page: each
|
|
90
|
+
// page's title, in sorted file order.
|
|
91
|
+
const featureDir = join(contentRoot, locale, "Feature");
|
|
92
|
+
try {
|
|
93
|
+
const titles = readdirSync(featureDir)
|
|
94
|
+
.filter((name) => name.endsWith(".mdx"))
|
|
95
|
+
.sort()
|
|
96
|
+
.map((name) => readFrontmatter(join(featureDir, name))?.["title"])
|
|
97
|
+
.filter((title): title is string => typeof title === "string" && title.length > 0);
|
|
98
|
+
if (titles.length > 0) return titles;
|
|
99
|
+
} catch {
|
|
100
|
+
// No Feature directory — fall through to the legacy probe.
|
|
101
|
+
}
|
|
88
102
|
let typeDirs: string[];
|
|
89
103
|
try {
|
|
90
104
|
typeDirs = readdirSync(join(contentRoot, locale)).sort();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site logo — the site's declared `brand.logomark` (#6820), else the square
|
|
3
|
+
* logomark (the central brand library's `logomark/loop/tile-black.svg`).
|
|
4
|
+
*
|
|
5
|
+
* Split from `site-brand.tsx`, which ten route and lib modules import for
|
|
6
|
+
* `SITE_NAME` / `SITE_BASE_URL`: the mark needs `@working-theory/ui`'s
|
|
7
|
+
* `Image` and site-config's schema, and those stay out of that wider graph
|
|
8
|
+
* (spec #6820 § 5.4). The two layouts are the only callers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BrandLogomarkSchema } from "@working-theory/site-config/schema";
|
|
12
|
+
import { Image } from "@working-theory/ui/primitives";
|
|
13
|
+
import type { ReactElement } from "react";
|
|
14
|
+
|
|
15
|
+
import { SITE_LOGOMARK, SITE_NAME } from "./site-brand";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared brand mark. The Nav pairs it with `/wordmark.svg`. RSC-safe (no
|
|
19
|
+
* hooks, no client state).
|
|
20
|
+
*
|
|
21
|
+
* The logomark is validated here, on the render that uses it, not at module
|
|
22
|
+
* init: a malformed mark throws naming the field while `SITE_NAME` /
|
|
23
|
+
* `SITE_BASE_URL` stay readable by the routes that need only them. A mark
|
|
24
|
+
* whose dark source differs renders through the `Image` primitive's
|
|
25
|
+
* `darkSrc` swap, so the dark mark is not dropped.
|
|
26
|
+
*/
|
|
27
|
+
export function siteLogo(): ReactElement {
|
|
28
|
+
if (SITE_LOGOMARK === undefined) {
|
|
29
|
+
return <img src="/logomark.svg" alt={SITE_NAME} width={28} height={28} className="block" />;
|
|
30
|
+
}
|
|
31
|
+
const { light, dark, alt } = BrandLogomarkSchema.parse(SITE_LOGOMARK);
|
|
32
|
+
if (light === dark) {
|
|
33
|
+
return <img src={light} alt={alt} width={28} height={28} className="block" />;
|
|
34
|
+
}
|
|
35
|
+
return <Image src={light} darkSrc={dark} alt={alt} width={28} height={28} />;
|
|
36
|
+
}
|
|
@@ -15,21 +15,26 @@
|
|
|
15
15
|
* The translation itself lives in `@working-theory/theme`
|
|
16
16
|
* (`siteThemeToBrandTokens`) — the token vocabulary is that package's own.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* The brand triad (#6820) — `brand.colors` and `brand.fonts`, a sibling of
|
|
19
|
+
* `theme:` that applying a design never rewrites — is read in the same parse
|
|
20
|
+
* and layered over the theme by `siteBrandToBrandTokens`.
|
|
21
|
+
*
|
|
22
|
+
* Returns `undefined` when the site declares no theme and no triad, so the
|
|
23
|
+
* layout passes no `brand` at all and the resolver emits byte-for-byte the
|
|
24
|
+
* CSS it emitted before this wiring existed.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import { readFileSync } from "node:fs";
|
|
24
28
|
import { join } from "node:path";
|
|
25
29
|
|
|
26
|
-
import { SiteThemeSchema } from "@working-theory/site-config/schema";
|
|
30
|
+
import { BrandSchema, SiteThemeSchema } from "@working-theory/site-config/schema";
|
|
27
31
|
import {
|
|
32
|
+
siteBrandToBrandTokens,
|
|
28
33
|
siteThemeShapeCss,
|
|
29
|
-
siteThemeToBrandTokens,
|
|
30
34
|
type BrandTokenOverrides,
|
|
31
35
|
} from "@working-theory/theme";
|
|
32
36
|
import { parse as parseYaml } from "yaml";
|
|
37
|
+
import { z } from "zod";
|
|
33
38
|
|
|
34
39
|
import { resolveContentRoot } from "./site-brand";
|
|
35
40
|
|
|
@@ -38,40 +43,55 @@ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
|
38
43
|
const SOURCE = "content/_site.mdx theme";
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
44
|
-
|
|
46
|
+
* `brand:` as this loader validates it: every key checked by name, but the
|
|
47
|
+
* `logomark` left to `siteLogo()` (`site-brand.tsx`), which parses it on the
|
|
48
|
+
* render that uses it — one throw site per field.
|
|
49
|
+
*/
|
|
50
|
+
const BrandTriadSchema = BrandSchema.extend({
|
|
51
|
+
logomark: z.unknown().optional(),
|
|
52
|
+
}).strict();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The `theme:` and `brand:` values as they appear in the frontmatter, each
|
|
56
|
+
* `undefined` when `_site.mdx` is absent, unreadable, or does not declare it.
|
|
57
|
+
* A site with no readable config has no theme to apply, and the default
|
|
58
|
+
* palette is the correct result — the same fallback `site-brand.tsx` takes.
|
|
45
59
|
*
|
|
46
|
-
* "
|
|
47
|
-
* YAML parses to `null`. That is the shape the most natural edit
|
|
48
|
-
* commenting out the block's children leaves the key dangling —
|
|
49
|
-
* exactly what an absent key means, so it takes the same path
|
|
50
|
-
* reaching the schema and throwing out of a Server Component.
|
|
60
|
+
* "Does not declare it" covers a bare `theme:` key with nothing under it,
|
|
61
|
+
* which YAML parses to `null`. That is the shape the most natural edit
|
|
62
|
+
* produces — commenting out the block's children leaves the key dangling —
|
|
63
|
+
* and it means exactly what an absent key means, so it takes the same path
|
|
64
|
+
* rather than reaching the schema and throwing out of a Server Component.
|
|
51
65
|
*/
|
|
52
|
-
function
|
|
66
|
+
function readSiteBlocks(): { theme: unknown; brand: unknown } {
|
|
53
67
|
try {
|
|
54
68
|
const raw = readFileSync(join(resolveContentRoot(), "_site.mdx"), "utf8");
|
|
55
69
|
const match = FRONTMATTER_RE.exec(raw);
|
|
56
|
-
if (!match) return undefined;
|
|
70
|
+
if (!match) return { theme: undefined, brand: undefined };
|
|
57
71
|
|
|
58
72
|
const parsed: unknown = parseYaml(match[1] ?? "");
|
|
59
|
-
if (!parsed || typeof parsed !== "object")
|
|
73
|
+
if (!parsed || typeof parsed !== "object") {
|
|
74
|
+
return { theme: undefined, brand: undefined };
|
|
75
|
+
}
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
const { theme, brand } = parsed as Record<string, unknown>;
|
|
78
|
+
return { theme: theme ?? undefined, brand: brand ?? undefined };
|
|
62
79
|
} catch {
|
|
63
|
-
return undefined;
|
|
80
|
+
return { theme: undefined, brand: undefined };
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
export function loadSiteBrandTokens(): BrandTokenOverrides | undefined {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
85
|
+
const blocks = readSiteBlocks();
|
|
86
|
+
if (blocks.theme === undefined && blocks.brand === undefined) return undefined;
|
|
70
87
|
|
|
71
88
|
// Validation failures and unsupported colours BOTH throw, deliberately: a
|
|
72
89
|
// customer who set a colour and saw no change would have been lied to.
|
|
73
|
-
const theme =
|
|
74
|
-
|
|
90
|
+
const theme =
|
|
91
|
+
blocks.theme === undefined ? undefined : SiteThemeSchema.parse(blocks.theme);
|
|
92
|
+
const brand =
|
|
93
|
+
blocks.brand === undefined ? undefined : BrandTriadSchema.parse(blocks.brand);
|
|
94
|
+
return siteBrandToBrandTokens(theme, brand, SOURCE);
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
/**
|
|
@@ -79,7 +99,7 @@ export function loadSiteBrandTokens(): BrandTokenOverrides | undefined {
|
|
|
79
99
|
* override block for `:root`. "" when the site sets neither.
|
|
80
100
|
*/
|
|
81
101
|
export function loadSiteShapeCss(): string {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
return siteThemeShapeCss(SiteThemeSchema.parse(
|
|
102
|
+
const { theme } = readSiteBlocks();
|
|
103
|
+
if (theme === undefined) return "";
|
|
104
|
+
return siteThemeShapeCss(SiteThemeSchema.parse(theme));
|
|
85
105
|
}
|