@tokenoftrust/storefront-runner 0.1.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/LICENSE +58 -0
- package/README.md +40 -0
- package/apps/storefront/.dev.vars.example +41 -0
- package/apps/storefront/.env.example +24 -0
- package/apps/storefront/astro.config.mjs +245 -0
- package/apps/storefront/env.d.ts +155 -0
- package/apps/storefront/lib/ensure-preview-reconcile-secret.sh +31 -0
- package/apps/storefront/lib/resolve-cf-token.sh +48 -0
- package/apps/storefront/lib/resolve-tot-credentials.sh +69 -0
- package/apps/storefront/migrations/0000_baseline.sql +54 -0
- package/apps/storefront/migrations/0001_products_fts.sql +9 -0
- package/apps/storefront/migrations/README.md +54 -0
- package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/_journal.json +20 -0
- package/apps/storefront/package.json +41 -0
- package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
- package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
- package/apps/storefront/public/brand/northwind-og.svg +13 -0
- package/apps/storefront/public/favicon.svg +5 -0
- package/apps/storefront/public/js/dashboard-team.js +147 -0
- package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
- package/apps/storefront/src/components/Badge.astro +34 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
- package/apps/storefront/src/components/CollectionCard.astro +56 -0
- package/apps/storefront/src/components/EmptyState.astro +35 -0
- package/apps/storefront/src/components/Img.astro +68 -0
- package/apps/storefront/src/components/ProductCard.astro +228 -0
- package/apps/storefront/src/components/Section.astro +39 -0
- package/apps/storefront/src/components/Seo.astro +70 -0
- package/apps/storefront/src/components/WidgetFrame.astro +34 -0
- package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
- package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
- package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +116 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +181 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +181 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
- package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
- package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +105 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
- package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
- package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
- package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
- package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +378 -0
- package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
- package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
- package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
- package/apps/storefront/src/components/marketing/Faq.astro +37 -0
- package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
- package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
- package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
- package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
- package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
- package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
- package/apps/storefront/src/components/marketing/Steps.astro +36 -0
- package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
- package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
- package/apps/storefront/src/components/nav/Footer.astro +148 -0
- package/apps/storefront/src/components/nav/Header.astro +190 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +146 -0
- package/apps/storefront/src/components/plp/Pagination.astro +99 -0
- package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
- package/apps/storefront/src/config/devTenants.ts +16 -0
- package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
- package/apps/storefront/src/config/resolver.ts +46 -0
- package/apps/storefront/src/config/tenants.ts +193 -0
- package/apps/storefront/src/layouts/Layout.astro +303 -0
- package/apps/storefront/src/lib/analytics/index.ts +47 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
- package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
- package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +107 -0
- package/apps/storefront/src/lib/auth/route.ts +146 -0
- package/apps/storefront/src/lib/auth/session.ts +203 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
- package/apps/storefront/src/lib/basePath.ts +41 -0
- package/apps/storefront/src/lib/cfImage.ts +55 -0
- package/apps/storefront/src/lib/chrome/model.ts +65 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/content-edit/client.ts +230 -0
- package/apps/storefront/src/lib/content-edit/index.ts +14 -0
- package/apps/storefront/src/lib/content-edit/route.ts +25 -0
- package/apps/storefront/src/lib/content-edit/types.ts +19 -0
- package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
- package/apps/storefront/src/lib/d1/catalog.ts +289 -0
- package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
- package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
- package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
- package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
- package/apps/storefront/src/lib/demoGate.ts +113 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +127 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +132 -0
- package/apps/storefront/src/lib/diag/redact.ts +20 -0
- package/apps/storefront/src/lib/edgeCache.ts +128 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
- package/apps/storefront/src/lib/env.ts +60 -0
- package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
- package/apps/storefront/src/lib/format.ts +29 -0
- package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
- package/apps/storefront/src/lib/imageAttrs.ts +55 -0
- package/apps/storefront/src/lib/jsonld.ts +136 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/memoryKv.ts +32 -0
- package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
- package/apps/storefront/src/lib/placeholder.ts +99 -0
- package/apps/storefront/src/lib/previewReload.ts +125 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +133 -0
- package/apps/storefront/src/lib/search/index.ts +74 -0
- package/apps/storefront/src/lib/sections.ts +129 -0
- package/apps/storefront/src/lib/securityHeaders.ts +31 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +524 -0
- package/apps/storefront/src/lib/tenantRouting.ts +91 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
- package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
- package/apps/storefront/src/lib/tot/query.ts +187 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +119 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +413 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
- package/apps/storefront/src/pages/api/auth/send.ts +85 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +64 -0
- package/apps/storefront/src/pages/api/csp-report.ts +25 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +44 -0
- package/apps/storefront/src/pages/api/newsletter.ts +104 -0
- package/apps/storefront/src/pages/api/request-access.ts +172 -0
- package/apps/storefront/src/pages/auth/login.astro +149 -0
- package/apps/storefront/src/pages/auth/magic.astro +105 -0
- package/apps/storefront/src/pages/capabilities.astro +292 -0
- package/apps/storefront/src/pages/collections/[handle].astro +154 -0
- package/apps/storefront/src/pages/collections/index.astro +59 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
- package/apps/storefront/src/pages/dashboard/index.astro +160 -0
- package/apps/storefront/src/pages/dev.astro +653 -0
- package/apps/storefront/src/pages/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +443 -0
- package/apps/storefront/src/pages/robots.txt.ts +24 -0
- package/apps/storefront/src/pages/saved.astro +35 -0
- package/apps/storefront/src/pages/search.astro +150 -0
- package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +646 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +85 -0
- package/apps/storefront/src/pages/style-guide/index.astro +94 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +277 -0
- package/apps/storefront/src/themes/reference.ts +61 -0
- package/apps/storefront/src/themes/schema.ts +111 -0
- package/apps/storefront/src/themes/tenants/index.ts +51 -0
- package/apps/storefront/tsconfig.json +16 -0
- package/apps/storefront/vitest.config.ts +22 -0
- package/apps/storefront/wrangler.toml +175 -0
- package/package.json +23 -0
- package/packages/public-runtime/LICENSE +58 -0
- package/packages/public-runtime/package.json +22 -0
- package/packages/public-runtime/src/binary-path.ts +68 -0
- package/packages/public-runtime/src/capabilities.ts +89 -0
- package/packages/public-runtime/src/catalog-api.ts +81 -0
- package/packages/public-runtime/src/catalog-d1.ts +192 -0
- package/packages/public-runtime/src/csp.ts +109 -0
- package/packages/public-runtime/src/customization-preview.ts +197 -0
- package/packages/public-runtime/src/customization-reconcile.ts +63 -0
- package/packages/public-runtime/src/customization-runtime.ts +157 -0
- package/packages/public-runtime/src/customization-scripts.ts +64 -0
- package/packages/public-runtime/src/customization-versioning.ts +95 -0
- package/packages/public-runtime/src/foxy.ts +184 -0
- package/packages/public-runtime/src/index.ts +30 -0
- package/packages/public-runtime/src/product.ts +159 -0
- package/packages/public-runtime/src/search.ts +45 -0
- package/packages/public-runtime/src/tenant-assets.ts +444 -0
- package/packages/public-runtime/src/tenant.ts +243 -0
- package/packages/public-runtime/tsconfig.json +8 -0
- package/packages/public-runtime/vitest.config.ts +8 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build/copy-tenant-assets.mjs +69 -0
- package/scripts/tot-dev.mjs +439 -0
- package/tenants/bigdvapor/content/blog.json +52 -0
- package/tenants/bigdvapor/content/chrome.html +149 -0
- package/tenants/bigdvapor/content/chrome.json +47 -0
- package/tenants/bigdvapor/content/home.html +463 -0
- package/tenants/bigdvapor/content/home.json +73 -0
- package/tenants/bigdvapor/content/pages/about.json +5 -0
- package/tenants/bigdvapor/content/pages/privacy.json +6 -0
- package/tenants/bigdvapor/content/pages/shipping-returns.json +6 -0
- package/tenants/bigdvapor/content/pages-html/blogs/news.html +86 -0
- package/tenants/bigdvapor/content/pages-html/pages/about-us.html +106 -0
- package/tenants/bigdvapor/content/pages-html/pages/contact-us.html +61 -0
- package/tenants/bigdvapor/content/pages-html/pages/privacy-policy.html +72 -0
- package/tenants/bigdvapor/content/pages-html/pages/shipping-returns.html +172 -0
- package/tenants/bigdvapor/content/themes/bigd-navy.json +109 -0
- package/tenants/bigdvapor/public/img/brands/adjust-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/again-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/al-fakher-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/arro-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/cookies-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/core-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/cyclone-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/dinner-lady-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/extre-bar-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/fifty-bar-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/firerose-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/flavor-beast-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/foger-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/kk-energy-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/kumi-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/lost-vape-dispoable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/melo-labs-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/mixo-vape-disposable.webp +0 -0
- package/tenants/bigdvapor/public/img/brands/one-tank-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/oxbar-vape-disposable.webp +0 -0
- package/tenants/bigdvapor/public/img/brands/pillow-talk-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/pod-king-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/raz-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/hero-texas.jpg +0 -0
- package/tenants/bigdvapor/public/img/og.jpg +0 -0
- package/tenants/bigdvapor/public/img/pages/AdobeStock_202106312_web.jpg +0 -0
- package/tenants/bigdvapor/public/img/pages/CREW_web.jpg +0 -0
- package/tenants/bigdvapor/public/img/pages/Hero-Image.jpg +0 -0
- package/tenants/bigdvapor/public/img/tiles/adjust.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/again.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/al-fakher.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/arro.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/cookies.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/core.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/cyclone.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/disposables.svg +23 -0
- package/tenants/bigdvapor/public/logo-wordmark.png +0 -0
- package/tenants/bigdvapor/public/pages/home.css +118 -0
- package/tenants/bigdvapor/public/pages/mkt.css +185 -0
- package/tenants/bigdvapor/public/pages/page.css +148 -0
- package/tenants/bigdvapor/public/themes/bigd-navy.css +73 -0
- package/tenants/bigdvapor/theme.json +12 -0
- package/tenants/giantvapes/content/chrome.html +152 -0
- package/tenants/giantvapes/content/chrome.json +94 -0
- package/tenants/giantvapes/content/home.html +194 -0
- package/tenants/giantvapes/content/home.json +50 -0
- package/tenants/giantvapes/content/pages/about.json +10 -0
- package/tenants/giantvapes/content/pages/privacy.json +6 -0
- package/tenants/giantvapes/content/pages/shipping-returns.json +6 -0
- package/tenants/giantvapes/content/pages-html/blogs/news.html +68 -0
- package/tenants/giantvapes/content/pages-html/pages/about-us.html +95 -0
- package/tenants/giantvapes/content/pages-html/pages/contact-us.html +68 -0
- package/tenants/giantvapes/content/pages-html/pages/privacy-policy.html +65 -0
- package/tenants/giantvapes/content/pages-html/pages/shipping-returns.html +77 -0
- package/tenants/giantvapes/content/themes/giant-navy.json +73 -0
- package/tenants/giantvapes/public/img/hero-suicide-bunny.jpg +0 -0
- package/tenants/giantvapes/public/img/hero.webp +0 -0
- package/tenants/giantvapes/public/img/og.jpg +0 -0
- package/tenants/giantvapes/public/logo-wordmark.png +0 -0
- package/tenants/giantvapes/public/pages/home.css +120 -0
- package/tenants/giantvapes/public/pages/mkt.css +185 -0
- package/tenants/giantvapes/public/pages/page.css +155 -0
- package/tenants/giantvapes/public/themes/giant-navy.css +76 -0
- package/tenants/giantvapes/theme.json +39 -0
- package/tenants/home/content/chrome.json +11 -0
- package/tenants/home/content/home.html +304 -0
- package/tenants/home/content/home.json +13 -0
- package/tenants/home/public/js/storefront.js +210 -0
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +212 -0
- package/tenants/home/theme.json +6 -0
- package/tenants/tokenoftrust/.tot/config.json +9 -0
- package/tenants/tokenoftrust/content/chrome.json +55 -0
- package/tenants/tokenoftrust/content/home.html +723 -0
- package/tenants/tokenoftrust/content/home.json +233 -0
- package/tenants/tokenoftrust/content/pages-html/careers.html +231 -0
- package/tenants/tokenoftrust/content/pages-html/company.html +275 -0
- package/tenants/tokenoftrust/content/pages-html/contact/contact-sales.html +188 -0
- package/tenants/tokenoftrust/content/pages-html/contact/startup-apply.html +206 -0
- package/tenants/tokenoftrust/content/pages-html/contact.html +178 -0
- package/tenants/tokenoftrust/content/pages-html/industries/adult-content-age-verification.html +286 -0
- package/tenants/tokenoftrust/content/pages-html/industries/cannabis.html +277 -0
- package/tenants/tokenoftrust/content/pages-html/pact-act-pricing.html +289 -0
- package/tenants/tokenoftrust/content/pages-html/pricing.html +483 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-estimation.html +278 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-gate.html +279 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-verification.html +265 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance.html +173 -0
- package/tenants/tokenoftrust/content/pages-html/product/compliance/aml.html +214 -0
- package/tenants/tokenoftrust/content/pages-html/product/compliance/pact-act.html +226 -0
- package/tenants/tokenoftrust/content/pages-html/product/fraud-prevention.html +254 -0
- package/tenants/tokenoftrust/content/pages-html/product/social-profile-verification.html +212 -0
- package/tenants/tokenoftrust/content/pages-html/product/tax/excise-tax.html +213 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/aml.html +259 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/biometric-selfie.html +305 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/coverage.html +254 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/document-verification.html +306 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/electronic-identity-verification.html +304 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/government-id-verification.html +308 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification.html +183 -0
- package/tenants/tokenoftrust/content/pages-html/product.html +228 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations/bigcommerce-integration.html +220 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations/shopify-integration.html +221 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations/wordpress.html +220 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations.html +172 -0
- package/tenants/tokenoftrust/content/pages-html/resources/refer-a-friend-program.html +317 -0
- package/tenants/tokenoftrust/content/pages-html/resources.html +177 -0
- package/tenants/tokenoftrust/content/pages-html/reviews.html +273 -0
- package/tenants/tokenoftrust/content/pages-html/solutions.html +292 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-alcohol.html +289 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-cigars-and-tobacco.html +286 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-firearm-accessories.html +267 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-peptides.html +273 -0
- package/tenants/tokenoftrust/content/themes/tot-navy.json +88 -0
- package/tenants/tokenoftrust/public/favicon.png +0 -0
- package/tenants/tokenoftrust/public/logo-icon.png +0 -0
- package/tenants/tokenoftrust/public/logo-wordmark.png +0 -0
- package/tenants/tokenoftrust/public/pages/company.css +108 -0
- package/tenants/tokenoftrust/public/pages/home.css +457 -0
- package/tenants/tokenoftrust/public/pages/mkt.css +482 -0
- package/tenants/tokenoftrust/public/pages/pricing.css +214 -0
- package/tenants/tokenoftrust/public/pages/solutions.css +66 -0
- package/tenants/tokenoftrust/public/themes/tot-navy-1.0.0.css +136 -0
- package/tenants/tokenoftrust/public/themes/tot-navy.css +358 -0
- package/tenants/tokenoftrust/theme.json +31 -0
- package/tsconfig.base.json +20 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /auth/magic — GET-safe landing for the magic-link sign-in.
|
|
4
|
+
*
|
|
5
|
+
* SECURITY INVARIANT: a GET of this page renders a "Confirm sign in" card and
|
|
6
|
+
* **consumes nothing** — the one-time token is only exchanged when the human clicks
|
|
7
|
+
* Confirm, which POSTs to /api/auth/magic-exchange. Inbox "safe-link" scanners fire
|
|
8
|
+
* GET on every URL; if this page auto-exchanged on GET, a scanner would silently burn
|
|
9
|
+
* the token before the user ever clicked. So: NEVER exchange on GET. The token rides
|
|
10
|
+
* in a hidden form field and leaves only via the POST.
|
|
11
|
+
*/
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
const cspNonce = Astro.locals.cspNonce;
|
|
15
|
+
const url = new URL(Astro.request.url);
|
|
16
|
+
const token = url.searchParams.get("token") ?? "";
|
|
17
|
+
const email = (url.searchParams.get("email") ?? "").trim(); // display-only (never trusted)
|
|
18
|
+
const errorCode = url.searchParams.get("error");
|
|
19
|
+
const next = (() => {
|
|
20
|
+
const n = url.searchParams.get("next") ?? "/dashboard";
|
|
21
|
+
return n.startsWith("/") && !n.startsWith("//") ? n : "/dashboard";
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
const ERROR_MESSAGES: Record<string, string> = {
|
|
25
|
+
invalid: "This sign-in link isn't valid or has already been used. Ask for a fresh invite.",
|
|
26
|
+
expired: "This sign-in link has expired. Ask for a fresh invite.",
|
|
27
|
+
used: "This sign-in link was already used. Ask for a fresh invite.",
|
|
28
|
+
missing: "This sign-in link is missing its token.",
|
|
29
|
+
unconfigured: "Sign-in isn't available right now. Please try again shortly.",
|
|
30
|
+
verify_failed: "We couldn't complete sign-in. Ask for a fresh invite and try again.",
|
|
31
|
+
};
|
|
32
|
+
// On GET with a token → confirm card. On GET with ?error → error card. No token, no
|
|
33
|
+
// error → treat as a broken link.
|
|
34
|
+
const errorMessage = errorCode
|
|
35
|
+
? (ERROR_MESSAGES[errorCode] ?? "Something went wrong — ask for a fresh invite.")
|
|
36
|
+
: token
|
|
37
|
+
? null
|
|
38
|
+
: ERROR_MESSAGES.missing;
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
45
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
46
|
+
<title>Confirm sign in — Regulated Storefront by Token of Trust©</title>
|
|
47
|
+
<style nonce={cspNonce}>
|
|
48
|
+
:root { color-scheme: light dark; }
|
|
49
|
+
* { box-sizing: border-box; }
|
|
50
|
+
body {
|
|
51
|
+
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
52
|
+
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
53
|
+
padding: 2rem 1.25rem; background: #f5f4f0; color: #1c1c1c;
|
|
54
|
+
}
|
|
55
|
+
@media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
|
|
56
|
+
.card {
|
|
57
|
+
width: 100%; max-width: 26rem; background: #fff; border: 1px solid #e6e4de;
|
|
58
|
+
border-radius: 14px; padding: 2rem 1.75rem; box-shadow: 0 6px 24px rgba(0,0,0,.06);
|
|
59
|
+
}
|
|
60
|
+
@media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; box-shadow: none; } }
|
|
61
|
+
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em;
|
|
62
|
+
text-transform: uppercase; color: #8a7f66; margin: 0 0 .75rem; }
|
|
63
|
+
h1 { font-size: 1.4rem; letter-spacing: -.01em; margin: 0 0 .4rem; }
|
|
64
|
+
.sub { color: #6a6a6a; margin: 0 0 1.5rem; font-size: .95rem; }
|
|
65
|
+
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
66
|
+
.who { font-weight: 600; }
|
|
67
|
+
button {
|
|
68
|
+
width: 100%; margin-top: .5rem; padding: .8rem 1rem; border: 0; border-radius: 9px;
|
|
69
|
+
background: #1a1a1a; color: #fff; font-weight: 600; font-size: 1rem; cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
@media (prefers-color-scheme: dark) { button { background: #ececf1; color: #14141a; } }
|
|
72
|
+
button:hover { opacity: .92; }
|
|
73
|
+
.err { background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c;
|
|
74
|
+
border-radius: 9px; padding: .7rem .85rem; font-size: .9rem; margin: 0 0 1rem; }
|
|
75
|
+
@media (prefers-color-scheme: dark) { .err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; } }
|
|
76
|
+
.note { font-size: .78rem; color: #9a9a9a; margin: 1.1rem 0 0; text-align: center; }
|
|
77
|
+
a { color: inherit; }
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
<body>
|
|
81
|
+
<main class="card">
|
|
82
|
+
<p class="eyebrow">Token of Trust© · Regulated Storefront</p>
|
|
83
|
+
{errorMessage ? (
|
|
84
|
+
<>
|
|
85
|
+
<h1>Sign-in link problem</h1>
|
|
86
|
+
<p class="err">{errorMessage}</p>
|
|
87
|
+
<p class="note"><a href="/auth/login">Sign in with a code instead →</a></p>
|
|
88
|
+
</>
|
|
89
|
+
) : (
|
|
90
|
+
<>
|
|
91
|
+
<h1>Confirm sign in</h1>
|
|
92
|
+
<p class="sub">{email
|
|
93
|
+
? <>Continue as <span class="who">{email}</span> to the Regulated Storefront dashboard.</>
|
|
94
|
+
: <>Continue to the Regulated Storefront dashboard.</>}</p>
|
|
95
|
+
<form method="post" action="/api/auth/magic-exchange">
|
|
96
|
+
<input type="hidden" name="token" value={token} />
|
|
97
|
+
<input type="hidden" name="next" value={next} />
|
|
98
|
+
<button type="submit">Confirm sign in</button>
|
|
99
|
+
</form>
|
|
100
|
+
<p class="note">This is a single-use link — confirming completes your sign-in.</p>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</main>
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Capabilities console (`/__capabilities`) — a read-only control surface for the
|
|
4
|
+
* three PR-gated switches (cart/checkout, age verification, excise tax) across
|
|
5
|
+
* the three environments (Local · Preview · Live).
|
|
6
|
+
*
|
|
7
|
+
* It is deliberately read-only: capabilities are config-as-code in the tenant
|
|
8
|
+
* repo's `.tot/config.json`, so the way you CHANGE a switch is a PR — a live
|
|
9
|
+
* toggle here would bypass the promote gate this whole design exists to enforce.
|
|
10
|
+
* The console reflects the real per-environment resolved state (each column reads
|
|
11
|
+
* the published capabilities artifact for its pointer) and makes the flow legible.
|
|
12
|
+
*/
|
|
13
|
+
import { readKv } from "@/lib/env";
|
|
14
|
+
import {
|
|
15
|
+
readPublishedArtifact,
|
|
16
|
+
capabilitiesArtifactPath,
|
|
17
|
+
resolveCapabilities,
|
|
18
|
+
} from "@tot/public-runtime";
|
|
19
|
+
import type { CapabilityConfig, CapabilityKey } from "@tot/public-runtime";
|
|
20
|
+
|
|
21
|
+
const { tenant, cspNonce } = Astro.locals;
|
|
22
|
+
|
|
23
|
+
// Resolve each environment's capabilities from ITS pointer. Local = the
|
|
24
|
+
// build-time / working-tree default (no publish); Preview = the `test` pointer
|
|
25
|
+
// (PR head, via reconcile); Live = the `live` pointer (promoted). In plain dev
|
|
26
|
+
// the KV store is empty, so all three read the static default — an honest "no PR
|
|
27
|
+
// has diverged them yet" state.
|
|
28
|
+
const kv = await readKv("TENANT_CACHE");
|
|
29
|
+
const kvGet = kv ? (k: string) => kv.get(k) : async () => null;
|
|
30
|
+
|
|
31
|
+
async function capsForPointer(env: "test" | "live"): Promise<CapabilityConfig> {
|
|
32
|
+
const raw = await readPublishedArtifact(
|
|
33
|
+
kvGet,
|
|
34
|
+
tenant.tenant_id,
|
|
35
|
+
env,
|
|
36
|
+
capabilitiesArtifactPath(tenant.tenant_id),
|
|
37
|
+
);
|
|
38
|
+
let override: Partial<CapabilityConfig> | null = null;
|
|
39
|
+
try {
|
|
40
|
+
override = raw ? (JSON.parse(raw) as Partial<CapabilityConfig>) : null;
|
|
41
|
+
} catch {
|
|
42
|
+
override = null;
|
|
43
|
+
}
|
|
44
|
+
return resolveCapabilities(tenant, override);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const local = resolveCapabilities(tenant, null);
|
|
48
|
+
const preview = await capsForPointer("test");
|
|
49
|
+
const live = await capsForPointer("live");
|
|
50
|
+
|
|
51
|
+
// Which deployment am I looking at? (host-based, for the "you are here" marker.)
|
|
52
|
+
const host = (Astro.request.headers.get("host") ?? "").toLowerCase();
|
|
53
|
+
const hereKey = host.startsWith("localhost") || host.startsWith("127.0.0.1")
|
|
54
|
+
? "local"
|
|
55
|
+
: host.startsWith("preview.")
|
|
56
|
+
? "preview"
|
|
57
|
+
: "live";
|
|
58
|
+
|
|
59
|
+
interface CapMeta {
|
|
60
|
+
key: CapabilityKey;
|
|
61
|
+
name: string;
|
|
62
|
+
sub: string;
|
|
63
|
+
track: string;
|
|
64
|
+
}
|
|
65
|
+
const CAPS: CapMeta[] = [
|
|
66
|
+
{ key: "cartCheckout", name: "Cart & Checkout", sub: "FoxyCart · Track A", track: "commerce" },
|
|
67
|
+
{ key: "ageVerification", name: "Age & ID Verification", sub: "Token of Trust · 21+", track: "compliance" },
|
|
68
|
+
{ key: "exciseTax", name: "Excise Tax Collection", sub: "at checkout · Track B", track: "compliance" },
|
|
69
|
+
];
|
|
70
|
+
const ENVS = [
|
|
71
|
+
{ key: "local", label: "Local", prov: "working tree", caps: local, gated: false },
|
|
72
|
+
{ key: "preview", label: "Preview", prov: "PR head → test", caps: preview, gated: false },
|
|
73
|
+
{ key: "live", label: "Live", prov: "promoted", caps: live, gated: true },
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
const on = (c: CapabilityConfig, k: CapabilityKey) => c[k].enabled;
|
|
77
|
+
// A cell "diverges" when its state differs from Live — the amber signal that a PR
|
|
78
|
+
// has changed something not yet promoted.
|
|
79
|
+
const diverges = (envCaps: CapabilityConfig, k: CapabilityKey) =>
|
|
80
|
+
on(envCaps, k) !== on(live, k);
|
|
81
|
+
const anyDivergence = CAPS.some((c) =>
|
|
82
|
+
ENVS.some((e) => !e.gated && diverges(e.caps, c.key)),
|
|
83
|
+
);
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<!doctype html>
|
|
87
|
+
<html lang="en" data-theme="console">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="utf-8" />
|
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
91
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
92
|
+
<title>Capabilities · {tenant.displayName}</title>
|
|
93
|
+
<style nonce={cspNonce} set:html={`
|
|
94
|
+
:root {
|
|
95
|
+
--field: #0a2130; --field-2: #0d2839; --panel: #10344a;
|
|
96
|
+
--line: rgba(122,182,214,.16); --line-2: rgba(122,182,214,.34);
|
|
97
|
+
--ink: #e9f3f8; --dim: #85a6ba; --faint: #5c7d92;
|
|
98
|
+
--on: #4fe3b0; --on-bg: rgba(79,227,176,.10);
|
|
99
|
+
--off: #607e91; --pending: #ffc24d; --pending-bg: rgba(255,194,77,.10);
|
|
100
|
+
--gate: #ff8a72;
|
|
101
|
+
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
102
|
+
--sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
103
|
+
}
|
|
104
|
+
* { box-sizing: border-box; }
|
|
105
|
+
html, body { margin: 0; }
|
|
106
|
+
body {
|
|
107
|
+
background:
|
|
108
|
+
radial-gradient(120% 80% at 88% -10%, #12405b 0%, transparent 55%),
|
|
109
|
+
linear-gradient(180deg, var(--field-2), var(--field));
|
|
110
|
+
color: var(--ink); font-family: var(--mono);
|
|
111
|
+
min-height: 100vh; padding: clamp(1.25rem, 4vw, 3rem);
|
|
112
|
+
-webkit-font-smoothing: antialiased;
|
|
113
|
+
}
|
|
114
|
+
.wrap { max-width: 1040px; margin: 0 auto; }
|
|
115
|
+
|
|
116
|
+
/* Header ------------------------------------------------------------ */
|
|
117
|
+
.head { display: flex; flex-wrap: wrap; align-items: flex-end;
|
|
118
|
+
justify-content: space-between; gap: 1rem 1.5rem;
|
|
119
|
+
padding-bottom: 1.25rem; border-bottom: 1px solid var(--line-2); }
|
|
120
|
+
.eyebrow { font-size: .68rem; letter-spacing: .32em; text-transform: uppercase;
|
|
121
|
+
color: var(--dim); margin: 0 0 .55rem; }
|
|
122
|
+
.title { font-family: var(--sans); font-weight: 800; letter-spacing: -.02em;
|
|
123
|
+
font-size: clamp(1.7rem, 4.5vw, 2.7rem); line-height: 1; margin: 0; }
|
|
124
|
+
.title span { color: var(--on); }
|
|
125
|
+
.source { text-align: right; font-size: .74rem; color: var(--dim); }
|
|
126
|
+
.source b { color: var(--ink); font-weight: 500; }
|
|
127
|
+
.chip { display: inline-block; margin-top: .35rem; padding: .3rem .6rem;
|
|
128
|
+
border: 1px solid var(--line-2); border-radius: 3px; color: var(--ink);
|
|
129
|
+
background: rgba(122,182,214,.06); font-size: .74rem; }
|
|
130
|
+
|
|
131
|
+
/* Matrix ------------------------------------------------------------ */
|
|
132
|
+
.matrix { margin-top: 1.5rem; border: 1px solid var(--line-2);
|
|
133
|
+
border-radius: 6px; overflow: hidden; background: var(--field-2); }
|
|
134
|
+
.row { display: grid; grid-template-columns: minmax(200px, 1.4fr) repeat(3, 1fr); }
|
|
135
|
+
.row + .row { border-top: 1px solid var(--line); }
|
|
136
|
+
.colhead { padding: .85rem 1rem; border-left: 1px solid var(--line);
|
|
137
|
+
background: rgba(122,182,214,.05); }
|
|
138
|
+
.colhead:first-child { border-left: 0; background: transparent; }
|
|
139
|
+
.env { font-size: .72rem; letter-spacing: .18em; text-transform: uppercase;
|
|
140
|
+
color: var(--dim); display: flex; align-items: center; gap: .45rem; }
|
|
141
|
+
.env .lock { color: var(--gate); font-size: .8rem; }
|
|
142
|
+
.env .here { margin-left: auto; font-size: .58rem; letter-spacing: .12em;
|
|
143
|
+
color: var(--field); background: var(--on); border-radius: 2px;
|
|
144
|
+
padding: .1rem .3rem; }
|
|
145
|
+
.env .gate-note { color: var(--gate); }
|
|
146
|
+
.prov { font-size: .66rem; color: var(--faint); margin-top: .3rem; }
|
|
147
|
+
|
|
148
|
+
.cap { padding: 1rem; }
|
|
149
|
+
.cap .name { font-family: var(--sans); font-weight: 650; font-size: 1.02rem;
|
|
150
|
+
letter-spacing: -.01em; }
|
|
151
|
+
.cap .sub { color: var(--dim); font-size: .72rem; margin-top: .25rem; }
|
|
152
|
+
.cap .track { display: inline-block; margin-top: .55rem; font-size: .58rem;
|
|
153
|
+
letter-spacing: .14em; text-transform: uppercase; color: var(--dim);
|
|
154
|
+
border: 1px solid var(--line-2); border-radius: 999px; padding: .12rem .5rem; }
|
|
155
|
+
|
|
156
|
+
.cell { padding: 1rem; border-left: 1px solid var(--line);
|
|
157
|
+
display: flex; flex-direction: column; gap: .5rem; }
|
|
158
|
+
.cell.here { background: rgba(79,227,176,.045); }
|
|
159
|
+
.cell.diverge { background: var(--pending-bg); }
|
|
160
|
+
.pill { display: inline-flex; align-items: center; gap: .5rem;
|
|
161
|
+
font-size: .8rem; letter-spacing: .02em; }
|
|
162
|
+
.dot { width: .62rem; height: .62rem; border-radius: 50%; flex: none; }
|
|
163
|
+
.on .dot { background: var(--on); box-shadow: 0 0 0 4px var(--on-bg); }
|
|
164
|
+
.off .dot { background: transparent; border: 1.5px solid var(--off); }
|
|
165
|
+
.on { color: var(--on); }
|
|
166
|
+
.off { color: var(--off); }
|
|
167
|
+
.flag { font-size: .62rem; letter-spacing: .1em; text-transform: uppercase;
|
|
168
|
+
color: var(--pending); }
|
|
169
|
+
|
|
170
|
+
/* Flow strip -------------------------------------------------------- */
|
|
171
|
+
.flow { margin-top: 1.5rem; display: grid;
|
|
172
|
+
grid-template-columns: repeat(3, 1fr); gap: .75rem; }
|
|
173
|
+
.step { border: 1px solid var(--line); border-radius: 5px; padding: .9rem 1rem;
|
|
174
|
+
background: var(--field-2); position: relative; }
|
|
175
|
+
.step .n { font-size: .64rem; letter-spacing: .2em; color: var(--on);
|
|
176
|
+
text-transform: uppercase; }
|
|
177
|
+
.step .t { font-family: var(--sans); font-weight: 600; margin: .35rem 0 .3rem;
|
|
178
|
+
font-size: .96rem; }
|
|
179
|
+
.step .d { font-size: .74rem; color: var(--dim); line-height: 1.45; }
|
|
180
|
+
.step + .step::before { content: "→"; position: absolute; left: -.6rem;
|
|
181
|
+
top: 50%; transform: translateY(-50%); color: var(--faint); font-size: .8rem; }
|
|
182
|
+
|
|
183
|
+
.foot { margin-top: 1.4rem; font-size: .72rem; color: var(--faint);
|
|
184
|
+
line-height: 1.55; }
|
|
185
|
+
.foot b { color: var(--dim); font-weight: 500; }
|
|
186
|
+
.legend { display: flex; flex-wrap: wrap; gap: 1.1rem; margin-top: .7rem; }
|
|
187
|
+
.legend span { display: inline-flex; align-items: center; gap: .4rem; color: var(--dim); }
|
|
188
|
+
|
|
189
|
+
@media (max-width: 720px) {
|
|
190
|
+
.row { grid-template-columns: 1fr; }
|
|
191
|
+
.colhead, .cell { border-left: 0; border-top: 1px solid var(--line); }
|
|
192
|
+
.colhead:first-child { border-top: 0; }
|
|
193
|
+
.flow { grid-template-columns: 1fr; }
|
|
194
|
+
.step + .step::before { content: "↓"; left: 50%; top: -.65rem;
|
|
195
|
+
transform: translateX(-50%); }
|
|
196
|
+
}
|
|
197
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
198
|
+
`}></style>
|
|
199
|
+
</head>
|
|
200
|
+
<body>
|
|
201
|
+
<div class="wrap">
|
|
202
|
+
<header class="head">
|
|
203
|
+
<div>
|
|
204
|
+
<p class="eyebrow">Tenant capabilities</p>
|
|
205
|
+
<h1 class="title">{tenant.displayName} <span>·</span> control</h1>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="source">
|
|
208
|
+
source of truth<br />
|
|
209
|
+
<span class="chip">.tot/config.json → capabilities</span>
|
|
210
|
+
</div>
|
|
211
|
+
</header>
|
|
212
|
+
|
|
213
|
+
<div class="matrix" role="table" aria-label="Capabilities by environment">
|
|
214
|
+
<div class="row" role="row">
|
|
215
|
+
<div class="colhead" role="columnheader">
|
|
216
|
+
<div class="env">Capability</div>
|
|
217
|
+
<div class="prov">what it switches</div>
|
|
218
|
+
</div>
|
|
219
|
+
{ENVS.map((e) => (
|
|
220
|
+
<div class="colhead" role="columnheader">
|
|
221
|
+
<div class="env">
|
|
222
|
+
{e.label}
|
|
223
|
+
{e.gated && <span class="lock" title="promote-gated">🔒</span>}
|
|
224
|
+
{hereKey === e.key && <span class="here">you are here</span>}
|
|
225
|
+
</div>
|
|
226
|
+
<div class="prov">
|
|
227
|
+
{e.prov}{e.gated ? " · evidence-gated" : ""}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{CAPS.map((c) => (
|
|
234
|
+
<div class="row" role="row">
|
|
235
|
+
<div class="cap" role="rowheader">
|
|
236
|
+
<div class="name">{c.name}</div>
|
|
237
|
+
<div class="sub">{c.sub}</div>
|
|
238
|
+
<span class="track">{c.track}</span>
|
|
239
|
+
</div>
|
|
240
|
+
{ENVS.map((e) => {
|
|
241
|
+
const enabled = on(e.caps, c.key);
|
|
242
|
+
const div = !e.gated && diverges(e.caps, c.key);
|
|
243
|
+
return (
|
|
244
|
+
<div
|
|
245
|
+
class:list={["cell", { here: hereKey === e.key, diverge: div }]}
|
|
246
|
+
role="cell"
|
|
247
|
+
>
|
|
248
|
+
<span class:list={["pill", enabled ? "on" : "off"]}>
|
|
249
|
+
<span class="dot"></span>{enabled ? "Enabled" : "Disabled"}
|
|
250
|
+
</span>
|
|
251
|
+
{div && <span class="flag">⚠ pending promote</span>}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div class="flow">
|
|
260
|
+
<div class="step">
|
|
261
|
+
<div class="n">01 · edit</div>
|
|
262
|
+
<div class="t">Change the switch</div>
|
|
263
|
+
<div class="d">Flip a capability in <b style="color:var(--ink)">.tot/config.json</b> in the tenant repo. Locally it takes effect on save (HMR).</div>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="step">
|
|
266
|
+
<div class="n">02 · preview</div>
|
|
267
|
+
<div class="t">Open a PR</div>
|
|
268
|
+
<div class="d">The PR head reconciles to the <b style="color:var(--ink)">test</b> pointer — Preview shows the new state on its own URL, per PR.</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="step">
|
|
271
|
+
<div class="n">03 · promote</div>
|
|
272
|
+
<div class="t">Merge to go live</div>
|
|
273
|
+
<div class="d">Accepting the PR promotes to the <b style="color:var(--ink)">live</b> pointer — the evidence/compliance gate hangs here.</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<p class="foot">
|
|
278
|
+
Each environment is pinned independently: Live keeps its promoted state until a PR is
|
|
279
|
+
accepted, so a shared-environment change is a PR by construction.
|
|
280
|
+
{anyDivergence
|
|
281
|
+
? " Amber cells mark a capability that differs from Live — a change in flight, not yet promoted."
|
|
282
|
+
: " No capability currently diverges from Live."}
|
|
283
|
+
<span class="legend">
|
|
284
|
+
<span><span class="dot" style="background:var(--on);box-shadow:0 0 0 4px var(--on-bg)"></span> enabled</span>
|
|
285
|
+
<span><span class="dot" style="border:1.5px solid var(--off)"></span> disabled</span>
|
|
286
|
+
<span style="color:var(--pending)">⚠ pending promote</span>
|
|
287
|
+
<span style="color:var(--gate)">🔒 promote-gated</span>
|
|
288
|
+
</span>
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
</body>
|
|
292
|
+
</html>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PLP / collection page (brief §5). Responsive grid, collection hero, sort,
|
|
4
|
+
* URL-addressable facets, numbered pagination + load-more. Product data from
|
|
5
|
+
* ToT (active-only); hero copy from the collection record (Storyblok could
|
|
6
|
+
* override later — documented seam).
|
|
7
|
+
*/
|
|
8
|
+
import Layout from "@/layouts/Layout.astro";
|
|
9
|
+
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
10
|
+
import ProductCard from "@/components/ProductCard.astro";
|
|
11
|
+
import EmptyState from "@/components/EmptyState.astro";
|
|
12
|
+
import FacetSidebar from "@/components/plp/FacetSidebar.astro";
|
|
13
|
+
import ActiveFilters from "@/components/plp/ActiveFilters.astro";
|
|
14
|
+
import SortSelect from "@/components/plp/SortSelect.astro";
|
|
15
|
+
import Pagination from "@/components/plp/Pagination.astro";
|
|
16
|
+
import Img from "@/components/Img.astro";
|
|
17
|
+
import { parseQuery } from "@/lib/url";
|
|
18
|
+
import { breadcrumbLd, itemListLd } from "@/lib/jsonld";
|
|
19
|
+
import { truncate } from "@/lib/format";
|
|
20
|
+
import { withBase } from "@/lib/basePath";
|
|
21
|
+
|
|
22
|
+
const { handle } = Astro.params;
|
|
23
|
+
const { tenant, tot, canonicalBase } = Astro.locals;
|
|
24
|
+
// Commerce-only route: marketing tenants have no catalog surface.
|
|
25
|
+
if (!Astro.locals.features.catalog) return new Response(null, { status: 404 });
|
|
26
|
+
if (!handle) return new Response(null, { status: 404 });
|
|
27
|
+
|
|
28
|
+
const collection = await tot.getCollectionByHandle(handle);
|
|
29
|
+
if (!collection) return new Response(null, { status: 404 });
|
|
30
|
+
|
|
31
|
+
const url = new URL(Astro.request.url);
|
|
32
|
+
const query = parseQuery(url);
|
|
33
|
+
query.collection = handle;
|
|
34
|
+
if (!query.pageSize) query.pageSize = 9;
|
|
35
|
+
|
|
36
|
+
const { products, page, pageSize, total, facets } = await tot.listProducts(query);
|
|
37
|
+
const basePath = `/collections/${handle}`;
|
|
38
|
+
// Tenant-prefixed page path for facet/sort/pagination, which build links by
|
|
39
|
+
// string-concatenating onto this base (so they stay inside a path-routed
|
|
40
|
+
// tenant). `basePath` itself stays unprefixed for the SEO canonical `path`,
|
|
41
|
+
// breadcrumbs, and EmptyState — those prefix on their own via withBase.
|
|
42
|
+
const linkBase = withBase(Astro.locals.basePath, basePath);
|
|
43
|
+
|
|
44
|
+
const crumbs = [
|
|
45
|
+
{ label: tenant.displayName, href: "/" },
|
|
46
|
+
{ label: collection.title, href: basePath },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const heroImg = collection.image;
|
|
50
|
+
const desc = collection.description_text || collection.description_html?.replace(/<[^>]+>/g, "");
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<Layout
|
|
54
|
+
title={collection.seo.title || collection.title}
|
|
55
|
+
description={collection.seo.description || (desc ? truncate(desc, 160) : `Shop ${collection.title} at ${tenant.displayName}.`)}
|
|
56
|
+
path={basePath}
|
|
57
|
+
jsonLd={[
|
|
58
|
+
breadcrumbLd(crumbs, canonicalBase),
|
|
59
|
+
itemListLd(products, canonicalBase, (page - 1) * pageSize),
|
|
60
|
+
]}
|
|
61
|
+
>
|
|
62
|
+
{/* Collection hero */}
|
|
63
|
+
<header class="bg-[var(--color-primary)] text-[var(--color-primary-contrast)]">
|
|
64
|
+
<div class="container-prose grid gap-6 py-[clamp(2.5rem,6vw,4.5rem)] md:grid-cols-[1fr_auto] md:items-end">
|
|
65
|
+
<div data-reveal>
|
|
66
|
+
<p class="eyebrow mb-3 !text-[color-mix(in_srgb,var(--color-primary-contrast)_70%,transparent)]">
|
|
67
|
+
Collection
|
|
68
|
+
</p>
|
|
69
|
+
<h1 class="display-xl">
|
|
70
|
+
{collection.title}
|
|
71
|
+
</h1>
|
|
72
|
+
{desc && (
|
|
73
|
+
<p class="mt-4 max-w-prose text-[1.05rem] text-[color-mix(in_srgb,var(--color-primary-contrast)_82%,transparent)]">
|
|
74
|
+
{truncate(desc, 220)}
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{heroImg && (
|
|
79
|
+
<div class="hidden w-48 overflow-hidden rounded-[var(--radius-md)] md:block" data-reveal>
|
|
80
|
+
<Img
|
|
81
|
+
src={heroImg.url}
|
|
82
|
+
rehosted={heroImg.rehosted}
|
|
83
|
+
seed={heroImg.id ?? collection.handle}
|
|
84
|
+
label={collection.title}
|
|
85
|
+
alt={heroImg.alt ?? collection.title}
|
|
86
|
+
width={heroImg.width ?? 400}
|
|
87
|
+
height={heroImg.height ?? 300}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</header>
|
|
93
|
+
|
|
94
|
+
<div class="container-prose py-4">
|
|
95
|
+
<Breadcrumbs crumbs={crumbs} />
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="container-prose grid gap-10 pb-16 lg:grid-cols-[230px_1fr]">
|
|
99
|
+
{/* Facets — sticky on desktop; a <details> drawer on mobile. */}
|
|
100
|
+
<div class="lg:sticky lg:top-24 lg:self-start">
|
|
101
|
+
<details class="lg:hidden">
|
|
102
|
+
<summary class="cursor-pointer rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-2.5 font-display text-sm font-medium">
|
|
103
|
+
Filters & sort
|
|
104
|
+
</summary>
|
|
105
|
+
<div class="mt-4">
|
|
106
|
+
<FacetSidebar facets={facets} query={query} basePath={linkBase} />
|
|
107
|
+
</div>
|
|
108
|
+
</details>
|
|
109
|
+
<div class="hidden lg:block">
|
|
110
|
+
<FacetSidebar facets={facets} query={query} basePath={linkBase} />
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div>
|
|
115
|
+
<div class="mb-5 flex flex-wrap items-center justify-between gap-3 border-b border-[var(--color-border)] pb-4">
|
|
116
|
+
<p class="text-sm text-[var(--color-muted)]">
|
|
117
|
+
{total} {total === 1 ? "item" : "items"}
|
|
118
|
+
</p>
|
|
119
|
+
<SortSelect query={query} basePath={linkBase} />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<ActiveFilters query={query} basePath={linkBase} />
|
|
123
|
+
|
|
124
|
+
{products.length === 0 ? (
|
|
125
|
+
<EmptyState
|
|
126
|
+
title="Nothing matches — yet"
|
|
127
|
+
body="No pieces fit these filters. Loosen a filter or clear them to see the full collection."
|
|
128
|
+
ctaLabel="Clear filters"
|
|
129
|
+
ctaHref={basePath}
|
|
130
|
+
/>
|
|
131
|
+
) : (
|
|
132
|
+
<>
|
|
133
|
+
<div class="grid grid-cols-2 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
134
|
+
{products.map((p, i) => (
|
|
135
|
+
<ProductCard
|
|
136
|
+
product={p}
|
|
137
|
+
position={(page - 1) * pageSize + i}
|
|
138
|
+
list={collection.handle}
|
|
139
|
+
priority={i < 3 && page === 1}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
<Pagination
|
|
144
|
+
query={query}
|
|
145
|
+
basePath={linkBase}
|
|
146
|
+
page={page}
|
|
147
|
+
pageSize={pageSize}
|
|
148
|
+
total={total}
|
|
149
|
+
/>
|
|
150
|
+
</>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</Layout>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /collections — the collections directory. Hero + grid of CollectionCards,
|
|
4
|
+
* one per collection (product counts from the collection record's handles).
|
|
5
|
+
*/
|
|
6
|
+
import Layout from "@/layouts/Layout.astro";
|
|
7
|
+
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
8
|
+
import CollectionCard from "@/components/CollectionCard.astro";
|
|
9
|
+
import EmptyState from "@/components/EmptyState.astro";
|
|
10
|
+
import { breadcrumbLd } from "@/lib/jsonld";
|
|
11
|
+
|
|
12
|
+
const { tenant, tot, canonicalBase } = Astro.locals;
|
|
13
|
+
// Commerce-only route: marketing tenants have no catalog surface.
|
|
14
|
+
if (!Astro.locals.features.catalog) return new Response(null, { status: 404 });
|
|
15
|
+
const collections = await tot.listCollections();
|
|
16
|
+
|
|
17
|
+
const crumbs = [
|
|
18
|
+
{ label: tenant.displayName, href: "/" },
|
|
19
|
+
{ label: "Collections", href: "/collections" },
|
|
20
|
+
];
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<Layout
|
|
24
|
+
title="Collections"
|
|
25
|
+
description={`Browse every collection at ${tenant.displayName}.`}
|
|
26
|
+
path="/collections"
|
|
27
|
+
jsonLd={breadcrumbLd(crumbs, canonicalBase)}
|
|
28
|
+
>
|
|
29
|
+
<div class="container-prose pt-6">
|
|
30
|
+
<Breadcrumbs crumbs={crumbs} />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<header class="container-prose pb-2 pt-8">
|
|
34
|
+
<p class="eyebrow eyebrow--tick mb-3">The range</p>
|
|
35
|
+
<h1 class="display-xl">
|
|
36
|
+
Collections
|
|
37
|
+
</h1>
|
|
38
|
+
<p class="mt-5 max-w-[60ch] text-[1.1rem] text-[var(--color-muted)]">
|
|
39
|
+
Small, considered groupings — each one a way into the catalog.
|
|
40
|
+
</p>
|
|
41
|
+
</header>
|
|
42
|
+
|
|
43
|
+
<div class="container-prose pb-[clamp(3rem,7vw,6rem)] pt-8">
|
|
44
|
+
{collections.length === 0 ? (
|
|
45
|
+
<EmptyState
|
|
46
|
+
title="Nothing here yet"
|
|
47
|
+
body="Collections are on their way. In the meantime, browse the full catalog."
|
|
48
|
+
ctaLabel="Back home"
|
|
49
|
+
ctaHref="/"
|
|
50
|
+
/>
|
|
51
|
+
) : (
|
|
52
|
+
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
53
|
+
{collections.map((c) => (
|
|
54
|
+
<CollectionCard collection={c} productCount={c.product_handles.length} />
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
</Layout>
|