@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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client behavior for the ToT-STAFF vendor search + enter on /dashboard (#8425).
|
|
3
|
+
*
|
|
4
|
+
* Served as a STATIC same-origin file (public/) and referenced via
|
|
5
|
+
* <script src="/js/dashboard-vendor-search.js">, so the strict production CSP
|
|
6
|
+
* allows it via `script-src 'self'` — no nonce, no per-build hash. (An inline
|
|
7
|
+
* <script> is blocked: Astro's `is:inline` drops the templated nonce, and a
|
|
8
|
+
* processed inline script needs its content-hash pinned in cspStaticHashes on
|
|
9
|
+
* every change. An external 'self' file avoids both — mirrors dashboard-team.js.)
|
|
10
|
+
*
|
|
11
|
+
* Two session-authed, staff-gated endpoints (no server-injected values, so this
|
|
12
|
+
* file is fully static):
|
|
13
|
+
* GET /api/dashboard/vendor-search?q=<prefix> → { vendors: [{appDomain, displayName}] }
|
|
14
|
+
* POST /api/dashboard/enter-vendor {vendorAppDomain} → { ok, redirect } (writes audit)
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
const $ = (id) => document.getElementById(id);
|
|
18
|
+
const input = $("vendorSearch");
|
|
19
|
+
if (!input) return; // non-staff page — nothing to wire.
|
|
20
|
+
const results = $("vendorResults");
|
|
21
|
+
const msg = $("vendorSearchMsg");
|
|
22
|
+
|
|
23
|
+
let seq = 0; // guards against out-of-order responses (last query wins).
|
|
24
|
+
let timer = null;
|
|
25
|
+
|
|
26
|
+
function showMsg(text, isErr) {
|
|
27
|
+
msg.textContent = text || "";
|
|
28
|
+
msg.className = isErr ? "err" : "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearResults() {
|
|
32
|
+
results.replaceChildren();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderResults(vendors) {
|
|
36
|
+
clearResults();
|
|
37
|
+
if (!vendors || !vendors.length) {
|
|
38
|
+
showMsg("No matching vendors.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
showMsg("");
|
|
42
|
+
for (const v of vendors) {
|
|
43
|
+
const li = document.createElement("li");
|
|
44
|
+
const btn = document.createElement("button");
|
|
45
|
+
btn.type = "button";
|
|
46
|
+
btn.dataset.appdomain = v.appDomain;
|
|
47
|
+
const dn = document.createElement("span");
|
|
48
|
+
dn.className = "dn";
|
|
49
|
+
dn.textContent = v.displayName || v.appDomain;
|
|
50
|
+
const ad = document.createElement("span");
|
|
51
|
+
ad.className = "ad";
|
|
52
|
+
ad.textContent = v.appDomain;
|
|
53
|
+
btn.appendChild(dn);
|
|
54
|
+
btn.appendChild(ad);
|
|
55
|
+
btn.addEventListener("click", () => enter(v.appDomain, btn));
|
|
56
|
+
li.appendChild(btn);
|
|
57
|
+
results.appendChild(li);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function search(q) {
|
|
62
|
+
const mine = ++seq;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch("/api/dashboard/vendor-search?q=" + encodeURIComponent(q), {
|
|
65
|
+
headers: { accept: "application/json" },
|
|
66
|
+
});
|
|
67
|
+
if (mine !== seq) return; // a newer query superseded this one.
|
|
68
|
+
const body = await res.json().catch(() => ({}));
|
|
69
|
+
if (res.ok) {
|
|
70
|
+
renderResults(body.vendors);
|
|
71
|
+
} else {
|
|
72
|
+
clearResults();
|
|
73
|
+
showMsg(body.error || "Search failed (HTTP " + res.status + ").", true);
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (mine !== seq) return;
|
|
77
|
+
clearResults();
|
|
78
|
+
showMsg("Search failed: " + (e && e.message ? e.message : e), true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function enter(appDomain, btn) {
|
|
83
|
+
if (btn) btn.disabled = true;
|
|
84
|
+
showMsg("Entering " + appDomain + "…");
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch("/api/dashboard/enter-vendor", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
89
|
+
body: JSON.stringify({ vendorAppDomain: appDomain }),
|
|
90
|
+
});
|
|
91
|
+
const body = await res.json().catch(() => ({}));
|
|
92
|
+
if (res.ok && body.redirect) {
|
|
93
|
+
window.location.assign(body.redirect);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
showMsg(body.error || "Couldn't enter that vendor (HTTP " + res.status + ").", true);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
showMsg("Couldn't enter: " + (e && e.message ? e.message : e), true);
|
|
99
|
+
} finally {
|
|
100
|
+
if (btn) btn.disabled = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
input.addEventListener("input", () => {
|
|
105
|
+
const q = input.value.trim();
|
|
106
|
+
if (timer) clearTimeout(timer);
|
|
107
|
+
if (!q) {
|
|
108
|
+
seq++; // cancel any in-flight render.
|
|
109
|
+
clearResults();
|
|
110
|
+
showMsg("");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
timer = setTimeout(() => search(q), 200); // debounce keystrokes.
|
|
114
|
+
});
|
|
115
|
+
})();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Badge> — tiny mono-uppercase pill used over product imagery and inline.
|
|
4
|
+
* Theme-agnostic: colors map to CSS-var-backed Tailwind utilities.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
/** `soldout` is the out-of-stock state; `compliant` marks a
|
|
8
|
+
* compliance-verified item (age/lab/regulatory). */
|
|
9
|
+
variant: "sale" | "new" | "low-stock" | "soldout" | "compliant";
|
|
10
|
+
/** Visible text. May also be supplied via the default slot. */
|
|
11
|
+
label?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { variant, label, class: className } = Astro.props;
|
|
16
|
+
|
|
17
|
+
const variants: Record<Props["variant"], string> = {
|
|
18
|
+
sale: "bg-sale text-white",
|
|
19
|
+
new: "bg-primary text-primary-contrast",
|
|
20
|
+
"low-stock": "bg-accent text-white",
|
|
21
|
+
soldout: "bg-surface text-muted border border-border",
|
|
22
|
+
compliant: "bg-[var(--color-success)] text-white",
|
|
23
|
+
};
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<span
|
|
27
|
+
class:list={[
|
|
28
|
+
"inline-flex items-center rounded-full px-2 py-0.5 font-mono text-[0.62rem] uppercase tracking-[0.12em] leading-none whitespace-nowrap",
|
|
29
|
+
variants[variant],
|
|
30
|
+
className,
|
|
31
|
+
]}
|
|
32
|
+
>
|
|
33
|
+
{label}<slot />
|
|
34
|
+
</span>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Breadcrumbs> — semantic breadcrumb trail. The last crumb is the current
|
|
4
|
+
* page (aria-current, not a link). No own container — inherits parent width.
|
|
5
|
+
*/
|
|
6
|
+
import { withBase } from "@/lib/basePath";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
crumbs: { label: string; href: string }[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { crumbs } = Astro.props;
|
|
13
|
+
const { basePath } = Astro.locals;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<nav aria-label="Breadcrumb" class="text-sm text-muted">
|
|
17
|
+
<ol class="flex flex-wrap items-center gap-x-1 gap-y-0.5">
|
|
18
|
+
{
|
|
19
|
+
crumbs.map((crumb, i) => {
|
|
20
|
+
const isLast = i === crumbs.length - 1;
|
|
21
|
+
return (
|
|
22
|
+
<li class="flex items-center gap-x-1">
|
|
23
|
+
{i > 0 && (
|
|
24
|
+
<span aria-hidden="true" class="select-none text-border">
|
|
25
|
+
/
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
{isLast ? (
|
|
29
|
+
<span aria-current="page" class="text-text">
|
|
30
|
+
{crumb.label}
|
|
31
|
+
</span>
|
|
32
|
+
) : (
|
|
33
|
+
<a
|
|
34
|
+
href={withBase(basePath, crumb.href)}
|
|
35
|
+
class="transition-colors hover:text-primary"
|
|
36
|
+
>
|
|
37
|
+
{crumb.label}
|
|
38
|
+
</a>
|
|
39
|
+
)}
|
|
40
|
+
</li>
|
|
41
|
+
);
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
</ol>
|
|
45
|
+
</nav>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <CollectionCard> — editorial collection tile with a dark gradient scrim so
|
|
4
|
+
* the overlaid title reads on any image. Theme-agnostic (CSS-var-backed
|
|
5
|
+
* utilities only).
|
|
6
|
+
*/
|
|
7
|
+
import type { CatalogCollection } from "@tot/public-runtime";
|
|
8
|
+
import Img from "@/components/Img.astro";
|
|
9
|
+
import { withBase } from "@/lib/basePath";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
collection: CatalogCollection;
|
|
13
|
+
productCount?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { collection, productCount } = Astro.props;
|
|
17
|
+
const { basePath } = Astro.locals;
|
|
18
|
+
const image = collection.image;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<a
|
|
22
|
+
href={withBase(basePath, `/collections/${collection.handle}`)}
|
|
23
|
+
class="group relative block aspect-[3/2] overflow-hidden rounded-md bg-border"
|
|
24
|
+
data-reveal
|
|
25
|
+
>
|
|
26
|
+
<Img
|
|
27
|
+
src={image?.url}
|
|
28
|
+
rehosted={image?.rehosted}
|
|
29
|
+
seed={image?.id ?? collection.handle}
|
|
30
|
+
label={collection.title}
|
|
31
|
+
alt={image?.alt ?? collection.title}
|
|
32
|
+
width={image?.width ?? 800}
|
|
33
|
+
height={image?.height ?? 600}
|
|
34
|
+
sizes="(min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw"
|
|
35
|
+
class="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-[1.03]"
|
|
36
|
+
/>
|
|
37
|
+
<!-- Scrim: deep enough at the foot that white text passes AA over any image
|
|
38
|
+
(incl. the lighter placeholder pairs). -->
|
|
39
|
+
<div
|
|
40
|
+
class="absolute inset-0 bg-gradient-to-t from-black/75 via-black/20 to-transparent"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="absolute inset-x-0 bottom-0 p-4 sm:p-5">
|
|
45
|
+
<h3 class="font-display text-xl font-semibold text-white drop-shadow-sm sm:text-2xl">
|
|
46
|
+
{collection.title}
|
|
47
|
+
</h3>
|
|
48
|
+
{
|
|
49
|
+
productCount != null && (
|
|
50
|
+
<p class="mt-0.5 font-mono text-[0.68rem] uppercase tracking-[0.12em] text-white/80">
|
|
51
|
+
{productCount} {productCount === 1 ? "piece" : "pieces"}
|
|
52
|
+
</p>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
</div>
|
|
56
|
+
</a>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <EmptyState> — intentional empty result (search/PLP). Copy is passed in by
|
|
4
|
+
* the parent (kept in the tenant's editorial voice).
|
|
5
|
+
*/
|
|
6
|
+
import { withBase } from "@/lib/basePath";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
ctaLabel?: string;
|
|
12
|
+
ctaHref?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { title, body, ctaLabel, ctaHref } = Astro.props;
|
|
16
|
+
const { basePath } = Astro.locals;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div
|
|
20
|
+
class="mx-auto flex max-w-md flex-col items-center px-6 py-20 text-center sm:py-28"
|
|
21
|
+
data-reveal
|
|
22
|
+
>
|
|
23
|
+
<h2 class="display-md">{title}</h2>
|
|
24
|
+
{body && <p class="mt-3 max-w-prose text-muted">{body}</p>}
|
|
25
|
+
{
|
|
26
|
+
ctaLabel && ctaHref && (
|
|
27
|
+
<a
|
|
28
|
+
href={withBase(basePath, ctaHref)}
|
|
29
|
+
class="mt-7 inline-flex items-center rounded-full bg-primary px-5 py-2 font-display text-sm font-medium text-primary-contrast transition-opacity hover:opacity-90"
|
|
30
|
+
>
|
|
31
|
+
{ctaLabel}
|
|
32
|
+
</a>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Img> — CLS-free, theme-agnostic image with offline placeholder fallback.
|
|
4
|
+
*
|
|
5
|
+
* Fixture image URLs are dead Shopify-CDN paths (brief: images note). When a
|
|
6
|
+
* URL is unusable, we render a deterministic, aspect-correct gradient SVG keyed
|
|
7
|
+
* by a stable seed so the site looks polished offline AND stays CLS-free
|
|
8
|
+
* (explicit width/height always set). When real rehosted URLs land
|
|
9
|
+
* (CatalogImage.rehosted === true), the same component serves them with
|
|
10
|
+
* responsive srcset/sizes.
|
|
11
|
+
*
|
|
12
|
+
* Responsive srcset is emitted for any URL whose CDN accepts `?width=`: the
|
|
13
|
+
* rehost target AND Shopify's image CDN (so referenced Shopify URLs from a live
|
|
14
|
+
* migration are responsive without rehosting).
|
|
15
|
+
*/
|
|
16
|
+
import { resolveImage } from "@/lib/imageAttrs";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
src?: string;
|
|
20
|
+
alt: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
/** Seed for the placeholder (image id or product handle). */
|
|
24
|
+
seed: string;
|
|
25
|
+
/** Short label drawn into the placeholder (e.g. product title). */
|
|
26
|
+
label?: string;
|
|
27
|
+
rehosted?: boolean;
|
|
28
|
+
/** LCP hint: eager + high priority + no lazy. */
|
|
29
|
+
priority?: boolean;
|
|
30
|
+
sizes?: string;
|
|
31
|
+
class?: string;
|
|
32
|
+
loading?: "eager" | "lazy";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
src,
|
|
37
|
+
alt,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
seed,
|
|
41
|
+
label,
|
|
42
|
+
rehosted = false,
|
|
43
|
+
priority = false,
|
|
44
|
+
sizes,
|
|
45
|
+
class: className,
|
|
46
|
+
loading,
|
|
47
|
+
} = Astro.props;
|
|
48
|
+
|
|
49
|
+
// Render decision (placeholder / Cloudflare resizing / Shopify ?width=) lives in
|
|
50
|
+
// lib/imageAttrs.ts so it's unit-testable without rendering this component (D4).
|
|
51
|
+
const { src: resolvedSrc, srcset } = resolveImage({ src, rehosted, width, height, seed, label });
|
|
52
|
+
|
|
53
|
+
const loadingAttr = loading ?? (priority ? "eager" : "lazy");
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<img
|
|
57
|
+
src={resolvedSrc}
|
|
58
|
+
srcset={srcset}
|
|
59
|
+
sizes={srcset ? (sizes ?? "(min-width: 768px) 50vw, 100vw") : undefined}
|
|
60
|
+
alt={alt}
|
|
61
|
+
width={width}
|
|
62
|
+
height={height}
|
|
63
|
+
loading={loadingAttr}
|
|
64
|
+
decoding={priority ? "sync" : "async"}
|
|
65
|
+
fetchpriority={priority ? "high" : "auto"}
|
|
66
|
+
class={className}
|
|
67
|
+
style={`aspect-ratio:${width}/${height};background:var(--color-border)`}
|
|
68
|
+
/>
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <ProductCard> — PLP/grid card. Uses the stretched-link pattern (the title is
|
|
4
|
+
* the link; an ::after overlay makes the whole card clickable) so interactive
|
|
5
|
+
* controls — wishlist heart, quick-view — can sit ABOVE the link without nesting
|
|
6
|
+
* buttons in an anchor. Image hover-zoom + optional second image; status badges;
|
|
7
|
+
* color swatches; price with struck compare-at. Theme-agnostic.
|
|
8
|
+
*/
|
|
9
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
10
|
+
import { discountPercent, truncate } from "@/lib/format";
|
|
11
|
+
import { isOnSale, isNew, isAvailable, fromPrice } from "@/lib/tot/query";
|
|
12
|
+
import { colorOptionValues, resolveSwatch, isLightFill } from "@/lib/colors";
|
|
13
|
+
import Img from "@/components/Img.astro";
|
|
14
|
+
import Badge from "@/components/Badge.astro";
|
|
15
|
+
import PriceDisplay from "@/components/commerce/PriceDisplay.astro";
|
|
16
|
+
import RatingStars from "@/components/commerce/RatingStars.astro";
|
|
17
|
+
import { withBase } from "@/lib/basePath";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
product: CatalogProduct;
|
|
21
|
+
position?: number;
|
|
22
|
+
list?: string;
|
|
23
|
+
priority?: boolean;
|
|
24
|
+
/** Optional aggregate rating (0–5) — renders <RatingStars> when present. */
|
|
25
|
+
rating?: number;
|
|
26
|
+
/** Optional review count shown with the stars. */
|
|
27
|
+
reviewCount?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { product, priority = false, rating, reviewCount } = Astro.props;
|
|
31
|
+
const { tenant, basePath } = Astro.locals;
|
|
32
|
+
|
|
33
|
+
// A compliance-verified item (age/lab/regulatory) earns a "Compliant" badge —
|
|
34
|
+
// tag-driven so it stays tenant-agnostic (no per-merchant branch).
|
|
35
|
+
const isCompliant = product.tags.some((t) =>
|
|
36
|
+
["compliant", "lab-tested", "age-verified"].includes(t),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const primary = product.images[0];
|
|
40
|
+
const secondary = product.images[1];
|
|
41
|
+
|
|
42
|
+
const onSale = isOnSale(product);
|
|
43
|
+
const available = isAvailable(product);
|
|
44
|
+
const isNewProduct = !onSale && isNew(product);
|
|
45
|
+
|
|
46
|
+
const saleVariant = product.variants
|
|
47
|
+
.filter((v) => v.compare_at_price != null && v.compare_at_price > v.price)
|
|
48
|
+
.sort(
|
|
49
|
+
(a, b) =>
|
|
50
|
+
discountPercent(b.price, b.compare_at_price)! -
|
|
51
|
+
discountPercent(a.price, a.compare_at_price)!,
|
|
52
|
+
)[0];
|
|
53
|
+
const salePercent = saleVariant
|
|
54
|
+
? discountPercent(saleVariant.price, saleVariant.compare_at_price)
|
|
55
|
+
: null;
|
|
56
|
+
|
|
57
|
+
const maxCompareAt = onSale
|
|
58
|
+
? Math.max(
|
|
59
|
+
...product.variants
|
|
60
|
+
.filter((v) => v.compare_at_price != null)
|
|
61
|
+
.map((v) => v.compare_at_price!),
|
|
62
|
+
)
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
const price = fromPrice(product);
|
|
66
|
+
const currency = product.price_range.currency;
|
|
67
|
+
const locale = tenant.locale;
|
|
68
|
+
|
|
69
|
+
const colors = colorOptionValues(product.options);
|
|
70
|
+
const MAX_SWATCHES = 5;
|
|
71
|
+
const swatches = colors.slice(0, MAX_SWATCHES).map(resolveSwatch);
|
|
72
|
+
const extraColors = Math.max(0, colors.length - MAX_SWATCHES);
|
|
73
|
+
|
|
74
|
+
// Compact snapshot for the wishlist heart + recently-viewed parity.
|
|
75
|
+
const snapshot = {
|
|
76
|
+
handle: product.handle,
|
|
77
|
+
title: product.title,
|
|
78
|
+
price,
|
|
79
|
+
currency,
|
|
80
|
+
vendor: product.vendor,
|
|
81
|
+
img: primary?.url,
|
|
82
|
+
rehosted: primary?.rehosted,
|
|
83
|
+
seed: primary?.id ?? product.handle,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Richer payload for the quick-view dialog (no variant picker — that's the PDP).
|
|
87
|
+
const quickview = {
|
|
88
|
+
...snapshot,
|
|
89
|
+
images: product.images.slice(0, 4).map((im) => ({
|
|
90
|
+
url: im.url,
|
|
91
|
+
rehosted: im.rehosted,
|
|
92
|
+
seed: im.id ?? product.handle,
|
|
93
|
+
})),
|
|
94
|
+
desc: truncate(product.description_text, 180),
|
|
95
|
+
colors,
|
|
96
|
+
onSale,
|
|
97
|
+
available,
|
|
98
|
+
maxCompareAt,
|
|
99
|
+
sku: product.variants[0]?.sku ?? null,
|
|
100
|
+
};
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
<div class="group relative" data-reveal>
|
|
104
|
+
<div class="relative aspect-[4/5] overflow-hidden rounded-md bg-border ring-1 ring-[var(--color-border)] transition-shadow duration-300 motion-safe:group-hover:shadow-[var(--shadow-md)]">
|
|
105
|
+
{
|
|
106
|
+
primary ? (
|
|
107
|
+
<Img
|
|
108
|
+
src={primary.url}
|
|
109
|
+
rehosted={primary.rehosted}
|
|
110
|
+
seed={primary.id ?? product.handle}
|
|
111
|
+
label={product.title}
|
|
112
|
+
alt={primary.alt ?? product.title}
|
|
113
|
+
width={primary.width ?? 800}
|
|
114
|
+
height={primary.height ?? 1000}
|
|
115
|
+
sizes="(min-width:1024px) 25vw, (min-width:640px) 50vw, 100vw"
|
|
116
|
+
priority={priority}
|
|
117
|
+
class="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-[1.03]"
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<Img
|
|
121
|
+
seed={product.handle}
|
|
122
|
+
label={product.title}
|
|
123
|
+
alt={product.title}
|
|
124
|
+
width={800}
|
|
125
|
+
height={1000}
|
|
126
|
+
sizes="(min-width:1024px) 25vw, (min-width:640px) 50vw, 100vw"
|
|
127
|
+
priority={priority}
|
|
128
|
+
class="h-full w-full object-cover"
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
{
|
|
133
|
+
secondary && (
|
|
134
|
+
<Img
|
|
135
|
+
src={secondary.url}
|
|
136
|
+
rehosted={secondary.rehosted}
|
|
137
|
+
seed={secondary.id ?? `${product.handle}-2`}
|
|
138
|
+
label={product.title}
|
|
139
|
+
alt=""
|
|
140
|
+
width={secondary.width ?? 800}
|
|
141
|
+
height={secondary.height ?? 1000}
|
|
142
|
+
sizes="(min-width:1024px) 25vw, (min-width:640px) 50vw, 100vw"
|
|
143
|
+
class="absolute inset-0 h-full w-full object-cover opacity-0 transition-opacity duration-500 motion-safe:group-hover:opacity-100"
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
{/* Status badges */}
|
|
149
|
+
<div class="pointer-events-none absolute left-2 top-2 z-10 flex flex-col items-start gap-1">
|
|
150
|
+
{onSale && <Badge variant="sale" label={salePercent ? `-${salePercent}%` : "Sale"} />}
|
|
151
|
+
{isNewProduct && <Badge variant="new" label="New" />}
|
|
152
|
+
{isCompliant && <Badge variant="compliant" label="Compliant" />}
|
|
153
|
+
{!available && <Badge variant="soldout" label="Sold out" />}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Wishlist heart — above the stretched link (z-10). State hydrated by the
|
|
157
|
+
global wishlist script; aria-pressed reflects saved state. */}
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="wishlist-toggle absolute right-2 top-2 z-10 grid h-9 w-9 place-items-center rounded-full bg-[color-mix(in_srgb,var(--color-surface)_88%,transparent)] text-[var(--color-text)] backdrop-blur transition-colors hover:text-[var(--color-sale)] focus-visible:text-[var(--color-sale)] aria-pressed:text-[var(--color-sale)]"
|
|
161
|
+
aria-pressed="false"
|
|
162
|
+
aria-label={`Save ${product.title}`}
|
|
163
|
+
data-wishlist={JSON.stringify(snapshot)}
|
|
164
|
+
>
|
|
165
|
+
<svg class="wishlist-heart h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
|
166
|
+
<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" />
|
|
167
|
+
</svg>
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
{/* Quick view — reveals on hover/focus-within; opens the global dialog. */}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
class="quickview-trigger absolute inset-x-2 bottom-2 z-10 rounded-[var(--radius-sm)] bg-[color-mix(in_srgb,var(--color-surface)_92%,transparent)] py-2 text-center font-display text-xs font-medium text-[var(--color-text)] opacity-0 backdrop-blur transition-opacity duration-200 hover:bg-[var(--color-surface)] focus-visible:opacity-100 motion-safe:group-hover:opacity-100 md:opacity-0"
|
|
174
|
+
data-quickview={JSON.stringify(quickview)}
|
|
175
|
+
>
|
|
176
|
+
Quick view
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="mt-3.5">
|
|
181
|
+
{
|
|
182
|
+
product.vendor && (
|
|
183
|
+
<p class="font-mono text-[0.62rem] uppercase tracking-[0.14em] text-muted">
|
|
184
|
+
{product.vendor}
|
|
185
|
+
</p>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
<h3 class="mt-1 font-display text-[0.95rem] font-medium leading-snug">
|
|
189
|
+
<a
|
|
190
|
+
href={withBase(basePath, `/products/${product.handle}`)}
|
|
191
|
+
class="after:absolute after:inset-0 after:z-0 transition-colors motion-safe:group-hover:text-primary"
|
|
192
|
+
>
|
|
193
|
+
{product.title}
|
|
194
|
+
</a>
|
|
195
|
+
</h3>
|
|
196
|
+
<PriceDisplay product={product} locale={locale} class="mt-1.5" />
|
|
197
|
+
|
|
198
|
+
{rating != null && <RatingStars rating={rating} count={reviewCount} class="mt-1.5" />}
|
|
199
|
+
|
|
200
|
+
{
|
|
201
|
+
swatches.length > 1 && (
|
|
202
|
+
<ul class="mt-2.5 flex items-center gap-1.5" aria-label={`Available in ${colors.length} colors`}>
|
|
203
|
+
{swatches.map((s) => (
|
|
204
|
+
<li
|
|
205
|
+
title={s.label}
|
|
206
|
+
class:list={[
|
|
207
|
+
"h-3.5 w-3.5 rounded-full",
|
|
208
|
+
s.fill && isLightFill(s.fill) ? "ring-1 ring-inset ring-[var(--color-border)]" : "",
|
|
209
|
+
]}
|
|
210
|
+
style={
|
|
211
|
+
s.fill
|
|
212
|
+
? `background:${s.fill}`
|
|
213
|
+
: "background:repeating-linear-gradient(45deg,var(--color-surface),var(--color-surface)2px,var(--color-border)2px,var(--color-border)4px)"
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
<span class="sr-only">{s.label}</span>
|
|
217
|
+
</li>
|
|
218
|
+
))}
|
|
219
|
+
{extraColors > 0 && (
|
|
220
|
+
<li class="ml-0.5 font-mono text-[0.62rem] text-muted" aria-hidden="true">
|
|
221
|
+
+{extraColors}
|
|
222
|
+
</li>
|
|
223
|
+
)}
|
|
224
|
+
</ul>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Section wrapper — consistent vertical rhythm + optional titled header.
|
|
4
|
+
* Structure encodes meaning (brief §6): an eyebrow/title pair only when there's
|
|
5
|
+
* a real section heading; otherwise just rhythm.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
title?: string;
|
|
9
|
+
eyebrow?: string;
|
|
10
|
+
/** "bg" (default) | "surface" — alternate band background. */
|
|
11
|
+
tone?: "bg" | "surface";
|
|
12
|
+
class?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
}
|
|
15
|
+
const { title, eyebrow, tone = "bg", class: className, id } = Astro.props;
|
|
16
|
+
const bg = tone === "surface" ? "bg-[var(--color-surface)]" : "bg-[var(--color-bg)]";
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<section
|
|
20
|
+
id={id}
|
|
21
|
+
class:list={[bg, "py-[clamp(3rem,7vw,6rem)]", className]}
|
|
22
|
+
>
|
|
23
|
+
<div class="container-prose">
|
|
24
|
+
{(title || eyebrow) && (
|
|
25
|
+
<header class="mb-9 flex flex-wrap items-end justify-between gap-4" data-reveal>
|
|
26
|
+
<div>
|
|
27
|
+
{eyebrow && <p class="eyebrow eyebrow--tick mb-3">{eyebrow}</p>}
|
|
28
|
+
{title && (
|
|
29
|
+
<h2 class="display-lg">
|
|
30
|
+
{title}
|
|
31
|
+
</h2>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
<slot name="header-aside" />
|
|
35
|
+
</header>
|
|
36
|
+
)}
|
|
37
|
+
<slot />
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|