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
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design package (#6812, spec docs/specs/6812-the-design-package-design-md-spec.md
|
|
3
|
+
* D1/D2/D3/D5): a design is read from ONE file, `<dir>/DESIGN.md`, whose
|
|
4
|
+
* frontmatter is its whole authored schema.
|
|
5
|
+
*
|
|
6
|
+
* The dials decision: a design's authored data is its DESIGN.md frontmatter alone
|
|
7
|
+
* (Sam Henry, 2026-09-26, rel-023 plan §8). There is no separate dial file;
|
|
8
|
+
* the Markdown body is generated from the frontmatter in marked regions
|
|
9
|
+
* (`scripts/build-design-md.mjs`).
|
|
10
|
+
*
|
|
11
|
+
* `readDesignPackage` parses the frontmatter with a dependency-free subset of
|
|
12
|
+
* YAML 1.2 (D5) — `create-ailk` ships exactly one runtime dependency (`tar`,
|
|
13
|
+
* § 1.5), and this reader runs at scaffold time, so it cannot import the
|
|
14
|
+
* `yaml` package used elsewhere in this repo's build scripts. The subset
|
|
15
|
+
* never accepts an input the real YAML 1.2 core schema would read
|
|
16
|
+
* differently: anything the subset does not explicitly cover is refused
|
|
17
|
+
* rather than guessed at.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { TemplateLayoutError, validateTemplateLayout, } from "./site-chrome.js";
|
|
22
|
+
// ── Public types ─────────────────────────────────────────────────────────────
|
|
23
|
+
/** The closed colour key set (D3) — 13 required, 5 optional dark fallbacks. */
|
|
24
|
+
export const DESIGN_COLOR_KEYS_REQUIRED = [
|
|
25
|
+
"bg",
|
|
26
|
+
"fg",
|
|
27
|
+
"surface",
|
|
28
|
+
"surface-secondary",
|
|
29
|
+
"border",
|
|
30
|
+
"text-muted",
|
|
31
|
+
"brand",
|
|
32
|
+
"fg-on-brand",
|
|
33
|
+
"accent",
|
|
34
|
+
"bg-dark",
|
|
35
|
+
"fg-dark",
|
|
36
|
+
"border-dark",
|
|
37
|
+
"brand-dark",
|
|
38
|
+
];
|
|
39
|
+
export const DESIGN_COLOR_KEYS_OPTIONAL = [
|
|
40
|
+
"surface-dark",
|
|
41
|
+
"surface-secondary-dark",
|
|
42
|
+
"text-muted-dark",
|
|
43
|
+
"fg-on-brand-dark",
|
|
44
|
+
"accent-dark",
|
|
45
|
+
];
|
|
46
|
+
/** Duplicated from `SiteThemeSchema`'s colour regex (D3) — proven equal by test. */
|
|
47
|
+
export const DESIGN_COLOR_REGEX = /^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\(.+\)|rgba\(.+\)|hsl\(.+\)|hsla\(.+\)|var\(--[^)]+\))$/;
|
|
48
|
+
/** Duplicated from `SiteThemeSchema`'s enums (D3) — proven equal by test. */
|
|
49
|
+
export const DESIGN_CORNERS_VALUES = ["rounded", "square"];
|
|
50
|
+
export const DESIGN_EYEBROW_CASE_VALUES = ["upper", "sentence"];
|
|
51
|
+
export const DESIGN_PLACEHOLDERS_VALUES = ["outline", "fill"];
|
|
52
|
+
export const DESIGN_CHECKMARKS_VALUES = ["circle", "plain"];
|
|
53
|
+
/** The closed copy-density enum (spec 6892 D4). */
|
|
54
|
+
export const DESIGN_COPY_DENSITY_VALUES = ["standard", "sparse"];
|
|
55
|
+
/**
|
|
56
|
+
* Thrown by `readDesignPackage` on any malformed package (D2) — never thrown
|
|
57
|
+
* after a write; every refusal happens before any I/O beyond reading
|
|
58
|
+
* `DESIGN.md` itself.
|
|
59
|
+
*/
|
|
60
|
+
export class DesignPackageError extends Error {
|
|
61
|
+
file;
|
|
62
|
+
field;
|
|
63
|
+
constructor(file, field, reason) {
|
|
64
|
+
super(`${file}: ${field} — ${reason}`);
|
|
65
|
+
this.name = "DesignPackageError";
|
|
66
|
+
this.file = file;
|
|
67
|
+
this.field = field;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function refuse(file, field, reason) {
|
|
71
|
+
throw new DesignPackageError(file, field, reason);
|
|
72
|
+
}
|
|
73
|
+
function refuseFrontmatter(file, lineNo, reason) {
|
|
74
|
+
refuse(file, "frontmatter", `line ${lineNo}: ${reason}`);
|
|
75
|
+
}
|
|
76
|
+
/** Split frontmatter text into significant lines, refusing tabs as we go. */
|
|
77
|
+
function toSignificantLines(text, file) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
const rawLines = text.split("\n");
|
|
80
|
+
for (let i = 0; i < rawLines.length; i += 1) {
|
|
81
|
+
const lineNo = i + 1;
|
|
82
|
+
const raw = rawLines[i] ?? "";
|
|
83
|
+
if (raw.includes("\t")) {
|
|
84
|
+
refuseFrontmatter(file, lineNo, "tabs are not allowed");
|
|
85
|
+
}
|
|
86
|
+
const trimmed = raw.replace(/\s+$/, "");
|
|
87
|
+
if (trimmed === "")
|
|
88
|
+
continue;
|
|
89
|
+
const indent = /^ */.exec(trimmed)[0].length;
|
|
90
|
+
const content = trimmed.slice(indent);
|
|
91
|
+
if (content.startsWith("#"))
|
|
92
|
+
continue;
|
|
93
|
+
lines.push({ lineNo, indent, content });
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
const KEY_RE = /^([A-Za-z0-9_-]+):(.*)$/;
|
|
98
|
+
function parseDoubleQuoted(raw, lineNo, file) {
|
|
99
|
+
const m = /^"((?:\\.|[^"\\])*)"$/.exec(raw);
|
|
100
|
+
if (!m) {
|
|
101
|
+
refuseFrontmatter(file, lineNo, "unterminated or malformed double-quoted string");
|
|
102
|
+
}
|
|
103
|
+
const body = m[1] ?? "";
|
|
104
|
+
let result = "";
|
|
105
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
106
|
+
const ch = body[i];
|
|
107
|
+
if (ch === "\\") {
|
|
108
|
+
const next = body[i + 1];
|
|
109
|
+
if (next === '"' || next === "\\") {
|
|
110
|
+
result += next;
|
|
111
|
+
i += 1;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
refuseFrontmatter(file, lineNo, `unsupported escape "\\${next ?? ""}"`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
result += ch;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function parseSingleQuoted(raw, lineNo, file) {
|
|
124
|
+
const m = /^'((?:''|[^'])*)'$/.exec(raw);
|
|
125
|
+
if (!m) {
|
|
126
|
+
refuseFrontmatter(file, lineNo, "unterminated or malformed single-quoted string");
|
|
127
|
+
}
|
|
128
|
+
return (m[1] ?? "").replace(/''/g, "'");
|
|
129
|
+
}
|
|
130
|
+
const RESERVED_BOOLEAN_CASE_RE = /^(?:True|TRUE|False|FALSE)$/;
|
|
131
|
+
const RESERVED_NULL_RE = /^(?:null|Null|NULL|~)$/;
|
|
132
|
+
const RESERVED_INF_NAN_RE = /^[+-]?\.(?:inf|nan)$/i;
|
|
133
|
+
const HEX_OCTAL_RE = /^[+-]?0[xXoO][0-9a-fA-F]+$/;
|
|
134
|
+
const INT_RE = /^-?(?:0|[1-9][0-9]*)$/;
|
|
135
|
+
const FLOAT_RE = /^-?(?:0|[1-9][0-9]*)\.[0-9]+$/;
|
|
136
|
+
const EXPONENT_RE = /^-?[0-9]+(?:\.[0-9]+)?[eE][+-]?[0-9]+$/;
|
|
137
|
+
const LEADING_ZERO_INT_RE = /^-?0[0-9]+$/;
|
|
138
|
+
const LEADING_DOT_FLOAT_RE = /^[+-]?\.[0-9]+$/;
|
|
139
|
+
function parsePlainScalar(raw, lineNo, file) {
|
|
140
|
+
if (raw.includes(": ") || raw.includes(" #") || raw.startsWith("#")) {
|
|
141
|
+
refuseFrontmatter(file, lineNo, 'a plain scalar must not contain ": " or " #"');
|
|
142
|
+
}
|
|
143
|
+
if (raw === "true")
|
|
144
|
+
return true;
|
|
145
|
+
if (raw === "false")
|
|
146
|
+
return false;
|
|
147
|
+
if (RESERVED_BOOLEAN_CASE_RE.test(raw)) {
|
|
148
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is a reserved boolean literal — use lowercase true/false, or quote it`);
|
|
149
|
+
}
|
|
150
|
+
if (RESERVED_NULL_RE.test(raw)) {
|
|
151
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is a reserved null literal — quote it if a string is meant`);
|
|
152
|
+
}
|
|
153
|
+
if (RESERVED_INF_NAN_RE.test(raw)) {
|
|
154
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is a reserved inf/nan literal — quote it if a string is meant`);
|
|
155
|
+
}
|
|
156
|
+
if (HEX_OCTAL_RE.test(raw)) {
|
|
157
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is a hex/octal literal, not supported — quote it if a string is meant`);
|
|
158
|
+
}
|
|
159
|
+
if (raw.startsWith("+")) {
|
|
160
|
+
refuseFrontmatter(file, lineNo, `"${raw}" has a leading "+", not supported — quote it if a string is meant`);
|
|
161
|
+
}
|
|
162
|
+
if (INT_RE.test(raw))
|
|
163
|
+
return Number(raw);
|
|
164
|
+
if (FLOAT_RE.test(raw))
|
|
165
|
+
return Number(raw);
|
|
166
|
+
if (EXPONENT_RE.test(raw)) {
|
|
167
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is an exponent-form number, not supported — quote it if a string is meant`);
|
|
168
|
+
}
|
|
169
|
+
if (LEADING_ZERO_INT_RE.test(raw)) {
|
|
170
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is a leading-zero integer, not supported — quote it if a string is meant`);
|
|
171
|
+
}
|
|
172
|
+
if (LEADING_DOT_FLOAT_RE.test(raw)) {
|
|
173
|
+
refuseFrontmatter(file, lineNo, `"${raw}" is a bare leading-dot float, not supported — quote it, or add a leading 0 before the "."`);
|
|
174
|
+
}
|
|
175
|
+
return raw;
|
|
176
|
+
}
|
|
177
|
+
function parseScalar(raw, lineNo, file) {
|
|
178
|
+
if (raw.startsWith('"'))
|
|
179
|
+
return parseDoubleQuoted(raw, lineNo, file);
|
|
180
|
+
if (raw.startsWith("'"))
|
|
181
|
+
return parseSingleQuoted(raw, lineNo, file);
|
|
182
|
+
if (/^[[{&*!|>]/.test(raw)) {
|
|
183
|
+
refuseFrontmatter(file, lineNo, "flow collections, anchors, aliases, tags and block scalars are not supported");
|
|
184
|
+
}
|
|
185
|
+
return parsePlainScalar(raw, lineNo, file);
|
|
186
|
+
}
|
|
187
|
+
/** Parse a block mapping starting at `pos.i`, whose keys sit at `indent`. */
|
|
188
|
+
function parseMapping(lines, pos, indent, file) {
|
|
189
|
+
const result = {};
|
|
190
|
+
while (pos.i < lines.length && lines[pos.i].indent === indent) {
|
|
191
|
+
const line = lines[pos.i];
|
|
192
|
+
if (line.content === "-" || line.content.startsWith("- ")) {
|
|
193
|
+
refuseFrontmatter(file, line.lineNo, "sequences are not supported");
|
|
194
|
+
}
|
|
195
|
+
const m = KEY_RE.exec(line.content);
|
|
196
|
+
if (!m) {
|
|
197
|
+
refuseFrontmatter(file, line.lineNo, `expected "key: value", got "${line.content}"`);
|
|
198
|
+
}
|
|
199
|
+
const key = m[1];
|
|
200
|
+
const rawRest = m[2];
|
|
201
|
+
if (Object.hasOwn(result, key)) {
|
|
202
|
+
refuseFrontmatter(file, line.lineNo, `duplicate key "${key}"`);
|
|
203
|
+
}
|
|
204
|
+
pos.i += 1;
|
|
205
|
+
if (rawRest === "") {
|
|
206
|
+
const next = lines[pos.i];
|
|
207
|
+
if (next !== undefined && next.indent > indent) {
|
|
208
|
+
if (next.indent !== indent + 2) {
|
|
209
|
+
refuseFrontmatter(file, next.lineNo, `bad indentation — expected ${indent + 2} spaces, got ${next.indent}`);
|
|
210
|
+
}
|
|
211
|
+
result[key] = parseMapping(lines, pos, next.indent, file);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
refuseFrontmatter(file, line.lineNo, `"${key}" has an empty value`);
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!rawRest.startsWith(" ")) {
|
|
219
|
+
refuseFrontmatter(file, line.lineNo, 'expected a single space after ":"');
|
|
220
|
+
}
|
|
221
|
+
const rest = rawRest.slice(1);
|
|
222
|
+
if (rest === "" || rest.startsWith(" ")) {
|
|
223
|
+
refuseFrontmatter(file, line.lineNo, `"${key}" has an empty value`);
|
|
224
|
+
}
|
|
225
|
+
result[key] = parseScalar(rest, line.lineNo, file);
|
|
226
|
+
const next = lines[pos.i];
|
|
227
|
+
if (next !== undefined && next.indent > indent) {
|
|
228
|
+
refuseFrontmatter(file, next.lineNo, `unexpected indentation after "${key}"'s scalar value`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
/** Parse `<dir>/DESIGN.md`'s frontmatter into a plain object tree (D5). */
|
|
234
|
+
function parseFrontmatter(text, file) {
|
|
235
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n[\s\S]*)?$/.exec(text);
|
|
236
|
+
if (!m) {
|
|
237
|
+
refuse(file, "frontmatter", "no closing --- found");
|
|
238
|
+
}
|
|
239
|
+
const lines = toSignificantLines(m[1] ?? "", file);
|
|
240
|
+
const pos = { i: 0 };
|
|
241
|
+
const result = parseMapping(lines, pos, 0, file);
|
|
242
|
+
if (pos.i < lines.length) {
|
|
243
|
+
const stray = lines[pos.i];
|
|
244
|
+
refuseFrontmatter(file, stray.lineNo, "unexpected indentation at the top level");
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
// ── Group validators (D2/D3) ────────────────────────────────────────────────
|
|
249
|
+
function requireString(value, file, field) {
|
|
250
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
251
|
+
refuse(file, field, "must be a non-empty string");
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
function requireObject(value, file, field) {
|
|
256
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
257
|
+
refuse(file, field, "must be an object");
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
function validateColors(raw, file) {
|
|
262
|
+
const obj = requireObject(raw, file, "colors");
|
|
263
|
+
const allowed = new Set([...DESIGN_COLOR_KEYS_REQUIRED, ...DESIGN_COLOR_KEYS_OPTIONAL]);
|
|
264
|
+
for (const key of Object.keys(obj)) {
|
|
265
|
+
if (!allowed.has(key)) {
|
|
266
|
+
refuse(file, `colors.${key}`, "is not a recognized colour key");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const key of DESIGN_COLOR_KEYS_REQUIRED) {
|
|
270
|
+
if (!(key in obj)) {
|
|
271
|
+
refuse(file, `colors.${key}`, "is required");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const result = {};
|
|
275
|
+
for (const key of Object.keys(obj)) {
|
|
276
|
+
const value = obj[key];
|
|
277
|
+
if (typeof value !== "string" || !DESIGN_COLOR_REGEX.test(value)) {
|
|
278
|
+
refuse(file, `colors.${key}`, "must be a hex/rgb()/rgba()/hsl()/hsla()/var(--…) colour string");
|
|
279
|
+
}
|
|
280
|
+
result[key] = value;
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
const TYPOGRAPHY_ROLE_KEYS = ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"];
|
|
285
|
+
function validateTypographyRole(raw, file, role) {
|
|
286
|
+
const obj = requireObject(raw, file, `typography.${role}`);
|
|
287
|
+
for (const key of Object.keys(obj)) {
|
|
288
|
+
if (!TYPOGRAPHY_ROLE_KEYS.includes(key)) {
|
|
289
|
+
refuse(file, `typography.${role}.${key}`, "is not a recognized typography field");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!("fontSize" in obj)) {
|
|
293
|
+
refuse(file, `typography.${role}.fontSize`, "is required");
|
|
294
|
+
}
|
|
295
|
+
const result = { fontSize: "" };
|
|
296
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
297
|
+
const field = `typography.${role}.${key}`;
|
|
298
|
+
if (key === "fontFamily" || key === "fontSize" || key === "letterSpacing") {
|
|
299
|
+
if (typeof value !== "string")
|
|
300
|
+
refuse(file, field, "must be a string");
|
|
301
|
+
if (key === "fontFamily")
|
|
302
|
+
result.fontFamily = value;
|
|
303
|
+
else if (key === "fontSize")
|
|
304
|
+
result.fontSize = value;
|
|
305
|
+
else
|
|
306
|
+
result.letterSpacing = value;
|
|
307
|
+
}
|
|
308
|
+
else if (key === "fontWeight" || key === "lineHeight") {
|
|
309
|
+
if (typeof value !== "number")
|
|
310
|
+
refuse(file, field, "must be a number");
|
|
311
|
+
if (key === "fontWeight")
|
|
312
|
+
result.fontWeight = value;
|
|
313
|
+
else
|
|
314
|
+
result.lineHeight = value;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
function validateTypography(raw, file) {
|
|
320
|
+
const obj = requireObject(raw, file, "typography");
|
|
321
|
+
const result = {};
|
|
322
|
+
for (const [role, value] of Object.entries(obj)) {
|
|
323
|
+
result[role] = validateTypographyRole(value, file, role);
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
function validateStringMap(raw, file, field) {
|
|
328
|
+
const obj = requireObject(raw, file, field);
|
|
329
|
+
const result = {};
|
|
330
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
331
|
+
if (typeof value !== "string") {
|
|
332
|
+
refuse(file, `${field}.${key}`, "must be a string");
|
|
333
|
+
}
|
|
334
|
+
result[key] = value;
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
const LAYOUT_KEYS = ["nav", "footer", "announcementBanner", "home"];
|
|
339
|
+
function validateLayout(raw, file) {
|
|
340
|
+
const obj = requireObject(raw, file, "layout");
|
|
341
|
+
for (const key of Object.keys(obj)) {
|
|
342
|
+
if (!LAYOUT_KEYS.includes(key)) {
|
|
343
|
+
refuse(file, `layout.${key}`, "is not a recognized layout key");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const key of LAYOUT_KEYS) {
|
|
347
|
+
if (!(key in obj)) {
|
|
348
|
+
refuse(file, `layout.${key}`, "is required");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
return validateTemplateLayout(obj, "design package");
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof TemplateLayoutError) {
|
|
356
|
+
const match = /: (layout\.[A-Za-z]+) must /.exec(error.message);
|
|
357
|
+
refuse(file, match?.[1] ?? "layout", error.message);
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const TREATMENTS_ENUMS = {
|
|
363
|
+
corners: DESIGN_CORNERS_VALUES,
|
|
364
|
+
eyebrowCase: DESIGN_EYEBROW_CASE_VALUES,
|
|
365
|
+
placeholders: DESIGN_PLACEHOLDERS_VALUES,
|
|
366
|
+
checkmarks: DESIGN_CHECKMARKS_VALUES,
|
|
367
|
+
};
|
|
368
|
+
function validateTreatments(raw, file) {
|
|
369
|
+
const obj = requireObject(raw, file, "treatments");
|
|
370
|
+
const result = {};
|
|
371
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
372
|
+
const values = TREATMENTS_ENUMS[key];
|
|
373
|
+
if (values === undefined) {
|
|
374
|
+
refuse(file, `treatments.${key}`, "is not a recognized treatment");
|
|
375
|
+
}
|
|
376
|
+
if (typeof value !== "string" || !values.includes(value)) {
|
|
377
|
+
refuse(file, `treatments.${key}`, `must be one of ${values.join(", ")}`);
|
|
378
|
+
}
|
|
379
|
+
result[key] = value;
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function validateSections(raw, file) {
|
|
384
|
+
const obj = requireObject(raw, file, "sections");
|
|
385
|
+
const result = {};
|
|
386
|
+
for (const [pageType, value] of Object.entries(obj)) {
|
|
387
|
+
result[pageType] = requireObject(value, file, `sections.${pageType}`);
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
function validateCopy(raw, file) {
|
|
392
|
+
const obj = requireObject(raw, file, "copy");
|
|
393
|
+
for (const key of Object.keys(obj)) {
|
|
394
|
+
if (key !== "density") {
|
|
395
|
+
refuse(file, `copy.${key}`, "is not a recognized copy key");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const density = obj["density"];
|
|
399
|
+
if (typeof density !== "string" ||
|
|
400
|
+
!DESIGN_COPY_DENSITY_VALUES.includes(density)) {
|
|
401
|
+
refuse(file, "copy.density", `must be one of ${DESIGN_COPY_DENSITY_VALUES.join(", ")}`);
|
|
402
|
+
}
|
|
403
|
+
return { density: density };
|
|
404
|
+
}
|
|
405
|
+
const TOP_LEVEL_KEYS = [
|
|
406
|
+
"name",
|
|
407
|
+
"description",
|
|
408
|
+
"colors",
|
|
409
|
+
"typography",
|
|
410
|
+
"rounded",
|
|
411
|
+
"spacing",
|
|
412
|
+
"layout",
|
|
413
|
+
"treatments",
|
|
414
|
+
"sections",
|
|
415
|
+
"copy",
|
|
416
|
+
];
|
|
417
|
+
const REQUIRED_TOP_LEVEL_KEYS = [
|
|
418
|
+
"name",
|
|
419
|
+
"description",
|
|
420
|
+
"colors",
|
|
421
|
+
"typography",
|
|
422
|
+
"rounded",
|
|
423
|
+
"spacing",
|
|
424
|
+
"layout",
|
|
425
|
+
];
|
|
426
|
+
function validatePackage(raw, file) {
|
|
427
|
+
for (const key of Object.keys(raw)) {
|
|
428
|
+
if (!TOP_LEVEL_KEYS.includes(key)) {
|
|
429
|
+
refuse(file, key, "is not a recognized design-package key");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const key of REQUIRED_TOP_LEVEL_KEYS) {
|
|
433
|
+
if (!(key in raw)) {
|
|
434
|
+
refuse(file, key, "is required");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const pkg = {
|
|
438
|
+
name: requireString(raw.name, file, "name"),
|
|
439
|
+
description: requireString(raw.description, file, "description"),
|
|
440
|
+
colors: validateColors(raw.colors, file),
|
|
441
|
+
typography: validateTypography(raw.typography, file),
|
|
442
|
+
rounded: validateStringMap(raw.rounded, file, "rounded"),
|
|
443
|
+
spacing: validateStringMap(raw.spacing, file, "spacing"),
|
|
444
|
+
layout: validateLayout(raw.layout, file),
|
|
445
|
+
sections: "sections" in raw ? validateSections(raw.sections, file) : {},
|
|
446
|
+
copy: "copy" in raw ? validateCopy(raw.copy, file) : { density: "standard" },
|
|
447
|
+
};
|
|
448
|
+
if ("treatments" in raw) {
|
|
449
|
+
pkg.treatments = validateTreatments(raw.treatments, file);
|
|
450
|
+
}
|
|
451
|
+
return pkg;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Read and validate `<dir>/DESIGN.md`'s frontmatter into a typed
|
|
455
|
+
* `DesignPackage` (D1/D2). Refuses — naming the file and the field — on any
|
|
456
|
+
* malformed package: a missing file, an unknown key, a wrong type, an
|
|
457
|
+
* unknown layout value. Nothing is written; this function has no side
|
|
458
|
+
* effects beyond the one read.
|
|
459
|
+
*/
|
|
460
|
+
export function readDesignPackage(dir) {
|
|
461
|
+
const file = join(dir, "DESIGN.md");
|
|
462
|
+
if (!existsSync(file)) {
|
|
463
|
+
refuse(file, "DESIGN.md", "file does not exist");
|
|
464
|
+
}
|
|
465
|
+
const text = readFileSync(file, "utf8");
|
|
466
|
+
const raw = parseFrontmatter(text, file);
|
|
467
|
+
return validatePackage(raw, file);
|
|
468
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type DesignSections } from "./design-package.js";
|
|
2
|
+
export interface SectionLookCatalogEntry {
|
|
3
|
+
/** The look group, in registry order (D2). */
|
|
4
|
+
readonly look: readonly string[];
|
|
5
|
+
/** The type's `data` JSON Schema `properties` (the catalog's `schema.properties`). */
|
|
6
|
+
readonly properties: Readonly<Record<string, unknown>>;
|
|
7
|
+
}
|
|
8
|
+
export type SectionLookCatalog = Readonly<Record<string, SectionLookCatalogEntry>>;
|
|
9
|
+
/** Read `<pkgRoot>/component-catalog.json` into the look catalog. Throws naming the file on a missing `look` or `schema`. */
|
|
10
|
+
export declare function readSectionLookCatalog(catalogPath: string): SectionLookCatalog;
|
|
11
|
+
/**
|
|
12
|
+
* Every `pageType` named by a row of a shipped `TEMPLATE-MANIFEST.json` under
|
|
13
|
+
* `<siteTemplatesDir>/*`, `<siteTemplatesDir>/shapes/*`, and — a treeless
|
|
14
|
+
* alias's own GENERATED record (spec 6892 D1/D5), which has no directory of
|
|
15
|
+
* its own — every `<siteTemplatesDir>/aliases/*.TEMPLATE-MANIFEST.json` file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function shippedPageTypes(siteTemplatesDir: string): ReadonlySet<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Refuse, with a `DesignPackageError` whose `field` names the page type, the
|
|
20
|
+
* section type and the field, the first preset (key order) that names a page
|
|
21
|
+
* type no shipped roster places, an unregistered section type, a field
|
|
22
|
+
* outside the type's look group, or a value outside that field's schema
|
|
23
|
+
* (D3's table, R1–R5b). A valid block returns without effect. Pure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateDesignPresets(sections: DesignSections, opts: {
|
|
26
|
+
catalog: SectionLookCatalog;
|
|
27
|
+
pageTypes: ReadonlySet<string>;
|
|
28
|
+
file: string;
|
|
29
|
+
}): void;
|
|
30
|
+
/** One `data` entry: its key and the `[valueStart, valueEnd)` span of its value. */
|
|
31
|
+
export interface LocatedDataEntry {
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly valueStart: number;
|
|
34
|
+
readonly valueEnd: number;
|
|
35
|
+
}
|
|
36
|
+
/** One `sections[]` element, located in the page text. */
|
|
37
|
+
export interface LocatedSection {
|
|
38
|
+
readonly type: string;
|
|
39
|
+
readonly form: "flow" | "block";
|
|
40
|
+
/** Flow: the index of `data`'s `{`. Block: the end of the `data:` line (before its newline). */
|
|
41
|
+
readonly dataOpen: number;
|
|
42
|
+
/** Flow: the index of `data`'s `}`. Block: `dataOpen`. */
|
|
43
|
+
readonly dataClose: number;
|
|
44
|
+
/** Block only: the column `data`'s entries sit at. */
|
|
45
|
+
readonly entryColumn: number;
|
|
46
|
+
readonly entries: readonly LocatedDataEntry[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Locate a page's `sections[]` in its frontmatter — the prettier-formatted
|
|
50
|
+
* flow form (`sections:\n [ { "id": …, "type": …, "data": { … } }, … ]`)
|
|
51
|
+
* or the block form (` - id: …\n type: …\n data:\n key: value`).
|
|
52
|
+
* Undefined when the page has no frontmatter or no column-0 `sections:` key.
|
|
53
|
+
* Any other shape throws `TemplateError` naming the element (D5).
|
|
54
|
+
*/
|
|
55
|
+
export declare function locatePageSections(text: string, file: string): LocatedSection[] | undefined;
|
|
56
|
+
export interface PresetPage {
|
|
57
|
+
readonly path: string;
|
|
58
|
+
readonly text: string;
|
|
59
|
+
}
|
|
60
|
+
export interface PresetApplication {
|
|
61
|
+
readonly path: string;
|
|
62
|
+
readonly pageType: string;
|
|
63
|
+
readonly index: number;
|
|
64
|
+
readonly sectionType: string;
|
|
65
|
+
readonly field: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Set one look field on section `index` of a page, splicing only its value
|
|
69
|
+
* span (D5): an existing value is replaced; an absent field is inserted as
|
|
70
|
+
* `data`'s first entry. Throws `TemplateError` when the page cannot be located.
|
|
71
|
+
*/
|
|
72
|
+
export declare function setSectionLookField(text: string, file: string, index: number, field: string, value: unknown): string;
|
|
73
|
+
/**
|
|
74
|
+
* Write a validated preset block's look fields onto the sections a page
|
|
75
|
+
* already places, for the pages whose `type:` is in `opts.pageTypes` (the
|
|
76
|
+
* roster's placed set) and has a preset. Never adds, removes, reorders,
|
|
77
|
+
* retypes or re-ids a section; never changes a byte outside the value it
|
|
78
|
+
* sets. Returns only the pages whose text changed. Pure; trusts its input to
|
|
79
|
+
* have passed `validateDesignPresets`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyDesignPresets(pages: readonly PresetPage[], sections: DesignSections, opts: {
|
|
82
|
+
pageTypes: ReadonlySet<string>;
|
|
83
|
+
}): {
|
|
84
|
+
pages: PresetPage[];
|
|
85
|
+
applied: PresetApplication[];
|
|
86
|
+
};
|