@wildo-ai/saas-website 1.1.1
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/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts +324 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-bridge.js +57 -0
- package/dist/esm/astro/blog-post-bridge.js.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts +129 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.js +139 -0
- package/dist/esm/astro/blog-post-head.renderer.js.map +1 -0
- package/dist/esm/astro/bridge-runtime.d.ts +224 -0
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -0
- package/dist/esm/astro/bridge-runtime.js +30 -0
- package/dist/esm/astro/bridge-runtime.js.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts +65 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.js +101 -0
- package/dist/esm/astro/collect-expected-label-keys.js.map +1 -0
- package/dist/esm/astro/headers-renderer.d.ts +32 -0
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -0
- package/dist/esm/astro/headers-renderer.js +64 -0
- package/dist/esm/astro/headers-renderer.js.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts +91 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.js +23 -0
- package/dist/esm/astro/i18n-routing.helper.js.map +1 -0
- package/dist/esm/astro/internal/html-escape.d.ts +31 -0
- package/dist/esm/astro/internal/html-escape.d.ts.map +1 -0
- package/dist/esm/astro/internal/html-escape.js +40 -0
- package/dist/esm/astro/internal/html-escape.js.map +1 -0
- package/dist/esm/astro/label-pack-loader.d.ts +134 -0
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -0
- package/dist/esm/astro/label-pack-loader.js +298 -0
- package/dist/esm/astro/label-pack-loader.js.map +1 -0
- package/dist/esm/astro/llms-txt-server.d.ts +28 -0
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -0
- package/dist/esm/astro/llms-txt-server.js +33 -0
- package/dist/esm/astro/llms-txt-server.js.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts +63 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.js +73 -0
- package/dist/esm/astro/pick-labels-for-page.js.map +1 -0
- package/dist/esm/astro/robots-renderer.d.ts +85 -0
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -0
- package/dist/esm/astro/robots-renderer.js +157 -0
- package/dist/esm/astro/robots-renderer.js.map +1 -0
- package/dist/esm/astro/sitemap-coverage.d.ts +101 -0
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -0
- package/dist/esm/astro/sitemap-coverage.js +67 -0
- package/dist/esm/astro/sitemap-coverage.js.map +1 -0
- package/dist/esm/astro/structured-data.renderer.d.ts +21 -0
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -0
- package/dist/esm/astro/structured-data.renderer.js +142 -0
- package/dist/esm/astro/structured-data.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts +93 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.js +184 -0
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts +112 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.js +60 -0
- package/dist/esm/astro/website-page-runtime.helper.js.map +1 -0
- package/dist/esm/astro/website-site-context.d.ts +126 -0
- package/dist/esm/astro/website-site-context.d.ts.map +1 -0
- package/dist/esm/astro/website-site-context.js +37 -0
- package/dist/esm/astro/website-site-context.js.map +1 -0
- package/dist/esm/astro-island.d.ts +56 -0
- package/dist/esm/astro-island.d.ts.map +1 -0
- package/dist/esm/astro-island.js +61 -0
- package/dist/esm/astro-island.js.map +1 -0
- package/dist/esm/astro.d.ts +151 -0
- package/dist/esm/astro.d.ts.map +1 -0
- package/dist/esm/astro.js +151 -0
- package/dist/esm/astro.js.map +1 -0
- package/dist/esm/companion-exports.d.ts +38 -0
- package/dist/esm/companion-exports.d.ts.map +1 -0
- package/dist/esm/companion-exports.js +38 -0
- package/dist/esm/companion-exports.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.js +68 -0
- package/dist/esm/components/low-level/WebsiteButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts +78 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.js +41 -0
- package/dist/esm/components/low-level/WebsiteHeading.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.js +21 -0
- package/dist/esm/components/low-level/WebsiteImage.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts +90 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js +47 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts +99 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js +68 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts +50 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.js +22 -0
- package/dist/esm/components/low-level/WebsiteLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts +57 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.js +57 -0
- package/dist/esm/components/low-level/WebsiteText.js.map +1 -0
- package/dist/esm/config/define-website-config.d.ts +83 -0
- package/dist/esm/config/define-website-config.d.ts.map +1 -0
- package/dist/esm/config/define-website-config.js +126 -0
- package/dist/esm/config/define-website-config.js.map +1 -0
- package/dist/esm/config/index.d.ts +38 -0
- package/dist/esm/config/index.d.ts.map +1 -0
- package/dist/esm/config/index.js +38 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/config/load-website-config.d.ts +136 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -0
- package/dist/esm/config/load-website-config.js +177 -0
- package/dist/esm/config/load-website-config.js.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts +251 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.js +302 -0
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -0
- package/dist/esm/config-loader.d.ts +35 -0
- package/dist/esm/config-loader.d.ts.map +1 -0
- package/dist/esm/config-loader.js +35 -0
- package/dist/esm/config-loader.js.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +38 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js +77 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +41 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +58 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +81 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +177 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts +40 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.js +8 -0
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +162 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js +22 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts +47 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js +8 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts +13 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.js +22 -0
- package/dist/esm/core/contexts/useWebsitePage.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +19 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts +12 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.js +22 -0
- package/dist/esm/core/contexts/useWebsiteSection.js.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +17 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js +23 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts +70 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.js +51 -0
- package/dist/esm/core/factories/define-website-page-manifest.js.map +1 -0
- package/dist/esm/core/factories/define-website-section.d.ts +101 -0
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-section.js +71 -0
- package/dist/esm/core/factories/define-website-section.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts +23 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js +25 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts +68 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js +103 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts +87 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js +170 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts +91 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.js +26 -0
- package/dist/esm/core/layouts/WebsiteSection.js.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts +65 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.js +14 -0
- package/dist/esm/core/routing/internal-routing.utils.js.map +1 -0
- package/dist/esm/index.d.ts +129 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +129 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mdx/BlogPost.d.ts +193 -0
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -0
- package/dist/esm/mdx/BlogPost.js +161 -0
- package/dist/esm/mdx/BlogPost.js.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts +139 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.js +27 -0
- package/dist/esm/mdx/blog-post-collection.config.js.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts +12 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js +13 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js.map +1 -0
- package/dist/esm/mdx/index.d.ts +22 -0
- package/dist/esm/mdx/index.d.ts.map +1 -0
- package/dist/esm/mdx/index.js +22 -0
- package/dist/esm/mdx/index.js.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts +90 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.js +8 -0
- package/dist/esm/mdx/website-mdx-provider.js.map +1 -0
- package/dist/esm/mdx.d.ts +38 -0
- package/dist/esm/mdx.d.ts.map +1 -0
- package/dist/esm/mdx.js +38 -0
- package/dist/esm/mdx.js.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts +75 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js +85 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +120 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +152 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts +73 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js +76 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts +209 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js +218 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +431 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +438 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts +40 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js +43 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts +41 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js +44 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts +63 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js +64 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +114 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +69 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +100 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +355 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +58 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js +2 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts +144 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.js +100 -0
- package/dist/esm/schemas/validators/label-pack.validator.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +143 -0
- package/src/__tests__/bundle-isolation.test.ts +607 -0
- package/src/astro/__tests__/blog-post-bridge.test.tsx +288 -0
- package/src/astro/__tests__/blog-post-head.renderer.test.ts +249 -0
- package/src/astro/__tests__/bridge-runtime.test.tsx +198 -0
- package/src/astro/__tests__/collect-expected-label-keys.test.ts +69 -0
- package/src/astro/__tests__/headers-renderer.test.ts +143 -0
- package/src/astro/__tests__/i18n-routing.helper.test.ts +116 -0
- package/src/astro/__tests__/label-pack-loader.test.ts +305 -0
- package/src/astro/__tests__/llms-txt-server.test.ts +127 -0
- package/src/astro/__tests__/pick-labels-for-page.test.ts +60 -0
- package/src/astro/__tests__/robots-renderer.test.ts +228 -0
- package/src/astro/__tests__/sitemap-coverage.test.ts +230 -0
- package/src/astro/__tests__/structured-data.renderer.test.ts +172 -0
- package/src/astro/__tests__/website-page-head.renderer.test.ts +424 -0
- package/src/astro/__tests__/website-page-runtime.helper.test.ts +157 -0
- package/src/astro/__tests__/website-site-context.test.ts +121 -0
- package/src/astro/blog-post-bridge.tsx +404 -0
- package/src/astro/blog-post-head.renderer.ts +293 -0
- package/src/astro/bridge-runtime.tsx +270 -0
- package/src/astro/collect-expected-label-keys.ts +113 -0
- package/src/astro/headers-renderer.ts +82 -0
- package/src/astro/i18n-routing.helper.ts +116 -0
- package/src/astro/internal/html-escape.ts +41 -0
- package/src/astro/label-pack-loader.ts +385 -0
- package/src/astro/llms-txt-server.ts +56 -0
- package/src/astro/pick-labels-for-page.ts +85 -0
- package/src/astro/robots-renderer.ts +171 -0
- package/src/astro/sitemap-coverage.ts +186 -0
- package/src/astro/structured-data.renderer.ts +171 -0
- package/src/astro/website-page-head.renderer.ts +279 -0
- package/src/astro/website-page-runtime.helper.ts +188 -0
- package/src/astro/website-site-context.ts +136 -0
- package/src/astro-island.ts +61 -0
- package/src/astro.ts +151 -0
- package/src/companion-exports.ts +38 -0
- package/src/components/low-level/WebsiteButton.tsx +118 -0
- package/src/components/low-level/WebsiteHeading.tsx +96 -0
- package/src/components/low-level/WebsiteImage.tsx +96 -0
- package/src/components/low-level/WebsiteInternalButton.tsx +172 -0
- package/src/components/low-level/WebsiteInternalLink.tsx +193 -0
- package/src/components/low-level/WebsiteLink.tsx +71 -0
- package/src/components/low-level/WebsiteText.tsx +75 -0
- package/src/components/low-level/__tests__/WebsiteInternalButton.test.tsx +169 -0
- package/src/components/low-level/__tests__/WebsiteInternalLink.test.tsx +202 -0
- package/src/components/low-level/__tests__/primitives.test.tsx +141 -0
- package/src/config/__tests__/define-website-config.test.ts +231 -0
- package/src/config/__tests__/load-website-config.test.ts +215 -0
- package/src/config/define-website-config.ts +134 -0
- package/src/config/index.ts +38 -0
- package/src/config/load-website-config.ts +229 -0
- package/src/config/wildo-website-config.schemas.ts +322 -0
- package/src/config-loader.ts +35 -0
- package/src/core/__tests__/WebsitePageLayout.test.tsx +184 -0
- package/src/core/__tests__/WebsiteSection.test.tsx +85 -0
- package/src/core/__tests__/contexts.test.tsx +133 -0
- package/src/core/__tests__/core-boundary.test.ts +85 -0
- package/src/core/__tests__/define-website-page-manifest.test.tsx +111 -0
- package/src/core/__tests__/define-website-section.test.tsx +89 -0
- package/src/core/__tests__/useWebsiteDesignTokens.test.tsx +41 -0
- package/src/core/__tests__/useWebsiteLabel.test.tsx +117 -0
- package/src/core/anonymous-session/InboundContactForm.tsx +157 -0
- package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +44 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +62 -0
- package/src/core/anonymous-session/website-anonymous-session-client.ts +194 -0
- package/src/core/contexts/WebsitePageContext.tsx +52 -0
- package/src/core/contexts/WebsiteRuntimeContext.tsx +175 -0
- package/src/core/contexts/WebsiteSectionContext.tsx +59 -0
- package/src/core/contexts/useWebsitePage.ts +28 -0
- package/src/core/contexts/useWebsiteRuntime.ts +34 -0
- package/src/core/contexts/useWebsiteSection.ts +28 -0
- package/src/core/external-providers/frontend-provider-registry.website.ts +62 -0
- package/src/core/factories/define-website-page-manifest.ts +102 -0
- package/src/core/factories/define-website-section.ts +128 -0
- package/src/core/hooks/useWebsiteDesignTokens.ts +26 -0
- package/src/core/hooks/useWebsiteLabel.ts +121 -0
- package/src/core/layouts/WebsitePageLayout.tsx +311 -0
- package/src/core/layouts/WebsiteSection.tsx +139 -0
- package/src/core/routing/__tests__/internal-routing.utils.test.ts +134 -0
- package/src/core/routing/internal-routing.utils.ts +77 -0
- package/src/index.ts +132 -0
- package/src/mdx/BlogPost.tsx +296 -0
- package/src/mdx/__tests__/BlogPost.test.tsx +218 -0
- package/src/mdx/__tests__/blog-post-collection.config.test.ts +127 -0
- package/src/mdx/__tests__/website-mdx-provider.test.tsx +82 -0
- package/src/mdx/blog-post-collection.config.ts +177 -0
- package/src/mdx/blog-post-frontmatter-source.ts +31 -0
- package/src/mdx/index.ts +39 -0
- package/src/mdx/website-mdx-provider.tsx +95 -0
- package/src/mdx.ts +38 -0
- package/src/schemas/__tests__/blog-frontmatter.test.ts +50 -0
- package/src/schemas/__tests__/companion-exports-react-free.test.ts +51 -0
- package/src/schemas/__tests__/design-tokens.test.ts +163 -0
- package/src/schemas/__tests__/label-key.test.ts +46 -0
- package/src/schemas/__tests__/label-pack-validator.test.ts +146 -0
- package/src/schemas/__tests__/page-manifest.test.ts +66 -0
- package/src/schemas/__tests__/refs.test.ts +37 -0
- package/src/schemas/__tests__/root-config.test.ts +292 -0
- package/src/schemas/__tests__/section-definition.test.ts +47 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +543 -0
- package/src/schemas/blog/blog-post-frontmatter.shared.schemas.ts +111 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +166 -0
- package/src/schemas/label-keys/website-label-key.schemas.ts +88 -0
- package/src/schemas/manifests/website-page-manifest.shared.schemas.ts +266 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +504 -0
- package/src/schemas/refs/page-ref.schemas.ts +49 -0
- package/src/schemas/refs/section-ref.schemas.ts +50 -0
- package/src/schemas/sections/website-section-category.shared.ts +65 -0
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +120 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +579 -0
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +62 -0
- package/src/schemas/validators/label-pack.validator.ts +201 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useWebsitePage } from '../contexts/useWebsitePage';
|
|
4
|
+
import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
|
|
5
|
+
import { useWebsiteSection } from '../contexts/useWebsiteSection';
|
|
6
|
+
import {
|
|
7
|
+
WebsiteLabelKeySchema,
|
|
8
|
+
composeWebsiteLabelKey,
|
|
9
|
+
type WebsiteLabelKey,
|
|
10
|
+
} from '../../schemas/label-keys/website-label-key.schemas';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @wildo_source:part:start saas.website.use-label.hook facet:layer:core facet:family:website
|
|
14
|
+
*
|
|
15
|
+
* `useWebsiteLabel(slotPath)` — leaf-relative label resolution.
|
|
16
|
+
*
|
|
17
|
+
* Composes the slot leaf with the active page+section context to
|
|
18
|
+
* derive the full `WebsiteLabelKey` (`website.{pageRef}.{sectionRef}.{slotPath}`),
|
|
19
|
+
* looks it up in the active locale's label pack, and returns the
|
|
20
|
+
* resolved string.
|
|
21
|
+
*
|
|
22
|
+
* **Author ergonomics**: section authors call
|
|
23
|
+
* `useWebsiteLabel('headline')` from inside their section component
|
|
24
|
+
* without ever typing the page or section ref — the hook reads them
|
|
25
|
+
* from context. This keeps section components portable across pages
|
|
26
|
+
* (the same `<HeroSection />` can be reused on `landing` and on a
|
|
27
|
+
* dedicated `/about` page; only the surrounding `<WebsiteSection
|
|
28
|
+
* sectionRef="hero">` differs, and the label keys re-root accordingly).
|
|
29
|
+
*
|
|
30
|
+
* **Fail-loud diagnostics** for missing keys: returns the composed key
|
|
31
|
+
* itself (visible in the rendered DOM as e.g. `website.landing.hero.headline`)
|
|
32
|
+
* AND emits a `console.warn`. The visible-key fallback is preferred
|
|
33
|
+
* over an empty string or a thrown error because:
|
|
34
|
+
* - empty string silently breaks layout (zero-height heading) and
|
|
35
|
+
* leaks past visual review,
|
|
36
|
+
* - throwing would crash the entire page on a single missing label
|
|
37
|
+
* while the SEO crawler indexes a 500 page,
|
|
38
|
+
* - the rendered key is a clear authoring signal ("ship the missing
|
|
39
|
+
* pack entry") that designers and copywriters can spot without
|
|
40
|
+
* opening DevTools.
|
|
41
|
+
*
|
|
42
|
+
* **Hard guards** (each enforced by the underlying context hooks):
|
|
43
|
+
* - MUST be called inside `<WebsiteRuntimeContextProvider>`,
|
|
44
|
+
* - MUST be called inside `<WebsitePageLayout>`,
|
|
45
|
+
* - MUST be called inside `<WebsiteSection>`.
|
|
46
|
+
*
|
|
47
|
+
* @param slotPath leaf identifier (eg. `'headline'`, `'cta.primary'`,
|
|
48
|
+
* `'tiers[0].name'`). Anything that survives the slot
|
|
49
|
+
* portion of `WebsiteLabelKeySchema`'s regex.
|
|
50
|
+
* @returns the resolved label string, or the composed key itself when
|
|
51
|
+
* the pack lacks an entry (with a `console.warn`).
|
|
52
|
+
*/
|
|
53
|
+
export function useWebsiteLabel(slotPath: string): string {
|
|
54
|
+
const { labelPack } = useWebsiteRuntime();
|
|
55
|
+
const { pageRef } = useWebsitePage();
|
|
56
|
+
const { sectionRef } = useWebsiteSection();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compose + parse memoized on the three identity inputs. The parse
|
|
60
|
+
* itself is cheap (regex + brand) but executing it on every render
|
|
61
|
+
* of every label-bound primitive in a section adds up across a
|
|
62
|
+
* 12-section landing page.
|
|
63
|
+
*/
|
|
64
|
+
const fullKey = useMemo<WebsiteLabelKey>(
|
|
65
|
+
() =>
|
|
66
|
+
composeWebsiteLabelKey({
|
|
67
|
+
pageRef: pageRef as unknown as string,
|
|
68
|
+
sectionRef: sectionRef as unknown as string,
|
|
69
|
+
slotPath,
|
|
70
|
+
}),
|
|
71
|
+
[pageRef, sectionRef, slotPath],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const resolved = labelPack[fullKey as unknown as string];
|
|
75
|
+
if (resolved === undefined) {
|
|
76
|
+
console.warn(
|
|
77
|
+
`[useWebsiteLabel] Missing label for key "${fullKey as unknown as string}". ` +
|
|
78
|
+
`Add it to the active locale's pack (or remove the section's claim on this slot).`,
|
|
79
|
+
);
|
|
80
|
+
return fullKey as unknown as string;
|
|
81
|
+
}
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
/** @wildo_source:part:end saas.website.use-label.hook */
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @wildo_source:part:start saas.website.use-label-by-key.hook facet:layer:core facet:family:website
|
|
88
|
+
*
|
|
89
|
+
* `useWebsiteLabelByKey(key)` — absolute key lookup.
|
|
90
|
+
*
|
|
91
|
+
* Use when the consumer already holds a fully-composed label key and
|
|
92
|
+
* does NOT need page+section composition. Two main call sites:
|
|
93
|
+
* - the navigation header/footer rendering page meta titles
|
|
94
|
+
* (`useWebsiteLabelByKey(manifest.metaLabelKeys.titleKey)`),
|
|
95
|
+
* - section components that accept a `*Key` prop from their parent
|
|
96
|
+
* page (`<HeroSection headlineKey="website.landing.hero.headline" />`)
|
|
97
|
+
* instead of using the leaf-relative hook.
|
|
98
|
+
*
|
|
99
|
+
* Same fail-loud diagnostics as `useWebsiteLabel`: returns the key
|
|
100
|
+
* string and emits a `console.warn` on miss.
|
|
101
|
+
*
|
|
102
|
+
* Defensively re-parses the key through `WebsiteLabelKeySchema` so a
|
|
103
|
+
* caller that bypassed the type system (eg. an `as WebsiteLabelKey`
|
|
104
|
+
* cast on a malformed string) gets caught at the hook boundary
|
|
105
|
+
* instead of silently looking up garbage.
|
|
106
|
+
*/
|
|
107
|
+
export function useWebsiteLabelByKey(key: WebsiteLabelKey): string {
|
|
108
|
+
const { labelPack } = useWebsiteRuntime();
|
|
109
|
+
|
|
110
|
+
const safeKey = useMemo(() => WebsiteLabelKeySchema.parse(key), [key]);
|
|
111
|
+
|
|
112
|
+
const resolved = labelPack[safeKey as unknown as string];
|
|
113
|
+
if (resolved === undefined) {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[useWebsiteLabelByKey] Missing label for key "${safeKey as unknown as string}".`,
|
|
116
|
+
);
|
|
117
|
+
return safeKey as unknown as string;
|
|
118
|
+
}
|
|
119
|
+
return resolved;
|
|
120
|
+
}
|
|
121
|
+
/** @wildo_source:part:end saas.website.use-label-by-key.hook */
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import React, { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { BUILTIN_PROVIDER_CAPABILITY } from '@wildo-ai/saas-models/public-runtime';
|
|
3
|
+
|
|
4
|
+
import { WebsitePageContextProvider } from '../contexts/WebsitePageContext';
|
|
5
|
+
import type { WebsitePageContextValue } from '../contexts/WebsitePageContext';
|
|
6
|
+
import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
|
|
7
|
+
import { useWebsiteLabelByKey } from '../hooks/useWebsiteLabel';
|
|
8
|
+
import {
|
|
9
|
+
WebsitePageRefSchema,
|
|
10
|
+
type WebsitePageRef,
|
|
11
|
+
} from '../../schemas/refs/page-ref.schemas';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stable DOM id stamped on the `<main>` element so the framework's
|
|
15
|
+
* skip-link `<a href="#main-content">` can move keyboard focus past
|
|
16
|
+
* the chrome on every page (Phase 6 audit fix M-α). Co-located here
|
|
17
|
+
* (rather than exposed as a prop) because:
|
|
18
|
+
* - The skip-link target is a framework concern — exposing it as a
|
|
19
|
+
* consumer-tunable id would invite drift between the link's href
|
|
20
|
+
* and the main element's id.
|
|
21
|
+
* - Screen readers announce the heading inside `<main id="...">` on
|
|
22
|
+
* focus; a stable, framework-owned id keeps that affordance
|
|
23
|
+
* deterministic across every page on every site.
|
|
24
|
+
*/
|
|
25
|
+
const MAIN_CONTENT_DOM_ID = 'main-content';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inline style for the skip link's "visually hidden by default,
|
|
29
|
+
* visible on focus" pattern. Inlined (NOT shipped as a CSS class)
|
|
30
|
+
* because:
|
|
31
|
+
* - The framework intentionally avoids shipping any CSS file the
|
|
32
|
+
* consumer must opt in to — the only stylesheet emitted is the
|
|
33
|
+
* `:root { --website-*: … }` design-token block, and the rest of
|
|
34
|
+
* the styling surface lives in consumer code (see Phase 5
|
|
35
|
+
* `renderWebsiteDesignTokensCss`). Inlining keeps the skip link
|
|
36
|
+
* self-sufficient and side-effect-free.
|
|
37
|
+
* - The "skip-link" pattern is a small, well-known a11y idiom; the
|
|
38
|
+
* handful of style declarations here do not benefit from being
|
|
39
|
+
* reorganized into a class with a custom-property hook.
|
|
40
|
+
*
|
|
41
|
+
* The "focused" variant uses BOTH `:focus` AND `:focus-visible`
|
|
42
|
+
* pseudo-classes via a scoped `<style>` tag inside the layout output.
|
|
43
|
+
* We cannot apply pseudo-class styles via a `style` attribute, but
|
|
44
|
+
* emitting a tiny scoped block once at the layout root is safe (one
|
|
45
|
+
* block per page render, ~120 bytes) and avoids importing a CSS-in-JS
|
|
46
|
+
* runtime.
|
|
47
|
+
*
|
|
48
|
+
* **Why both selectors and not just `:focus-visible`** (Phase 6 fourth
|
|
49
|
+
* audit fix M-Η): `:focus-visible` is the modern UA-heuristic-driven
|
|
50
|
+
* selector that hides focus rings on mouse clicks, and it is the
|
|
51
|
+
* RIGHT default for buttons / links. For a skip link though we WANT
|
|
52
|
+
* the affordance to appear on EVERY focus event — the link is
|
|
53
|
+
* invisible by default and a keyboard-only user (the only intended
|
|
54
|
+
* audience) is the only one who can reach it. Older browsers that
|
|
55
|
+
* predate `:focus-visible` (and Safari < 15.4 with the feature
|
|
56
|
+
* disabled) would skip the rule entirely with a `:focus-visible`-only
|
|
57
|
+
* declaration, leaving the keyboard user looking at an apparently
|
|
58
|
+
* empty page when they pressed Tab. Pairing `:focus, :focus-visible`
|
|
59
|
+
* is the documented WCAG 2.1 fallback pattern.
|
|
60
|
+
*/
|
|
61
|
+
const SKIP_LINK_HIDDEN_STYLE: React.CSSProperties = {
|
|
62
|
+
position: 'absolute',
|
|
63
|
+
top: 0,
|
|
64
|
+
left: 0,
|
|
65
|
+
width: '1px',
|
|
66
|
+
height: '1px',
|
|
67
|
+
padding: 0,
|
|
68
|
+
margin: '-1px',
|
|
69
|
+
overflow: 'hidden',
|
|
70
|
+
clip: 'rect(0 0 0 0)',
|
|
71
|
+
whiteSpace: 'nowrap',
|
|
72
|
+
border: 0,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Single scoped style block for the skip link's `:focus-visible`
|
|
77
|
+
* affordance. Targets the `data-website-skip-link` attribute (NOT a
|
|
78
|
+
* class) so consumer Tailwind / utility classes cannot accidentally
|
|
79
|
+
* shadow it. Stays trivially small to keep payload under the
|
|
80
|
+
* "framework rendering" implicit budget.
|
|
81
|
+
*/
|
|
82
|
+
const SKIP_LINK_FOCUS_CSS = `
|
|
83
|
+
[data-website-skip-link]:focus,
|
|
84
|
+
[data-website-skip-link]:focus-visible {
|
|
85
|
+
position: fixed !important;
|
|
86
|
+
top: 0.5rem !important;
|
|
87
|
+
left: 0.5rem !important;
|
|
88
|
+
width: auto !important;
|
|
89
|
+
height: auto !important;
|
|
90
|
+
padding: 0.75rem 1rem !important;
|
|
91
|
+
margin: 0 !important;
|
|
92
|
+
overflow: visible !important;
|
|
93
|
+
clip: auto !important;
|
|
94
|
+
white-space: normal !important;
|
|
95
|
+
background: var(--website-colors-background, #fff) !important;
|
|
96
|
+
color: var(--website-colors-foreground, #000) !important;
|
|
97
|
+
border: 2px solid var(--website-colors-primary, #000) !important;
|
|
98
|
+
border-radius: var(--website-radii-md, 0.375rem) !important;
|
|
99
|
+
z-index: 9999 !important;
|
|
100
|
+
text-decoration: none !important;
|
|
101
|
+
}
|
|
102
|
+
`.trim();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @wildo_source:part:start saas.website.page-layout.component facet:layer:core facet:family:website
|
|
106
|
+
*
|
|
107
|
+
* `WebsitePageLayout` — the per-page structural primitive.
|
|
108
|
+
*
|
|
109
|
+
* **Three responsibilities** (each carrying its own semantic weight):
|
|
110
|
+
*
|
|
111
|
+
* 1. **Provides `WebsitePageContext`** — registers the active page
|
|
112
|
+
* ref so child sections (`<WebsiteSection>`) and label hooks
|
|
113
|
+
* (`useWebsiteLabel`) can derive label keys without re-typing
|
|
114
|
+
* the page ref at every leaf.
|
|
115
|
+
* 2. **Renders the site-wide chrome** — pulls
|
|
116
|
+
* `headerComponent` and `footerComponent` from the runtime
|
|
117
|
+
* navigation config and wraps the page's main content in the
|
|
118
|
+
* site's standard header/main/footer scaffold. Per-page
|
|
119
|
+
* overrides (`header={…}` / `footer={…}` props) take precedence
|
|
120
|
+
* over the runtime defaults so individual pages can opt into a
|
|
121
|
+
* custom chrome (eg. a campaign landing page running with
|
|
122
|
+
* header-only minimal chrome).
|
|
123
|
+
* 3. **Stamps DOM landmarks** — emits `data-website-page-ref` on
|
|
124
|
+
* the layout root for analytics / SEO crawlers / overlay
|
|
125
|
+
* tooling that needs to identify the active page from the DOM
|
|
126
|
+
* without booting React. When the website facet hydrates
|
|
127
|
+
* frontend-provider bootstraps, the same root also carries the
|
|
128
|
+
* active public provider refs for analytics / browser-monitoring /
|
|
129
|
+
* captcha so non-React tooling can detect the configured vendor
|
|
130
|
+
* without reverse-engineering app code.
|
|
131
|
+
*
|
|
132
|
+
* **Why semantic `<header>` / `<main>` / `<footer>` elements**:
|
|
133
|
+
* marketing-site SEO scoring penalizes pages that ship a single
|
|
134
|
+
* unstructured `<div>` tree. Using semantic landmarks costs nothing
|
|
135
|
+
* and improves crawler accessibility AND screen-reader
|
|
136
|
+
* navigability — both are first-class concerns for a public
|
|
137
|
+
* marketing surface.
|
|
138
|
+
*
|
|
139
|
+
* **Why this lives in `core/`** (not `components/`): the layout is
|
|
140
|
+
* structural framework machinery (it owns the page-context
|
|
141
|
+
* provisioning + the navigation slot resolution), not a styled
|
|
142
|
+
* primitive. Designers override `header` / `footer` via runtime
|
|
143
|
+
* config or per-page props, NOT by replacing this component.
|
|
144
|
+
*/
|
|
145
|
+
export interface WebsitePageLayoutProps {
|
|
146
|
+
/**
|
|
147
|
+
* The active page's ref. Accepts either an already-branded
|
|
148
|
+
* `WebsitePageRef` or a raw string — the layout parses through
|
|
149
|
+
* `WebsitePageRefSchema` so a malformed ref fails loud at render.
|
|
150
|
+
*
|
|
151
|
+
* Typically sourced from the page's `WebsitePageManifest.ref`
|
|
152
|
+
* (eg. `<WebsitePageLayout pageRef={LANDING_PAGE_MANIFEST.ref}>`).
|
|
153
|
+
*/
|
|
154
|
+
pageRef: WebsitePageRef | string;
|
|
155
|
+
/**
|
|
156
|
+
* Per-page header override. When supplied, takes precedence over
|
|
157
|
+
* `WebsiteRuntimeContext.designTokens` navigation config's
|
|
158
|
+
* `headerComponent`. Pass `null` explicitly to suppress the header
|
|
159
|
+
* entirely (campaign landing pages without site-wide nav).
|
|
160
|
+
*/
|
|
161
|
+
header?: ReactNode | null;
|
|
162
|
+
/**
|
|
163
|
+
* Per-page footer override. Same semantics as `header`.
|
|
164
|
+
*/
|
|
165
|
+
footer?: ReactNode | null;
|
|
166
|
+
/**
|
|
167
|
+
* Optional className applied to the root `<div data-website-page-ref="…">`
|
|
168
|
+
* wrapper. Use for page-level layout tweaks (eg. `min-h-screen` on
|
|
169
|
+
* landing pages).
|
|
170
|
+
*/
|
|
171
|
+
className?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Optional className applied to the inner `<main>` element. Use for
|
|
174
|
+
* vertical-rhythm overrides on long-scroll landing pages.
|
|
175
|
+
*/
|
|
176
|
+
mainClassName?: string;
|
|
177
|
+
/**
|
|
178
|
+
* The page's section composition. Typically a sequence of
|
|
179
|
+
* `<WebsiteSection>` wrappers around concrete section components.
|
|
180
|
+
*/
|
|
181
|
+
children: ReactNode;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function WebsitePageLayout({
|
|
185
|
+
pageRef,
|
|
186
|
+
header,
|
|
187
|
+
footer,
|
|
188
|
+
className,
|
|
189
|
+
mainClassName,
|
|
190
|
+
children,
|
|
191
|
+
}: WebsitePageLayoutProps): ReactNode {
|
|
192
|
+
const runtime = useWebsiteRuntime();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Defensive parse: callers should pass a `WebsitePageManifest.ref`
|
|
196
|
+
* (already branded), but accepting raw strings + parsing here means
|
|
197
|
+
* a hand-typed call site can't smuggle a malformed ref into the
|
|
198
|
+
* label-key derivation chain.
|
|
199
|
+
*/
|
|
200
|
+
const safePageRef = useMemo<WebsitePageRef>(
|
|
201
|
+
() => WebsitePageRefSchema.parse(pageRef),
|
|
202
|
+
[pageRef],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const pageContextValue = useMemo<WebsitePageContextValue>(
|
|
206
|
+
() => ({ pageRef: safePageRef }),
|
|
207
|
+
[safePageRef],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Header resolution rules:
|
|
212
|
+
* - `header === null` => explicit suppression
|
|
213
|
+
* - `header !== undefined` => per-page override (any ReactNode)
|
|
214
|
+
* - `header === undefined` => fall back to the runtime navigation
|
|
215
|
+
* config's `headerComponent`
|
|
216
|
+
*
|
|
217
|
+
* The fallback is the common path: every page renders the same
|
|
218
|
+
* site-wide chrome. The override path is the escape hatch (campaign
|
|
219
|
+
* landing pages with custom header layouts).
|
|
220
|
+
*/
|
|
221
|
+
const resolvedHeader = useMemo<ReactNode>(() => {
|
|
222
|
+
if (header === null) return null;
|
|
223
|
+
if (header !== undefined) return header;
|
|
224
|
+
const HeaderComponent = runtime.navigation.headerComponent;
|
|
225
|
+
return <HeaderComponent />;
|
|
226
|
+
}, [header, runtime.navigation.headerComponent]);
|
|
227
|
+
|
|
228
|
+
const resolvedFooter = useMemo<ReactNode>(() => {
|
|
229
|
+
if (footer === null) return null;
|
|
230
|
+
if (footer !== undefined) return footer;
|
|
231
|
+
const FooterComponent = runtime.navigation.footerComponent;
|
|
232
|
+
return <FooterComponent />;
|
|
233
|
+
}, [footer, runtime.navigation.footerComponent]);
|
|
234
|
+
|
|
235
|
+
const providerRootAttributes = useMemo<Record<string, string>>(() => {
|
|
236
|
+
const attrs: Record<string, string> = {};
|
|
237
|
+
const productAnalyticsProviderRef = runtime.frontendProviderRegistry
|
|
238
|
+
?.findProviderByProviderCapability(
|
|
239
|
+
BUILTIN_PROVIDER_CAPABILITY.FRONTEND_PRODUCT_ANALYTICS,
|
|
240
|
+
)
|
|
241
|
+
?.metadata.ref;
|
|
242
|
+
const errorMonitoringProviderRef = runtime.frontendProviderRegistry
|
|
243
|
+
?.findProviderByProviderCapability(
|
|
244
|
+
BUILTIN_PROVIDER_CAPABILITY.FRONTEND_ERROR_MONITORING,
|
|
245
|
+
)
|
|
246
|
+
?.metadata.ref;
|
|
247
|
+
const captchaProviderRef = runtime.frontendProviderRegistry
|
|
248
|
+
?.findProviderByProviderCapability(
|
|
249
|
+
BUILTIN_PROVIDER_CAPABILITY.FRONTEND_CAPTCHA,
|
|
250
|
+
)
|
|
251
|
+
?.metadata.ref;
|
|
252
|
+
|
|
253
|
+
if (productAnalyticsProviderRef) {
|
|
254
|
+
attrs['data-website-product-analytics-provider-ref'] = productAnalyticsProviderRef;
|
|
255
|
+
}
|
|
256
|
+
if (errorMonitoringProviderRef) {
|
|
257
|
+
attrs['data-website-error-monitoring-provider-ref'] = errorMonitoringProviderRef;
|
|
258
|
+
}
|
|
259
|
+
if (captchaProviderRef) {
|
|
260
|
+
attrs['data-website-captcha-provider-ref'] = captchaProviderRef;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return attrs;
|
|
264
|
+
}, [runtime.frontendProviderRegistry]);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolved skip-link label. Reads through the runtime label pack
|
|
268
|
+
* via the standard `useWebsiteLabelByKey` hook so a missing
|
|
269
|
+
* translation surfaces the same diagnostic shape as any other
|
|
270
|
+
* missing chrome label. The framework guarantees the key reaches
|
|
271
|
+
* the bridge (see `pickLabelsForPage`) and the consumer's
|
|
272
|
+
* label-pack validator (see `composeEffectiveChromeLabelKeys` in
|
|
273
|
+
* `label-pack-loader.ts`) ensures it is present in every locale
|
|
274
|
+
* pack — there is therefore no silent-fallback path that would
|
|
275
|
+
* ship an English skip link to a non-EN visitor.
|
|
276
|
+
*/
|
|
277
|
+
const skipLinkLabel = useWebsiteLabelByKey(runtime.skipToMainContentLabelKey);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<WebsitePageContextProvider value={pageContextValue}>
|
|
281
|
+
<div
|
|
282
|
+
data-website-page-ref={safePageRef as unknown as string}
|
|
283
|
+
className={className}
|
|
284
|
+
{...providerRootAttributes}
|
|
285
|
+
>
|
|
286
|
+
{/*
|
|
287
|
+
* Skip link rendered as the FIRST focusable element on the
|
|
288
|
+
* page so a keyboard / AT user can bypass the chrome on every
|
|
289
|
+
* navigation (WCAG 2.1 §2.4.1 "Bypass Blocks", Level A).
|
|
290
|
+
* Visually hidden by default; revealed on focus via the
|
|
291
|
+
* scoped `<style>` block below.
|
|
292
|
+
*/}
|
|
293
|
+
<style>{SKIP_LINK_FOCUS_CSS}</style>
|
|
294
|
+
<a
|
|
295
|
+
href={`#${MAIN_CONTENT_DOM_ID}`}
|
|
296
|
+
data-website-skip-link=""
|
|
297
|
+
style={SKIP_LINK_HIDDEN_STYLE}
|
|
298
|
+
>
|
|
299
|
+
{skipLinkLabel}
|
|
300
|
+
</a>
|
|
301
|
+
{resolvedHeader !== null ? <header>{resolvedHeader}</header> : null}
|
|
302
|
+
<main id={MAIN_CONTENT_DOM_ID} className={mainClassName} tabIndex={-1}>
|
|
303
|
+
{children}
|
|
304
|
+
</main>
|
|
305
|
+
{resolvedFooter !== null ? <footer>{resolvedFooter}</footer> : null}
|
|
306
|
+
</div>
|
|
307
|
+
</WebsitePageContextProvider>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
WebsitePageLayout.displayName = 'WebsitePageLayout';
|
|
311
|
+
/** @wildo_source:part:end saas.website.page-layout.component */
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useMemo, type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { WebsiteSectionContextProvider } from '../contexts/WebsiteSectionContext';
|
|
4
|
+
import type { WebsiteSectionContextValue } from '../contexts/WebsiteSectionContext';
|
|
5
|
+
import { WebsiteSectionCategory } from '../../schemas/sections/website-section-category.shared';
|
|
6
|
+
import {
|
|
7
|
+
WebsiteSectionRefSchema,
|
|
8
|
+
type WebsiteSectionRef,
|
|
9
|
+
} from '../../schemas/refs/section-ref.schemas';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @wildo_source:part:start saas.website.section.component facet:layer:core facet:family:website
|
|
13
|
+
*
|
|
14
|
+
* `WebsiteSection` — the per-section structural primitive.
|
|
15
|
+
*
|
|
16
|
+
* **Three responsibilities** (semantic-weight discipline):
|
|
17
|
+
*
|
|
18
|
+
* 1. **Provides `WebsiteSectionContext`** — registers the active
|
|
19
|
+
* sectionRef + category so leaf primitives (`useWebsiteLabel`,
|
|
20
|
+
* analytics overlays, structured-data emitters) can derive
|
|
21
|
+
* keys / metadata without re-typing the section's identity.
|
|
22
|
+
* 2. **Renders a semantic `<section>` landmark** — emits
|
|
23
|
+
* `data-section-ref` and `data-section-category` attributes so
|
|
24
|
+
* analytics tooling and SEO crawlers can identify and group
|
|
25
|
+
* sections without parsing className conventions. Optional
|
|
26
|
+
* `id` prop wires up anchor-link navigation
|
|
27
|
+
* (`<a href="#pricing">`).
|
|
28
|
+
* 3. **Provides a styling seam** — `className` is forwarded to the
|
|
29
|
+
* `<section>` element so designers can apply utility-class
|
|
30
|
+
* decorations (background, padding, max-width container) without
|
|
31
|
+
* having to compose multiple wrappers.
|
|
32
|
+
*
|
|
33
|
+
* **Why NOT render a `<div>` instead of `<section>`**: marketing
|
|
34
|
+
* pages benefit from semantic landmarks for SEO scoring and
|
|
35
|
+
* accessibility (screen-reader section navigation). A `<section>`
|
|
36
|
+
* with `data-section-category="hero"` is structurally meaningful;
|
|
37
|
+
* a `<div>` with the same data-attribute carries the same metadata
|
|
38
|
+
* but loses the landmark benefit.
|
|
39
|
+
*
|
|
40
|
+
* **Why expose `as` is NOT a prop**: switching the rendered tag
|
|
41
|
+
* (`<article>` for blog post bodies, `<aside>` for sidebar widgets)
|
|
42
|
+
* is legitimate but rare enough to be worth a separate primitive
|
|
43
|
+
* (Phase 4: `<WebsiteAside>`, `<WebsiteArticle>`). Keeping
|
|
44
|
+
* `WebsiteSection` rendered as a literal `<section>` removes a
|
|
45
|
+
* rarely-used prop from the section-author API and keeps the SEO
|
|
46
|
+
* landmark contract guaranteed at the type level.
|
|
47
|
+
*/
|
|
48
|
+
export interface WebsiteSectionProps {
|
|
49
|
+
/**
|
|
50
|
+
* The section's ref. Accepts an already-branded `WebsiteSectionRef`
|
|
51
|
+
* or a raw string — the layout parses through
|
|
52
|
+
* `WebsiteSectionRefSchema` so a malformed ref fails loud at
|
|
53
|
+
* render. Typically sourced from the page manifest's
|
|
54
|
+
* `sectionRefs[]` (eg. `<WebsiteSection sectionRef="hero" …>`).
|
|
55
|
+
*/
|
|
56
|
+
sectionRef: WebsiteSectionRef | string;
|
|
57
|
+
/**
|
|
58
|
+
* The section's structural intent (see `WebsiteSectionCategory`).
|
|
59
|
+
* Required at the wrapper level (rather than only on the section
|
|
60
|
+
* definition) because the wrapper emits `data-section-category`
|
|
61
|
+
* for analytics, and asking the wrapper to look up the category
|
|
62
|
+
* on a separate manifest registry would re-introduce the runtime
|
|
63
|
+
* coupling the per-section context exists to avoid.
|
|
64
|
+
*/
|
|
65
|
+
category: WebsiteSectionCategory;
|
|
66
|
+
/**
|
|
67
|
+
* Optional DOM id. Wires anchor-link navigation
|
|
68
|
+
* (`<a href="#pricing">`). When omitted, the section has no DOM
|
|
69
|
+
* id — by design; not every section is an anchor target.
|
|
70
|
+
*/
|
|
71
|
+
id?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Optional className applied to the `<section>` element. Used by
|
|
74
|
+
* designers for layout / spacing utilities. Section components
|
|
75
|
+
* themselves should NOT pass className through to this wrapper —
|
|
76
|
+
* keep the styling responsibility at the page-composition level.
|
|
77
|
+
*/
|
|
78
|
+
className?: string;
|
|
79
|
+
/**
|
|
80
|
+
* The section's body. Typically a single section component
|
|
81
|
+
* (`<HeroSection {…heroProps} />`). May be plain JSX for one-off
|
|
82
|
+
* sections that don't justify a `defineWebsiteSection` factory call.
|
|
83
|
+
*/
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
/**
|
|
86
|
+
* Pass-through `aria-*` / `role` attributes for sections that need
|
|
87
|
+
* to refine their landmark semantics (eg. `aria-label="Featured
|
|
88
|
+
* customer logos"` on a logo wall).
|
|
89
|
+
*/
|
|
90
|
+
ariaLabel?: string;
|
|
91
|
+
ariaLabelledBy?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function WebsiteSection({
|
|
95
|
+
sectionRef,
|
|
96
|
+
category,
|
|
97
|
+
id,
|
|
98
|
+
className,
|
|
99
|
+
children,
|
|
100
|
+
ariaLabel,
|
|
101
|
+
ariaLabelledBy,
|
|
102
|
+
}: WebsiteSectionProps): ReactNode {
|
|
103
|
+
const safeSectionRef = useMemo<WebsiteSectionRef>(
|
|
104
|
+
() => WebsiteSectionRefSchema.parse(sectionRef),
|
|
105
|
+
[sectionRef],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const sectionContextValue = useMemo<WebsiteSectionContextValue>(
|
|
109
|
+
() => ({ sectionRef: safeSectionRef, category }),
|
|
110
|
+
[safeSectionRef, category],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build the `<section>` attribute set imperatively to keep
|
|
115
|
+
* `aria-*` props off the element when omitted (rendering
|
|
116
|
+
* `aria-label={undefined}` still emits the attribute in some
|
|
117
|
+
* React versions — explicit absence is cleaner).
|
|
118
|
+
*/
|
|
119
|
+
const sectionAttrs: HTMLAttributes<HTMLElement> = {
|
|
120
|
+
className,
|
|
121
|
+
};
|
|
122
|
+
if (id !== undefined) sectionAttrs.id = id;
|
|
123
|
+
if (ariaLabel !== undefined) sectionAttrs['aria-label'] = ariaLabel;
|
|
124
|
+
if (ariaLabelledBy !== undefined) sectionAttrs['aria-labelledby'] = ariaLabelledBy;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<WebsiteSectionContextProvider value={sectionContextValue}>
|
|
128
|
+
<section
|
|
129
|
+
data-section-ref={safeSectionRef as unknown as string}
|
|
130
|
+
data-section-category={category}
|
|
131
|
+
{...sectionAttrs}
|
|
132
|
+
>
|
|
133
|
+
{children}
|
|
134
|
+
</section>
|
|
135
|
+
</WebsiteSectionContextProvider>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
WebsiteSection.displayName = 'WebsiteSection';
|
|
139
|
+
/** @wildo_source:part:end saas.website.section.component */
|