@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,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Header> — sticky site header: optional announcement bar, wordmark logo,
|
|
4
|
+
* desktop nav with CSS-only mega-menus (focus-within / group-hover, no JS),
|
|
5
|
+
* search typeahead island, and a mobile hamburger that mounts the MobileNav
|
|
6
|
+
* island. Theme-agnostic (CSS-var-backed utilities only).
|
|
7
|
+
*/
|
|
8
|
+
import type { SiteChrome } from "@/lib/storyblok/content-model";
|
|
9
|
+
import SearchTypeahead from "@/components/islands/SearchTypeahead.tsx";
|
|
10
|
+
import MobileNav from "@/components/islands/MobileNav.tsx";
|
|
11
|
+
import { mergeTheme } from "@/themes/schema";
|
|
12
|
+
import { referenceTheme } from "@/themes/reference";
|
|
13
|
+
import { withBase } from "@/lib/basePath";
|
|
14
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
chrome: SiteChrome;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { chrome } = Astro.props;
|
|
21
|
+
const { tenant, basePath, features } = Astro.locals;
|
|
22
|
+
|
|
23
|
+
// Simple, always-visible auth entry point: "Dashboard" when a session exists,
|
|
24
|
+
// "Sign in" otherwise. Without this there's no discoverable path from the
|
|
25
|
+
// storefront into sign-in / the vendor dashboard (#8425). On login-gated hosts
|
|
26
|
+
// the middleware already resolved the viewer (locals.viewer); reuse it and only
|
|
27
|
+
// read the session KV on open hosts where the gate never ran.
|
|
28
|
+
const viewer = Astro.locals.viewer ?? (await readViewerSession(Astro));
|
|
29
|
+
// Signed-out "Sign in" carries the current path as `next`, so after the email-code
|
|
30
|
+
// round-trip the visitor lands back where they were (e.g. the gated tenant page
|
|
31
|
+
// they were trying to reach) instead of the site root.
|
|
32
|
+
const nextParam = encodeURIComponent(Astro.url.pathname + Astro.url.search);
|
|
33
|
+
const authHref = viewer ? "/dashboard" : `/auth/login?next=${nextParam}`;
|
|
34
|
+
const authLabel = viewer ? "Dashboard" : "Sign in";
|
|
35
|
+
|
|
36
|
+
// Marketing tenants swap the commerce search/saved controls for conversion CTAs.
|
|
37
|
+
const headerCtas = chrome.headerCtas ?? [];
|
|
38
|
+
const ctaClass = (variant?: string) =>
|
|
39
|
+
variant === "secondary" || variant === "ghost"
|
|
40
|
+
? "inline-flex items-center rounded-full border border-current px-4 py-2 font-display text-sm font-medium transition-opacity hover:opacity-80"
|
|
41
|
+
: "inline-flex items-center rounded-full bg-accent px-4 py-2 font-display text-sm font-medium text-[var(--color-primary-contrast)] transition-transform hover:-translate-y-0.5";
|
|
42
|
+
|
|
43
|
+
// Nav handed to the (client-rendered) MobileNav island with hrefs pre-prefixed,
|
|
44
|
+
// so it doesn't depend on the runtime base path inside the island.
|
|
45
|
+
const mobileNav = chrome.nav.map((item) => ({
|
|
46
|
+
...item,
|
|
47
|
+
href: withBase(basePath, item.href),
|
|
48
|
+
children: item.children?.map((child) => ({
|
|
49
|
+
...child,
|
|
50
|
+
href: withBase(basePath, child.href),
|
|
51
|
+
})),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Per-vendor header treatment from the theme file: a "dark" header renders the
|
|
55
|
+
// bar in the primary color carrying the light logo (e.g. Giant Vapes' navy +
|
|
56
|
+
// white logo); otherwise a light bar with the wordmark.
|
|
57
|
+
const brand = mergeTheme(referenceTheme, tenant.theme_tokens).brand;
|
|
58
|
+
const dark = brand.headerVariant === "dark";
|
|
59
|
+
const logo = dark ? brand.logoLight : brand.logoDark;
|
|
60
|
+
const showLogo = dark && !!logo && /^(https?:|\/)/.test(logo);
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
<header class="sticky top-0 z-40">
|
|
64
|
+
{
|
|
65
|
+
chrome.announcement && (
|
|
66
|
+
<div class="bg-primary text-primary-contrast">
|
|
67
|
+
<p class="container-prose py-1.5 text-center font-mono text-[0.72rem] tracking-[0.08em]">
|
|
68
|
+
{chrome.announcement}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
class:list={[
|
|
76
|
+
"border-b backdrop-blur",
|
|
77
|
+
dark ? "border-[color-mix(in_srgb,var(--color-primary-contrast)_18%,var(--color-primary))] bg-primary text-primary-contrast" : "border-border bg-surface/95 text-text",
|
|
78
|
+
]}
|
|
79
|
+
>
|
|
80
|
+
<div class="container-prose flex h-16 items-center justify-between gap-4">
|
|
81
|
+
<!-- Logo / wordmark -->
|
|
82
|
+
<a
|
|
83
|
+
href={withBase(basePath, "/")}
|
|
84
|
+
class="flex shrink-0 items-center transition-opacity hover:opacity-80"
|
|
85
|
+
aria-label={tenant.displayName}
|
|
86
|
+
>
|
|
87
|
+
{showLogo ? (
|
|
88
|
+
<img src={logo} alt={tenant.displayName} class="h-10 w-auto max-w-[160px] object-contain" width="160" height="64" />
|
|
89
|
+
) : (
|
|
90
|
+
<span class="font-display text-lg font-bold tracking-tight">{tenant.displayName}</span>
|
|
91
|
+
)}
|
|
92
|
+
</a>
|
|
93
|
+
|
|
94
|
+
<!-- Desktop nav -->
|
|
95
|
+
<nav aria-label="Primary" class="hidden md:block">
|
|
96
|
+
<ul class="flex items-center gap-7">
|
|
97
|
+
{
|
|
98
|
+
chrome.nav.map((item) =>
|
|
99
|
+
item.children && item.children.length > 0 ? (
|
|
100
|
+
<li class="group relative">
|
|
101
|
+
<a
|
|
102
|
+
href={withBase(basePath, item.href)}
|
|
103
|
+
class="inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent group-focus-within:text-accent"
|
|
104
|
+
aria-haspopup="true"
|
|
105
|
+
>
|
|
106
|
+
{item.label}
|
|
107
|
+
<span aria-hidden="true" class="text-[0.7em] opacity-60">
|
|
108
|
+
▾
|
|
109
|
+
</span>
|
|
110
|
+
</a>
|
|
111
|
+
<div class="invisible absolute left-0 top-full z-50 min-w-56 translate-y-1 opacity-0 transition-all duration-150 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100">
|
|
112
|
+
<ul class="mt-1 grid gap-0.5 rounded-md border border-border bg-surface p-3 shadow-md">
|
|
113
|
+
{item.children.map((child) => (
|
|
114
|
+
<li>
|
|
115
|
+
<a
|
|
116
|
+
href={withBase(basePath, child.href)}
|
|
117
|
+
class="block rounded-sm px-3 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
118
|
+
>
|
|
119
|
+
{child.label}
|
|
120
|
+
</a>
|
|
121
|
+
</li>
|
|
122
|
+
))}
|
|
123
|
+
</ul>
|
|
124
|
+
</div>
|
|
125
|
+
</li>
|
|
126
|
+
) : (
|
|
127
|
+
<li>
|
|
128
|
+
<a
|
|
129
|
+
href={withBase(basePath, item.href)}
|
|
130
|
+
class="py-2 text-sm transition-colors hover:text-accent"
|
|
131
|
+
>
|
|
132
|
+
{item.label}
|
|
133
|
+
</a>
|
|
134
|
+
</li>
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
</ul>
|
|
139
|
+
</nav>
|
|
140
|
+
|
|
141
|
+
<!-- Right cluster: search + saved (commerce) or CTAs (marketing) + mobile menu -->
|
|
142
|
+
<div class="flex flex-1 items-center justify-end gap-2 md:max-w-sm">
|
|
143
|
+
{features.productSearch && <SearchTypeahead client:idle />}
|
|
144
|
+
{
|
|
145
|
+
features.savedItems && (
|
|
146
|
+
<a
|
|
147
|
+
href={withBase(basePath, "/saved")}
|
|
148
|
+
class="relative grid h-9 w-9 shrink-0 place-items-center rounded-full transition-colors hover:text-accent"
|
|
149
|
+
aria-label="Saved items"
|
|
150
|
+
>
|
|
151
|
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
|
152
|
+
<path d="M12 21s-7.5-4.6-10-9.2C.4 8.6 2 5 5.5 5 8 5 9.4 6.6 12 9c2.6-2.4 4-4 6.5-4C22 5 23.6 8.6 22 11.8 19.5 16.4 12 21 12 21z" />
|
|
153
|
+
</svg>
|
|
154
|
+
<span
|
|
155
|
+
data-wishlist-count
|
|
156
|
+
hidden
|
|
157
|
+
class="absolute -right-0.5 -top-0.5 grid h-4 min-w-4 place-items-center rounded-full bg-[var(--color-sale)] px-1 font-mono text-[0.6rem] font-medium text-[var(--color-primary-contrast)]"
|
|
158
|
+
/>
|
|
159
|
+
</a>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
{
|
|
163
|
+
headerCtas.length > 0 && (
|
|
164
|
+
<div class="hidden items-center gap-2 sm:flex">
|
|
165
|
+
{headerCtas.map((cta) => (
|
|
166
|
+
<a href={withBase(basePath, cta.href)} class={ctaClass(cta.variant)}>
|
|
167
|
+
{cta.label}
|
|
168
|
+
</a>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
<!-- Auth entry point: Sign in / Dashboard (#8425) -->
|
|
174
|
+
<a
|
|
175
|
+
href={authHref}
|
|
176
|
+
class="inline-flex shrink-0 items-center rounded-full border border-current px-4 py-2 font-display text-sm font-medium transition-opacity hover:opacity-80"
|
|
177
|
+
>
|
|
178
|
+
{authLabel}
|
|
179
|
+
</a>
|
|
180
|
+
<div class="md:hidden">
|
|
181
|
+
<MobileNav
|
|
182
|
+
client:idle
|
|
183
|
+
nav={mobileNav}
|
|
184
|
+
announcement={chrome.announcement}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</header>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Active-filter chips (Shopify-style). Shows what's applied as removable pills
|
|
4
|
+
* above the grid — each chip is a link that drops exactly one filter, so it's
|
|
5
|
+
* URL-addressable and back-button safe like the rest of faceting (src/lib/url.ts).
|
|
6
|
+
*/
|
|
7
|
+
import type { ListProductsQuery } from "@tot/public-runtime";
|
|
8
|
+
import { toggleFacet, buildQueryString } from "@/lib/url";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
query: ListProductsQuery;
|
|
12
|
+
basePath: string;
|
|
13
|
+
}
|
|
14
|
+
const { query, basePath } = Astro.props;
|
|
15
|
+
|
|
16
|
+
type Chip = { label: string; href: string };
|
|
17
|
+
const chips: Chip[] = [];
|
|
18
|
+
|
|
19
|
+
// Option + tag facet values (toggling an active value removes it).
|
|
20
|
+
for (const [name, values] of Object.entries(query.filters ?? {})) {
|
|
21
|
+
for (const v of values) {
|
|
22
|
+
chips.push({
|
|
23
|
+
label: name === "tags" ? `#${v}` : `${name}: ${v}`,
|
|
24
|
+
href: basePath + toggleFacet(query, name, v),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Price range.
|
|
30
|
+
if (query.priceMin != null || query.priceMax != null) {
|
|
31
|
+
const lo = query.priceMin != null ? `$${query.priceMin}` : "$0";
|
|
32
|
+
const hi = query.priceMax != null ? `$${query.priceMax}` : "any";
|
|
33
|
+
const { priceMin, priceMax, ...rest } = query;
|
|
34
|
+
chips.push({ label: `${lo}–${hi}`, href: basePath + buildQueryString(rest) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Availability (the meaningful filter is "in-stock"; "all" is the default).
|
|
38
|
+
if (query.availability === "in-stock") {
|
|
39
|
+
const { availability, ...rest } = query;
|
|
40
|
+
chips.push({ label: "In stock", href: basePath + buildQueryString(rest) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const clearHref =
|
|
44
|
+
basePath +
|
|
45
|
+
buildQueryString({ sort: query.sort, pageSize: query.pageSize });
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
chips.length > 0 && (
|
|
50
|
+
<div class="mb-5 flex flex-wrap items-center gap-2" data-active-filters>
|
|
51
|
+
<span class="sr-only">Active filters:</span>
|
|
52
|
+
{chips.map((chip) => (
|
|
53
|
+
<a
|
|
54
|
+
href={chip.href}
|
|
55
|
+
rel="nofollow"
|
|
56
|
+
class="group inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] py-1 pl-3 pr-2 text-xs transition-colors hover:border-[var(--color-accent-ink)] hover:text-[var(--color-accent-ink)]"
|
|
57
|
+
>
|
|
58
|
+
<span>{chip.label}</span>
|
|
59
|
+
<span
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
class="grid h-4 w-4 place-items-center rounded-full text-[var(--color-muted)] transition-colors group-hover:bg-[var(--color-accent-ink)] group-hover:text-[var(--color-primary-contrast)]"
|
|
62
|
+
>
|
|
63
|
+
×
|
|
64
|
+
</span>
|
|
65
|
+
<span class="sr-only">Remove filter</span>
|
|
66
|
+
</a>
|
|
67
|
+
))}
|
|
68
|
+
{chips.length > 1 && (
|
|
69
|
+
<a
|
|
70
|
+
href={clearHref}
|
|
71
|
+
rel="nofollow"
|
|
72
|
+
class="ml-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]"
|
|
73
|
+
>
|
|
74
|
+
Clear all
|
|
75
|
+
</a>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Faceted filtering UI (brief §5). URL-ADDRESSABLE and back-button safe: every
|
|
4
|
+
* facet is a real link that toggles a querystring param (no JS required for
|
|
5
|
+
* filtering). The optional mobile drawer toggle is the only island.
|
|
6
|
+
*
|
|
7
|
+
* Encoding handled by src/lib/url.ts (opt.<Name>=<value>, tag=, price, avail).
|
|
8
|
+
*/
|
|
9
|
+
import type { Facet, ListProductsQuery } from "@tot/public-runtime";
|
|
10
|
+
import { toggleFacet, buildQueryString } from "@/lib/url";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
facets: Facet[];
|
|
14
|
+
query: ListProductsQuery;
|
|
15
|
+
basePath: string; // e.g. /collections/bestsellers
|
|
16
|
+
}
|
|
17
|
+
const { facets, query, basePath } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const selected = (name: string, value: string) =>
|
|
20
|
+
(query.filters?.[name] ?? []).includes(value);
|
|
21
|
+
|
|
22
|
+
// Order facets: option facets first, then tags, availability last.
|
|
23
|
+
const ordered = [...facets].sort((a, b) => {
|
|
24
|
+
const rank = (n: string) => (n === "availability" ? 2 : n === "tags" ? 1 : 0);
|
|
25
|
+
return rank(a.name) - rank(b.name);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const hasActiveFilters =
|
|
29
|
+
!!query.filters && Object.keys(query.filters).length > 0;
|
|
30
|
+
|
|
31
|
+
const clearHref =
|
|
32
|
+
basePath +
|
|
33
|
+
buildQueryString({ sort: query.sort, q: query.q, pageSize: query.pageSize });
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<aside aria-label="Filters" class="text-sm">
|
|
37
|
+
<div class="mb-4 flex items-center justify-between">
|
|
38
|
+
<h2 class="eyebrow !text-[var(--color-text)]">Filter</h2>
|
|
39
|
+
{hasActiveFilters && (
|
|
40
|
+
<a href={clearHref} class="text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]">
|
|
41
|
+
Clear all
|
|
42
|
+
</a>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="space-y-7">
|
|
47
|
+
{
|
|
48
|
+
ordered.map((facet) => (
|
|
49
|
+
<fieldset>
|
|
50
|
+
<legend class="mb-2.5 font-display text-[0.95rem] font-medium">
|
|
51
|
+
{facet.name === "tags"
|
|
52
|
+
? "Tags"
|
|
53
|
+
: facet.name === "availability"
|
|
54
|
+
? "Availability"
|
|
55
|
+
: facet.name}
|
|
56
|
+
</legend>
|
|
57
|
+
<ul class="space-y-1.5">
|
|
58
|
+
{facet.values.map((fv) => {
|
|
59
|
+
const isSel = selected(facet.name, fv.value);
|
|
60
|
+
const href =
|
|
61
|
+
basePath + toggleFacet(query, facet.name, fv.value);
|
|
62
|
+
return (
|
|
63
|
+
<li>
|
|
64
|
+
<a
|
|
65
|
+
href={href}
|
|
66
|
+
rel="nofollow"
|
|
67
|
+
aria-pressed={isSel}
|
|
68
|
+
class:list={[
|
|
69
|
+
"flex items-center justify-between gap-3 rounded-[var(--radius-sm)] px-2 py-1 transition-colors",
|
|
70
|
+
isSel
|
|
71
|
+
? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
|
|
72
|
+
: "hover:bg-[color-mix(in_srgb,var(--color-primary)_8%,transparent)]",
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
<span class="flex items-center gap-2">
|
|
76
|
+
<span
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
class:list={[
|
|
79
|
+
"grid h-3.5 w-3.5 place-items-center rounded-[2px] border",
|
|
80
|
+
isSel
|
|
81
|
+
? "border-[var(--color-primary-contrast)] bg-[var(--color-primary-contrast)] text-[var(--color-primary)]"
|
|
82
|
+
: "border-[var(--color-border)]",
|
|
83
|
+
]}
|
|
84
|
+
>
|
|
85
|
+
{isSel ? "✓" : ""}
|
|
86
|
+
</span>
|
|
87
|
+
{fv.value === "all" ? "All items" : fv.value}
|
|
88
|
+
</span>
|
|
89
|
+
<span class:list={["text-xs", isSel ? "opacity-80" : "text-[var(--color-muted)]"]}>
|
|
90
|
+
{fv.count}
|
|
91
|
+
</span>
|
|
92
|
+
</a>
|
|
93
|
+
</li>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</ul>
|
|
97
|
+
</fieldset>
|
|
98
|
+
))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
{/* Price range — GET form, preserves other params via hidden inputs. */}
|
|
102
|
+
<form method="get" action={basePath} class="border-t border-[var(--color-border)] pt-5">
|
|
103
|
+
<fieldset>
|
|
104
|
+
<legend class="mb-2.5 font-display text-[0.95rem] font-medium">Price</legend>
|
|
105
|
+
{query.sort && <input type="hidden" name="sort" value={query.sort} />}
|
|
106
|
+
{query.availability && query.availability !== "all" && (
|
|
107
|
+
<input type="hidden" name="availability" value={query.availability} />
|
|
108
|
+
)}
|
|
109
|
+
{Object.entries(query.filters ?? {}).flatMap(([name, values]) =>
|
|
110
|
+
values.map((v) => (
|
|
111
|
+
<input type="hidden" name={name === "tags" ? "tag" : `opt.${name}`} value={v} />
|
|
112
|
+
)),
|
|
113
|
+
)}
|
|
114
|
+
<div class="flex items-center gap-2">
|
|
115
|
+
<label class="sr-only" for="priceMin">Minimum price</label>
|
|
116
|
+
<input
|
|
117
|
+
id="priceMin"
|
|
118
|
+
type="number"
|
|
119
|
+
name="priceMin"
|
|
120
|
+
min="0"
|
|
121
|
+
placeholder="Min"
|
|
122
|
+
value={query.priceMin ?? ""}
|
|
123
|
+
class="w-20 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1"
|
|
124
|
+
/>
|
|
125
|
+
<span aria-hidden="true" class="text-[var(--color-muted)]">–</span>
|
|
126
|
+
<label class="sr-only" for="priceMax">Maximum price</label>
|
|
127
|
+
<input
|
|
128
|
+
id="priceMax"
|
|
129
|
+
type="number"
|
|
130
|
+
name="priceMax"
|
|
131
|
+
min="0"
|
|
132
|
+
placeholder="Max"
|
|
133
|
+
value={query.priceMax ?? ""}
|
|
134
|
+
class="w-20 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1"
|
|
135
|
+
/>
|
|
136
|
+
<button
|
|
137
|
+
type="submit"
|
|
138
|
+
class="rounded-[var(--radius-sm)] bg-[var(--color-primary)] px-3 py-1 text-xs font-medium text-[var(--color-primary-contrast)]"
|
|
139
|
+
>
|
|
140
|
+
Go
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</fieldset>
|
|
144
|
+
</form>
|
|
145
|
+
</div>
|
|
146
|
+
</aside>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Numbered pagination (SEO-friendly, brief §5 default) + a "Load more" link.
|
|
4
|
+
* Both are plain anchors carrying the full query, so they're crawlable and
|
|
5
|
+
* back-button safe. "Load more" simply links to the next page; with JS it could
|
|
6
|
+
* be progressively enhanced to append, but the no-JS path stays correct.
|
|
7
|
+
*/
|
|
8
|
+
import type { ListProductsQuery } from "@tot/public-runtime";
|
|
9
|
+
import { buildQueryString } from "@/lib/url";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
query: ListProductsQuery;
|
|
13
|
+
basePath: string;
|
|
14
|
+
page: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
const { query, basePath, page, pageSize, total } = Astro.props;
|
|
19
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
20
|
+
if (pageCount <= 1) {
|
|
21
|
+
// nothing to render
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const href = (p: number) => basePath + buildQueryString(query, { page: p });
|
|
25
|
+
|
|
26
|
+
// Compact page-number window.
|
|
27
|
+
const windowSize = 5;
|
|
28
|
+
let start = Math.max(1, page - Math.floor(windowSize / 2));
|
|
29
|
+
let end = Math.min(pageCount, start + windowSize - 1);
|
|
30
|
+
start = Math.max(1, end - windowSize + 1);
|
|
31
|
+
const pages = Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
32
|
+
const hasNext = page < pageCount;
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
{pageCount > 1 && (
|
|
36
|
+
<nav aria-label="Pagination" class="mt-12 flex flex-col items-center gap-6">
|
|
37
|
+
{hasNext && (
|
|
38
|
+
<a
|
|
39
|
+
href={href(page + 1)}
|
|
40
|
+
rel="next"
|
|
41
|
+
class="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-8 py-3 font-display text-sm font-medium transition-colors hover:border-[var(--color-accent)] hover:text-[var(--color-accent-ink)]"
|
|
42
|
+
>
|
|
43
|
+
Load more
|
|
44
|
+
</a>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<ul class="flex items-center gap-1">
|
|
48
|
+
<li>
|
|
49
|
+
<a
|
|
50
|
+
href={href(Math.max(1, page - 1))}
|
|
51
|
+
rel="prev"
|
|
52
|
+
aria-label="Previous page"
|
|
53
|
+
aria-disabled={page === 1}
|
|
54
|
+
class:list={[
|
|
55
|
+
"grid h-9 w-9 place-items-center rounded-[var(--radius-sm)]",
|
|
56
|
+
page === 1 ? "pointer-events-none text-[var(--color-muted)] opacity-40" : "hover:bg-[var(--color-surface)]",
|
|
57
|
+
]}
|
|
58
|
+
>‹</a>
|
|
59
|
+
</li>
|
|
60
|
+
{start > 1 && (
|
|
61
|
+
<>
|
|
62
|
+
<li><a href={href(1)} class="grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] hover:bg-[var(--color-surface)]">1</a></li>
|
|
63
|
+
{start > 2 && <li aria-hidden="true" class="px-1 text-[var(--color-muted)]">…</li>}
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
{pages.map((p) => (
|
|
67
|
+
<li>
|
|
68
|
+
<a
|
|
69
|
+
href={href(p)}
|
|
70
|
+
aria-current={p === page ? "page" : undefined}
|
|
71
|
+
class:list={[
|
|
72
|
+
"grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] font-display text-sm",
|
|
73
|
+
p === page
|
|
74
|
+
? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
|
|
75
|
+
: "hover:bg-[var(--color-surface)]",
|
|
76
|
+
]}
|
|
77
|
+
>{p}</a>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
{end < pageCount && (
|
|
81
|
+
<>
|
|
82
|
+
{end < pageCount - 1 && <li aria-hidden="true" class="px-1 text-[var(--color-muted)]">…</li>}
|
|
83
|
+
<li><a href={href(pageCount)} class="grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] hover:bg-[var(--color-surface)]">{pageCount}</a></li>
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
<li>
|
|
87
|
+
<a
|
|
88
|
+
href={href(Math.min(pageCount, page + 1))}
|
|
89
|
+
aria-label="Next page"
|
|
90
|
+
aria-disabled={!hasNext}
|
|
91
|
+
class:list={[
|
|
92
|
+
"grid h-9 w-9 place-items-center rounded-[var(--radius-sm)]",
|
|
93
|
+
!hasNext ? "pointer-events-none text-[var(--color-muted)] opacity-40" : "hover:bg-[var(--color-surface)]",
|
|
94
|
+
]}
|
|
95
|
+
>›</a>
|
|
96
|
+
</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</nav>
|
|
99
|
+
)}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Sort control. A plain <form> select that auto-submits on change — works without
|
|
4
|
+
* a framework, stays URL-addressable. The current query's other params ride
|
|
5
|
+
* along as hidden inputs so sort doesn't drop active filters.
|
|
6
|
+
*
|
|
7
|
+
* Auto-submit is wired by a nonce'd `is:inline` <script> rather than an inline
|
|
8
|
+
* `onchange=` handler, so a strict, ENFORCED CSP allows it without 'unsafe-inline'
|
|
9
|
+
* / 'unsafe-hashes' (Epic S2). The <noscript> Apply button covers the no-JS case.
|
|
10
|
+
*/
|
|
11
|
+
import type { ListProductsQuery } from "@tot/public-runtime";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
query: ListProductsQuery;
|
|
15
|
+
basePath: string;
|
|
16
|
+
}
|
|
17
|
+
const { query, basePath } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const OPTIONS: { value: NonNullable<ListProductsQuery["sort"]>; label: string }[] = [
|
|
20
|
+
{ value: "featured", label: "Featured" },
|
|
21
|
+
{ value: "newest", label: "Newest" },
|
|
22
|
+
{ value: "price-asc", label: "Price: Low to High" },
|
|
23
|
+
{ value: "price-desc", label: "Price: High to Low" },
|
|
24
|
+
{ value: "title-asc", label: "Alphabetical (A–Z)" },
|
|
25
|
+
];
|
|
26
|
+
const current = query.sort ?? "featured";
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<form method="get" action={basePath} class="flex items-center gap-2 text-sm">
|
|
30
|
+
{/* Preserve filters/price/availability/q across a sort change. */}
|
|
31
|
+
{query.q && <input type="hidden" name="q" value={query.q} />}
|
|
32
|
+
{query.availability && query.availability !== "all" && (
|
|
33
|
+
<input type="hidden" name="availability" value={query.availability} />
|
|
34
|
+
)}
|
|
35
|
+
{query.priceMin != null && <input type="hidden" name="priceMin" value={query.priceMin} />}
|
|
36
|
+
{query.priceMax != null && <input type="hidden" name="priceMax" value={query.priceMax} />}
|
|
37
|
+
{Object.entries(query.filters ?? {}).flatMap(([name, values]) =>
|
|
38
|
+
values.map((v) => (
|
|
39
|
+
<input type="hidden" name={name === "tags" ? "tag" : `opt.${name}`} value={v} />
|
|
40
|
+
)),
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
<label for="sort" class="text-[var(--color-muted)]">Sort</label>
|
|
44
|
+
<select
|
|
45
|
+
id="sort"
|
|
46
|
+
name="sort"
|
|
47
|
+
data-sort-autosubmit
|
|
48
|
+
class="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 font-display"
|
|
49
|
+
>
|
|
50
|
+
{OPTIONS.map((o) => (
|
|
51
|
+
<option value={o.value} selected={o.value === current}>{o.label}</option>
|
|
52
|
+
))}
|
|
53
|
+
</select>
|
|
54
|
+
<noscript><button type="submit" class="underline">Apply</button></noscript>
|
|
55
|
+
</form>
|
|
56
|
+
|
|
57
|
+
<script is:inline nonce={Astro.locals.cspNonce}>
|
|
58
|
+
// Progressive enhancement: submit the form when the sort changes. Nonce'd
|
|
59
|
+
// inline (the platform pattern for app-authored inline JS) so a strict,
|
|
60
|
+
// ENFORCED CSP allows it without 'unsafe-inline'.
|
|
61
|
+
for (const el of document.querySelectorAll("select[data-sort-autosubmit]")) {
|
|
62
|
+
el.addEventListener("change", () => el.form && el.form.submit());
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only tenant-registry seam for the localhost sign-in bypass.
|
|
3
|
+
*
|
|
4
|
+
* `lib/auth/devAuth` can't import the concrete static tenant map directly
|
|
5
|
+
* (tenancy-resolver-fence — the registry stays behind the `config` boundary so a
|
|
6
|
+
* single-tenant extraction stays a drop-in). This exposes exactly the one thing
|
|
7
|
+
* that bypass needs: the resources a local dev session is granted membership on.
|
|
8
|
+
* In a single-tenant extraction this collapses to that one tenant with no change
|
|
9
|
+
* to devAuth. Same shape of config seam as {@link ./previewTenantLookup}.
|
|
10
|
+
*/
|
|
11
|
+
import { staticTenants } from "@/config/tenants";
|
|
12
|
+
|
|
13
|
+
/** Every registered tenant's resource id (appDomain, falling back to tenant_id). */
|
|
14
|
+
export function devTenantResources(): string[] {
|
|
15
|
+
return staticTenants.map((t) => t.appDomain ?? t.tenant_id);
|
|
16
|
+
}
|