@tokenoftrust/storefront-runner 1.3.2
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 +42 -0
- package/apps/storefront/astro.config.mjs +66 -0
- package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
- package/apps/storefront/env.d.ts +162 -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 +42 -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/public/shared/commerce-chrome.css +369 -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 +55 -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/admin/AdminAIWorkflowsTab.astro +531 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
- package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -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 +201 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
- package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -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/commerce/ReviewList.astro +54 -0
- package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
- package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +109 -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 +375 -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/membership/IllustrativeLabel.astro +56 -0
- package/apps/storefront/src/components/membership/TierCards.astro +164 -0
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -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/components/style-guide/StyleGuideNav.astro +95 -0
- package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
- package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
- package/apps/storefront/src/config/devTenantSeed.ts +78 -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 +102 -0
- package/apps/storefront/src/config/tenants.ts +87 -0
- package/apps/storefront/src/layouts/Layout.astro +368 -0
- package/apps/storefront/src/lib/admin/README.md +122 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
- package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
- package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
- package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
- package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
- package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
- package/apps/storefront/src/lib/admin/envelope.ts +10 -0
- package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
- package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
- package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
- package/apps/storefront/src/lib/admin/seed.ts +54 -0
- package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
- package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
- package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
- package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
- package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
- package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
- package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
- package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
- package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
- package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
- package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
- package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
- package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -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 +119 -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 +244 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -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 +366 -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/activityIngestToken.ts +78 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
- package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
- package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
- package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
- package/apps/storefront/src/lib/dev/storeStatus.ts +142 -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 +38 -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 +166 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/membership/model.ts +97 -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/priceBuckets.ts +33 -0
- package/apps/storefront/src/lib/rawChrome.ts +505 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -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 +607 -0
- package/apps/storefront/src/lib/subscription/model.ts +68 -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 +249 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +142 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +455 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/admin.astro +1675 -0
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
- package/apps/storefront/src/pages/api/admin/orders.ts +71 -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/checkout-style.css.ts +43 -0
- package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -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/activity.ts +124 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
- package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
- package/apps/storefront/src/pages/api/dev/file.ts +57 -0
- package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
- package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
- package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
- package/apps/storefront/src/pages/api/dev/write.ts +116 -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 +177 -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/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +402 -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 +1280 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
- package/apps/storefront/src/pages/style-guide/index.astro +98 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +292 -0
- package/apps/storefront/src/themes/reference.ts +68 -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/package.json +26 -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/checkout-style-publish.ts +200 -0
- package/packages/public-runtime/src/checkout-style.ts +375 -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-orders.ts +136 -0
- package/packages/public-runtime/src/foxy.ts +196 -0
- package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
- package/packages/public-runtime/src/index.ts +34 -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 +384 -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/dev/ai-edit.mjs +183 -0
- package/scripts/dev/checkout-watch.mjs +116 -0
- package/scripts/dev/file-browser.mjs +159 -0
- package/scripts/dev/file-writer.mjs +141 -0
- package/scripts/dev/git-status.mjs +171 -0
- package/scripts/dev/locate-handler.mjs +163 -0
- package/scripts/dev/locate.mjs +112 -0
- package/scripts/dev/port-check.mjs +48 -0
- package/scripts/dev/safe-path.mjs +109 -0
- package/scripts/dev/shot.mjs +46 -0
- package/scripts/dev/tenant-source-root.mjs +76 -0
- package/scripts/dev/unified-diff.mjs +81 -0
- package/scripts/tot-dev.mjs +376 -0
- package/tenants/README.md +14 -0
- package/tenants/home/content/chrome.json +7 -0
- package/tenants/home/content/home.html +58 -0
- package/tenants/home/content/home.json +11 -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/tsconfig.base.json +20 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <BrandGrid> — a "Shop by Brands" logo wall: a grid of brand tiles linking to
|
|
4
|
+
* brand collections, each rendered as a rehosted logo image OR a text-wordmark
|
|
5
|
+
* fallback when no logo is available. Tenant-agnostic — the `brands` array is
|
|
6
|
+
* content, theme tokens are the skin. Replaces per-tenant hand-authored blocks
|
|
7
|
+
* (image-logo walls and text-wordmark tiles alike) with one shared component.
|
|
8
|
+
*
|
|
9
|
+
* Two delivery paths, one look: this component styles itself from the token
|
|
10
|
+
* contract via a scoped <style> (so it renders under the app Layout AND in the
|
|
11
|
+
* standalone style guide, neither of which links the raw sheet). The class-only
|
|
12
|
+
* raw-chrome parity lives in the `.brand-grid` / `.brand-tile` block of
|
|
13
|
+
* public/shared/commerce-chrome.css, for tenants that ship hand-authored HTML
|
|
14
|
+
* through the rawMarketingHtml splice pipeline. Keep the two visually equivalent.
|
|
15
|
+
*/
|
|
16
|
+
import Section from "@/components/Section.astro";
|
|
17
|
+
import Img from "@/components/Img.astro";
|
|
18
|
+
import { withBase } from "@/lib/basePath";
|
|
19
|
+
|
|
20
|
+
export interface Brand {
|
|
21
|
+
/** Brand name — the link label, image alt, and text-fallback wordmark. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Collection (or brand) URL, e.g. /collections/<handle>. Base-prefixed here. */
|
|
24
|
+
href: string;
|
|
25
|
+
/** Rehosted logo image URL. Omit to render the text wordmark fallback. */
|
|
26
|
+
logoSrc?: string;
|
|
27
|
+
/** True when logoSrc is a rehosted asset (enables responsive srcset). */
|
|
28
|
+
logoRehosted?: boolean;
|
|
29
|
+
logoWidth?: number;
|
|
30
|
+
logoHeight?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
brands: Brand[];
|
|
35
|
+
title?: string;
|
|
36
|
+
eyebrow?: string;
|
|
37
|
+
tone?: "bg" | "surface";
|
|
38
|
+
/** Optional "View all brands" call-to-action below the grid. */
|
|
39
|
+
viewAllHref?: string;
|
|
40
|
+
viewAllLabel?: string;
|
|
41
|
+
class?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
brands,
|
|
46
|
+
title,
|
|
47
|
+
eyebrow,
|
|
48
|
+
tone = "bg",
|
|
49
|
+
viewAllHref,
|
|
50
|
+
viewAllLabel = "View all brands",
|
|
51
|
+
class: className,
|
|
52
|
+
} = Astro.props;
|
|
53
|
+
|
|
54
|
+
const basePath = Astro.locals.basePath ?? "";
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
<Section title={title} eyebrow={eyebrow} tone={tone} class={className}>
|
|
58
|
+
<ul class="brand-grid" role="list">
|
|
59
|
+
{
|
|
60
|
+
brands.map((brand) => (
|
|
61
|
+
<li>
|
|
62
|
+
<a
|
|
63
|
+
class="brand-tile"
|
|
64
|
+
href={withBase(basePath, brand.href)}
|
|
65
|
+
aria-label={brand.label}
|
|
66
|
+
data-reveal
|
|
67
|
+
>
|
|
68
|
+
{brand.logoSrc ? (
|
|
69
|
+
<Img
|
|
70
|
+
src={brand.logoSrc}
|
|
71
|
+
rehosted={brand.logoRehosted}
|
|
72
|
+
seed={brand.href}
|
|
73
|
+
label={brand.label}
|
|
74
|
+
alt={brand.label}
|
|
75
|
+
width={brand.logoWidth ?? 200}
|
|
76
|
+
height={brand.logoHeight ?? 100}
|
|
77
|
+
sizes="(min-width: 640px) 160px, 44vw"
|
|
78
|
+
/>
|
|
79
|
+
) : (
|
|
80
|
+
<span class="brand-tile__text">{brand.label}</span>
|
|
81
|
+
)}
|
|
82
|
+
</a>
|
|
83
|
+
</li>
|
|
84
|
+
))
|
|
85
|
+
}
|
|
86
|
+
</ul>
|
|
87
|
+
{
|
|
88
|
+
viewAllHref && (
|
|
89
|
+
<p class="brand-grid__more">
|
|
90
|
+
<a class="brand-grid__cta" href={withBase(basePath, viewAllHref)}>
|
|
91
|
+
{viewAllLabel}
|
|
92
|
+
</a>
|
|
93
|
+
</p>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
</Section>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
/* Mirrors the raw-chrome `.brand-grid` / `.brand-tile` block in
|
|
100
|
+
public/shared/commerce-chrome.css — same token vocabulary, same look. */
|
|
101
|
+
.brand-grid {
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
104
|
+
gap: clamp(10px, 1.4vw, 16px);
|
|
105
|
+
}
|
|
106
|
+
.brand-tile {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
min-height: 92px;
|
|
111
|
+
padding: 18px 16px;
|
|
112
|
+
text-align: center;
|
|
113
|
+
background: var(--color-surface);
|
|
114
|
+
border: 1px solid var(--color-border);
|
|
115
|
+
border-radius: var(--radius-md);
|
|
116
|
+
font-family: var(--font-display);
|
|
117
|
+
font-weight: 700;
|
|
118
|
+
font-size: 0.95rem;
|
|
119
|
+
letter-spacing: 0.01em;
|
|
120
|
+
color: var(--color-text);
|
|
121
|
+
transition:
|
|
122
|
+
transform 0.14s ease,
|
|
123
|
+
box-shadow 0.14s ease,
|
|
124
|
+
border-color 0.14s ease,
|
|
125
|
+
color 0.14s ease;
|
|
126
|
+
}
|
|
127
|
+
.brand-tile:hover {
|
|
128
|
+
transform: translateY(-2px);
|
|
129
|
+
box-shadow: var(--shadow-md);
|
|
130
|
+
border-color: var(--color-primary);
|
|
131
|
+
color: var(--color-primary);
|
|
132
|
+
}
|
|
133
|
+
.brand-tile :global(img) {
|
|
134
|
+
max-height: 46px;
|
|
135
|
+
max-width: 100%;
|
|
136
|
+
width: auto;
|
|
137
|
+
object-fit: contain;
|
|
138
|
+
}
|
|
139
|
+
.brand-grid__more {
|
|
140
|
+
margin-top: clamp(1.25rem, 3vw, 1.75rem);
|
|
141
|
+
text-align: center;
|
|
142
|
+
}
|
|
143
|
+
/* Ghost-button parity with the raw sheet's .btn-ghost, token-only. */
|
|
144
|
+
.brand-grid__cta {
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 0.5em;
|
|
148
|
+
font-family: var(--font-display);
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
font-size: 0.95rem;
|
|
151
|
+
padding: 0.8em 1.6em;
|
|
152
|
+
border: 2px solid var(--color-border);
|
|
153
|
+
border-radius: var(--radius-full);
|
|
154
|
+
background: var(--color-surface);
|
|
155
|
+
color: var(--color-primary);
|
|
156
|
+
transition:
|
|
157
|
+
border-color 0.15s ease,
|
|
158
|
+
transform 0.12s ease;
|
|
159
|
+
}
|
|
160
|
+
.brand-grid__cta:hover {
|
|
161
|
+
border-color: var(--color-primary);
|
|
162
|
+
transform: translateY(-1px);
|
|
163
|
+
}
|
|
164
|
+
@media (max-width: 560px) {
|
|
165
|
+
.brand-grid {
|
|
166
|
+
grid-template-columns: repeat(auto-fill, minmax(44%, 1fr));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
.brand-tile,
|
|
171
|
+
.brand-grid__cta {
|
|
172
|
+
transition: none;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <FoxyLoader> — injects FoxyCart's loader.js (Track A).
|
|
4
|
+
*
|
|
5
|
+
* Foxy's loader turns our add-to-cart forms/links into the sidecart + hosted
|
|
6
|
+
* checkout ("Powered by FoxyCart"). It's loaded from the store's CDN path and
|
|
7
|
+
* must sit near the end of <body>. Rendered only when the tenant has a store
|
|
8
|
+
* (locals.commerce); marketing tenants ship nothing.
|
|
9
|
+
*
|
|
10
|
+
* Note: Foxy's loader is a mutable, versioned CDN artifact and does NOT support
|
|
11
|
+
* Subresource Integrity — pinning a hash would break it on Foxy's next release.
|
|
12
|
+
* It's constrained instead by the per-tenant CSP allowlist (cdn.foxycart.com +
|
|
13
|
+
* the store origin) set in middleware. It is loaded as a plain cross-origin
|
|
14
|
+
* script (NO `crossorigin` attribute) — cdn.foxycart.com serves it without CORS
|
|
15
|
+
* headers, so `crossorigin="anonymous"` would make the browser block it.
|
|
16
|
+
*/
|
|
17
|
+
const { commerce } = Astro.locals;
|
|
18
|
+
---
|
|
19
|
+
{
|
|
20
|
+
commerce && (
|
|
21
|
+
<script src={commerce.loaderSrc} async defer></script>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <FoxySidecartStyles> — brands Foxy's v2.0 sidecart to the tenant's theme.
|
|
4
|
+
*
|
|
5
|
+
* Foxy's loader.js injects the sidecart into OUR page DOM (root `#fc`, light DOM
|
|
6
|
+
* — no iframe, no shadow root), so it inherits the theme's CSS custom properties
|
|
7
|
+
* (`--color-primary`, `--font-*`, …) and can be restyled with plain CSS. We emit
|
|
8
|
+
* a small nonce'd <style> that maps the sidecart's own selectors to those tokens,
|
|
9
|
+
* so a merchant's cart matches their brand out of the box.
|
|
10
|
+
*
|
|
11
|
+
* Two layers: this brand BASELINE, then the tenant's optional `sidecartCss`
|
|
12
|
+
* override (commerce.foxy.sidecartCss) appended verbatim for bespoke tweaks.
|
|
13
|
+
*
|
|
14
|
+
* `!important` is deliberate: Foxy's own responsive_styles.css loads from its CDN
|
|
15
|
+
* AFTER this <head> <style>, so equal-specificity rules would lose the cascade.
|
|
16
|
+
* Everything is scoped under `#fc` so nothing leaks to the storefront itself.
|
|
17
|
+
*
|
|
18
|
+
* Rendered only for commerce tenants with a store (Astro.locals.commerce); the
|
|
19
|
+
* hosted checkout PAGE is a separate origin and is themed in the Foxy admin — out
|
|
20
|
+
* of scope here.
|
|
21
|
+
*/
|
|
22
|
+
const { commerce, cspNonce } = Astro.locals;
|
|
23
|
+
|
|
24
|
+
// Selectors verified against the live tot-preview sidecart DOM:
|
|
25
|
+
// #fc — sidecart root (in-page)
|
|
26
|
+
// #fc-cart.fc-container — cart body
|
|
27
|
+
// .fc-action--checkout--button — the primary "Proceed to Checkout" CTA
|
|
28
|
+
// a.fc-btn-action.fc-sidecart-button — the fixed Keep-Shopping/Checkout nav
|
|
29
|
+
const baseline = `
|
|
30
|
+
#fc, #fc input, #fc button, #fc select, #fc textarea {
|
|
31
|
+
font-family: var(--font-body), system-ui, -apple-system, sans-serif;
|
|
32
|
+
}
|
|
33
|
+
#fc h1, #fc h2, #fc h3, #fc .fc-cart-title, #fc [class*="__title"] {
|
|
34
|
+
font-family: var(--font-display), var(--font-body), sans-serif !important;
|
|
35
|
+
}
|
|
36
|
+
#fc #fc-cart { color: var(--color-text); }
|
|
37
|
+
#fc a { color: var(--color-primary); }
|
|
38
|
+
/* Primary checkout CTA(s) → brand primary. */
|
|
39
|
+
#fc .fc-action--checkout--button,
|
|
40
|
+
#fc a.fc-action--checkout--button,
|
|
41
|
+
#fc .fc-btn-action.fc-sidecart-button--checkout {
|
|
42
|
+
background: var(--color-primary) !important;
|
|
43
|
+
border-color: var(--color-primary) !important;
|
|
44
|
+
color: var(--color-primary-contrast) !important;
|
|
45
|
+
border-radius: var(--radius-md) !important;
|
|
46
|
+
}
|
|
47
|
+
#fc .fc-action--checkout--button:hover,
|
|
48
|
+
#fc a.fc-action--checkout--button:hover {
|
|
49
|
+
filter: brightness(1.08);
|
|
50
|
+
}
|
|
51
|
+
/* Quantity / minor actions keep an outline in brand color. */
|
|
52
|
+
#fc .fc-btn-action.fc-sidecart-button {
|
|
53
|
+
border-radius: var(--radius-md) !important;
|
|
54
|
+
}
|
|
55
|
+
`.trim();
|
|
56
|
+
|
|
57
|
+
const css = commerce?.sidecartCss
|
|
58
|
+
? `${baseline}\n\n/* --- tenant override --- */\n${commerce.sidecartCss}`
|
|
59
|
+
: baseline;
|
|
60
|
+
---
|
|
61
|
+
{commerce && <style set:html={css} nonce={cspNonce}></style>}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <PriceDisplay> — atom. Renders a product price with:
|
|
4
|
+
* - struck compare-at price when on sale,
|
|
5
|
+
* - a "From $X.XX" prefix when the product's variants span a price range,
|
|
6
|
+
* - an optional sale-percent badge.
|
|
7
|
+
*
|
|
8
|
+
* Tenant-agnostic: colours come from theme tokens (`text-sale`, `text-muted`).
|
|
9
|
+
* Pass a `product` (it derives everything) OR the primitives directly, so the
|
|
10
|
+
* atom is usable outside a CatalogProduct context (cart line, PDP, fixtures).
|
|
11
|
+
*/
|
|
12
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
13
|
+
import { formatPrice, discountPercent } from "@/lib/format";
|
|
14
|
+
import { fromPrice, isOnSale } from "@/lib/tot/query";
|
|
15
|
+
import Badge from "@/components/Badge.astro";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Derive amount/compareAt/from/currency from a product. */
|
|
19
|
+
product?: CatalogProduct;
|
|
20
|
+
/** Explicit amount (overrides product). */
|
|
21
|
+
amount?: number;
|
|
22
|
+
/** Struck original price; only shown when > amount. */
|
|
23
|
+
compareAt?: number | null;
|
|
24
|
+
/** Prefix with "From " (variants span a range). */
|
|
25
|
+
from?: boolean;
|
|
26
|
+
currency?: string;
|
|
27
|
+
locale?: string;
|
|
28
|
+
/** Show the "-N%" sale badge next to the price. */
|
|
29
|
+
showBadge?: boolean;
|
|
30
|
+
/** Type scale. */
|
|
31
|
+
size?: "sm" | "md" | "lg";
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
product,
|
|
37
|
+
amount: amountProp,
|
|
38
|
+
compareAt: compareAtProp,
|
|
39
|
+
from: fromProp,
|
|
40
|
+
currency: currencyProp,
|
|
41
|
+
locale = "en-US",
|
|
42
|
+
showBadge = false,
|
|
43
|
+
size = "md",
|
|
44
|
+
class: className,
|
|
45
|
+
} = Astro.props;
|
|
46
|
+
|
|
47
|
+
// Derive from the product when one is supplied; explicit props win.
|
|
48
|
+
const derivedAmount = product ? fromPrice(product) : 0;
|
|
49
|
+
const amount = amountProp ?? derivedAmount;
|
|
50
|
+
const currency = currencyProp ?? product?.price_range.currency ?? "USD";
|
|
51
|
+
|
|
52
|
+
const productHasRange = !!product && product.price_range.min !== product.price_range.max;
|
|
53
|
+
const from = fromProp ?? productHasRange;
|
|
54
|
+
|
|
55
|
+
// Highest compare-at across variants (only meaningful while on sale).
|
|
56
|
+
const derivedCompareAt =
|
|
57
|
+
product && isOnSale(product)
|
|
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
|
+
const compareAt = compareAtProp ?? derivedCompareAt;
|
|
65
|
+
|
|
66
|
+
const onSale = compareAt != null && compareAt > amount;
|
|
67
|
+
const percent = onSale ? discountPercent(amount, compareAt) : null;
|
|
68
|
+
|
|
69
|
+
const amountClass =
|
|
70
|
+
size === "lg" ? "text-lg" : size === "sm" ? "text-[0.8rem]" : "text-sm";
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
<p class:list={["flex items-baseline gap-2", className]}>
|
|
74
|
+
<span
|
|
75
|
+
class:list={[
|
|
76
|
+
"font-display font-medium tabular-nums",
|
|
77
|
+
amountClass,
|
|
78
|
+
onSale && "text-sale",
|
|
79
|
+
]}
|
|
80
|
+
>
|
|
81
|
+
{from && <span class="text-muted">From </span>}{formatPrice(amount, currency, locale)}
|
|
82
|
+
</span>
|
|
83
|
+
{
|
|
84
|
+
onSale && compareAt != null && (
|
|
85
|
+
<span class="text-xs text-muted line-through tabular-nums">
|
|
86
|
+
{formatPrice(compareAt, currency, locale)}
|
|
87
|
+
</span>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
{showBadge && percent != null && <Badge variant="sale" label={`-${percent}%`} />}
|
|
91
|
+
</p>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <ProductCarousel> — a horizontal-scroll row of <ProductCard>s bound to a
|
|
4
|
+
* SectionSource, with optional CSS-only tabs (each tab is its own SectionSource).
|
|
5
|
+
* Tenant-agnostic: the source(s) decide the products, theme tokens decide the
|
|
6
|
+
* skin. Presets (BestSellers / NewArrivals / OnSale) are just this component with
|
|
7
|
+
* a `SECTION_PRESETS.*` source — there are no bespoke preset components.
|
|
8
|
+
*
|
|
9
|
+
* Tabs are pure CSS (radio + adjacent-sibling reveal, labels pulled to the top
|
|
10
|
+
* row with flex `order`) — zero JS, so it works identically under the app Layout
|
|
11
|
+
* and in the standalone style guide.
|
|
12
|
+
*
|
|
13
|
+
* Data: pass `products` (already-resolved / fixtures) OR a `source` this resolves
|
|
14
|
+
* through `Astro.locals.tot`; or `tabs` for the multi-source tabbed variant.
|
|
15
|
+
*/
|
|
16
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
17
|
+
import Section from "@/components/Section.astro";
|
|
18
|
+
import ProductCard from "@/components/ProductCard.astro";
|
|
19
|
+
import {
|
|
20
|
+
resolveSectionSource,
|
|
21
|
+
DEFAULT_SECTION_LIMIT,
|
|
22
|
+
type SectionSource,
|
|
23
|
+
} from "@/lib/sections";
|
|
24
|
+
|
|
25
|
+
interface Tab {
|
|
26
|
+
label: string;
|
|
27
|
+
source?: SectionSource;
|
|
28
|
+
/** Pre-resolved products for this tab (skips resolution). */
|
|
29
|
+
products?: CatalogProduct[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
source?: SectionSource;
|
|
34
|
+
/** Pre-resolved products (skips resolution). Wins over `source`. */
|
|
35
|
+
products?: CatalogProduct[];
|
|
36
|
+
/** Tabbed variant: each tab its own SectionSource. Wins over source/products. */
|
|
37
|
+
tabs?: Tab[];
|
|
38
|
+
limit?: number;
|
|
39
|
+
title?: string;
|
|
40
|
+
eyebrow?: string;
|
|
41
|
+
/** Visible-cards density at the lg breakpoint. */
|
|
42
|
+
columns?: 2 | 3 | 4 | 5;
|
|
43
|
+
/** `compact` tightens the gap. */
|
|
44
|
+
variant?: "default" | "compact";
|
|
45
|
+
tone?: "bg" | "surface";
|
|
46
|
+
class?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
source,
|
|
51
|
+
products: provided,
|
|
52
|
+
tabs,
|
|
53
|
+
limit = DEFAULT_SECTION_LIMIT,
|
|
54
|
+
title,
|
|
55
|
+
eyebrow,
|
|
56
|
+
columns = 4,
|
|
57
|
+
variant = "default",
|
|
58
|
+
tone = "bg",
|
|
59
|
+
class: className,
|
|
60
|
+
} = Astro.props;
|
|
61
|
+
|
|
62
|
+
const tot = Astro.locals.tot;
|
|
63
|
+
|
|
64
|
+
async function resolveTab(t: Tab): Promise<CatalogProduct[]> {
|
|
65
|
+
if (t.products) return t.products;
|
|
66
|
+
return t.source ? resolveSectionSource(t.source, tot, limit) : [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Normalize everything to a list of panels so the template is uniform.
|
|
70
|
+
type Panel = { label: string; products: CatalogProduct[] };
|
|
71
|
+
let panels: Panel[];
|
|
72
|
+
if (tabs && tabs.length > 0) {
|
|
73
|
+
panels = await Promise.all(
|
|
74
|
+
tabs.map(async (t) => ({ label: t.label, products: await resolveTab(t) })),
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
const products =
|
|
78
|
+
provided ?? (source ? await resolveSectionSource(source, tot, limit) : []);
|
|
79
|
+
panels = [{ label: title ?? "", products }];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const showTabs = panels.length > 1;
|
|
83
|
+
|
|
84
|
+
// Unique-per-instance id so multiple carousels on one page don't share radios.
|
|
85
|
+
const uid = "carousel-" + Math.random().toString(36).slice(2, 9);
|
|
86
|
+
|
|
87
|
+
// lg card min-width by density (mobile/sm are fixed for a natural peek).
|
|
88
|
+
const lgWidth: Record<NonNullable<Props["columns"]>, string> = {
|
|
89
|
+
2: "lg:min-w-[48%]",
|
|
90
|
+
3: "lg:min-w-[31%]",
|
|
91
|
+
4: "lg:min-w-[23%]",
|
|
92
|
+
5: "lg:min-w-[18.4%]",
|
|
93
|
+
};
|
|
94
|
+
const gap = variant === "compact" ? "gap-3" : "gap-5";
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
<Section title={title} eyebrow={eyebrow} tone={tone} class={className}>
|
|
98
|
+
<div class="tot-carousel">
|
|
99
|
+
{
|
|
100
|
+
panels.map((panel, i) => (
|
|
101
|
+
<Fragment>
|
|
102
|
+
{showTabs && (
|
|
103
|
+
<>
|
|
104
|
+
<input
|
|
105
|
+
class="tot-carousel__radio"
|
|
106
|
+
type="radio"
|
|
107
|
+
name={uid}
|
|
108
|
+
id={`${uid}-t${i}`}
|
|
109
|
+
checked={i === 0}
|
|
110
|
+
aria-label={panel.label}
|
|
111
|
+
/>
|
|
112
|
+
<label class="tot-carousel__tab" for={`${uid}-t${i}`}>
|
|
113
|
+
{panel.label}
|
|
114
|
+
</label>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
<div class="tot-carousel__panel">
|
|
118
|
+
{panel.products.length > 0 ? (
|
|
119
|
+
<ul
|
|
120
|
+
class:list={[
|
|
121
|
+
"flex snap-x snap-mandatory overflow-x-auto scroll-smooth pb-2",
|
|
122
|
+
gap,
|
|
123
|
+
"[scrollbar-width:thin]",
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
{panel.products.map((p, idx) => (
|
|
127
|
+
<li
|
|
128
|
+
class:list={[
|
|
129
|
+
"min-w-[72%] shrink-0 snap-start sm:min-w-[42%] md:min-w-[31%]",
|
|
130
|
+
lgWidth[columns],
|
|
131
|
+
]}
|
|
132
|
+
>
|
|
133
|
+
<ProductCard product={p} position={idx} list={panel.label} />
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
) : (
|
|
138
|
+
<p class="py-8 text-center text-sm text-muted">No products to show.</p>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</Fragment>
|
|
142
|
+
))
|
|
143
|
+
}
|
|
144
|
+
</div>
|
|
145
|
+
</Section>
|
|
146
|
+
|
|
147
|
+
<style>
|
|
148
|
+
/* CSS-only tabs: radios hidden; labels pulled to a top row via flex order; the
|
|
149
|
+
checked radio's adjacent panel is the only one shown. No JS, CSP-clean. */
|
|
150
|
+
.tot-carousel {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-wrap: wrap;
|
|
153
|
+
align-items: flex-start;
|
|
154
|
+
}
|
|
155
|
+
.tot-carousel__radio {
|
|
156
|
+
position: absolute;
|
|
157
|
+
width: 1px;
|
|
158
|
+
height: 1px;
|
|
159
|
+
opacity: 0;
|
|
160
|
+
pointer-events: none;
|
|
161
|
+
}
|
|
162
|
+
.tot-carousel__tab {
|
|
163
|
+
order: -1;
|
|
164
|
+
margin: 0 0.5rem 1.25rem 0;
|
|
165
|
+
padding: 0.35rem 0.9rem;
|
|
166
|
+
border: 1px solid var(--color-border);
|
|
167
|
+
border-radius: var(--radius-full);
|
|
168
|
+
font-family: var(--font-mono);
|
|
169
|
+
font-size: 0.72rem;
|
|
170
|
+
letter-spacing: 0.08em;
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
color: var(--color-muted);
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
|
175
|
+
}
|
|
176
|
+
.tot-carousel__tab:hover {
|
|
177
|
+
color: var(--color-text);
|
|
178
|
+
}
|
|
179
|
+
.tot-carousel__radio:focus-visible + .tot-carousel__tab {
|
|
180
|
+
outline: 2px solid var(--color-accent);
|
|
181
|
+
outline-offset: 2px;
|
|
182
|
+
}
|
|
183
|
+
.tot-carousel__radio:checked + .tot-carousel__tab {
|
|
184
|
+
background: var(--color-primary);
|
|
185
|
+
border-color: var(--color-primary);
|
|
186
|
+
color: var(--color-primary-contrast);
|
|
187
|
+
}
|
|
188
|
+
.tot-carousel__panel {
|
|
189
|
+
display: none;
|
|
190
|
+
width: 100%;
|
|
191
|
+
}
|
|
192
|
+
/* Single-panel (untabbed) carousels have no radio sibling — show always. */
|
|
193
|
+
.tot-carousel__panel:first-child,
|
|
194
|
+
.tot-carousel__radio:checked + .tot-carousel__tab + .tot-carousel__panel {
|
|
195
|
+
display: block;
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <ProductGrid> — a paginated-look grid of <ProductCard>s bound to a
|
|
4
|
+
* SectionSource. Thin sibling of <ProductCarousel> (same card, non-scrolling
|
|
5
|
+
* layout). Tenant-agnostic: the source decides the products, theme tokens decide
|
|
6
|
+
* the skin, `columns` tunes density.
|
|
7
|
+
*
|
|
8
|
+
* Data: pass `products` directly (e.g. fixtures / already-resolved) OR a `source`
|
|
9
|
+
* that this component resolves through `Astro.locals.tot`.
|
|
10
|
+
*/
|
|
11
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
12
|
+
import Section from "@/components/Section.astro";
|
|
13
|
+
import ProductCard from "@/components/ProductCard.astro";
|
|
14
|
+
import EmptyState from "@/components/EmptyState.astro";
|
|
15
|
+
import {
|
|
16
|
+
resolveSectionSource,
|
|
17
|
+
DEFAULT_SECTION_LIMIT,
|
|
18
|
+
type SectionSource,
|
|
19
|
+
} from "@/lib/sections";
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
source?: SectionSource;
|
|
23
|
+
/** Pre-resolved products (skips resolution). Wins over `source`. */
|
|
24
|
+
products?: CatalogProduct[];
|
|
25
|
+
limit?: number;
|
|
26
|
+
title?: string;
|
|
27
|
+
eyebrow?: string;
|
|
28
|
+
/** Grid columns at the lg breakpoint (2 on mobile). */
|
|
29
|
+
columns?: 2 | 3 | 4 | 5;
|
|
30
|
+
tone?: "bg" | "surface";
|
|
31
|
+
/** Hide the whole section when there are no products (default true). */
|
|
32
|
+
hideWhenEmpty?: boolean;
|
|
33
|
+
class?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
source,
|
|
38
|
+
products: provided,
|
|
39
|
+
limit = DEFAULT_SECTION_LIMIT,
|
|
40
|
+
title,
|
|
41
|
+
eyebrow,
|
|
42
|
+
columns = 4,
|
|
43
|
+
tone = "bg",
|
|
44
|
+
hideWhenEmpty = true,
|
|
45
|
+
class: className,
|
|
46
|
+
} = Astro.props;
|
|
47
|
+
|
|
48
|
+
const products =
|
|
49
|
+
provided ??
|
|
50
|
+
(source ? await resolveSectionSource(source, Astro.locals.tot, limit) : []);
|
|
51
|
+
|
|
52
|
+
const colClass: Record<NonNullable<Props["columns"]>, string> = {
|
|
53
|
+
2: "grid-cols-2",
|
|
54
|
+
3: "grid-cols-2 md:grid-cols-3",
|
|
55
|
+
4: "grid-cols-2 lg:grid-cols-4",
|
|
56
|
+
5: "grid-cols-2 md:grid-cols-3 lg:grid-cols-5",
|
|
57
|
+
};
|
|
58
|
+
const hidden = hideWhenEmpty && products.length === 0;
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
!hidden && (
|
|
63
|
+
<Section title={title} eyebrow={eyebrow} tone={tone} class={className}>
|
|
64
|
+
{products.length > 0 ? (
|
|
65
|
+
<div class:list={["grid gap-5", colClass[columns]]}>
|
|
66
|
+
{products.map((p, i) => (
|
|
67
|
+
<ProductCard product={p} position={i} list={title} />
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
) : (
|
|
71
|
+
<EmptyState title="Nothing here yet" body="Check back soon." />
|
|
72
|
+
)}
|
|
73
|
+
</Section>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <RatingStars> — atom. A 0–5 star rating with fractional fill (a single clipped
|
|
4
|
+
* overlay, no half-star glyph juggling) and an accessible label. Tenant-agnostic:
|
|
5
|
+
* filled stars use `--color-accent`, the empty track uses `--color-border`.
|
|
6
|
+
*
|
|
7
|
+
* Presentation only — it renders whatever aggregate you pass. No review data is
|
|
8
|
+
* invented here (claims-ledger discipline: source ratings before shipping them).
|
|
9
|
+
*/
|
|
10
|
+
interface Props {
|
|
11
|
+
/** 0–5. */
|
|
12
|
+
rating: number;
|
|
13
|
+
/** Optional review count shown after the stars. */
|
|
14
|
+
count?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
size?: "sm" | "md";
|
|
17
|
+
/** Show the numeric "(N)" count. */
|
|
18
|
+
showCount?: boolean;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
rating,
|
|
24
|
+
count,
|
|
25
|
+
max = 5,
|
|
26
|
+
size = "sm",
|
|
27
|
+
showCount = true,
|
|
28
|
+
class: className,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const clamped = Math.max(0, Math.min(max, rating));
|
|
32
|
+
const pct = (clamped / max) * 100;
|
|
33
|
+
const dim = size === "md" ? "text-base" : "text-xs";
|
|
34
|
+
const stars = "★".repeat(max);
|
|
35
|
+
const label =
|
|
36
|
+
count != null
|
|
37
|
+
? `Rated ${clamped} out of ${max} — ${count} review${count === 1 ? "" : "s"}`
|
|
38
|
+
: `Rated ${clamped} out of ${max}`;
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<span class:list={["inline-flex items-center gap-1.5", className]} aria-label={label}>
|
|
42
|
+
<span class:list={["relative inline-block leading-none tracking-[0.08em]", dim]} aria-hidden="true">
|
|
43
|
+
<span class="text-[var(--color-border)]">{stars}</span>
|
|
44
|
+
<span
|
|
45
|
+
class="absolute inset-0 overflow-hidden whitespace-nowrap text-[var(--color-accent)]"
|
|
46
|
+
style={`width:${pct}%`}
|
|
47
|
+
>
|
|
48
|
+
{stars}
|
|
49
|
+
</span>
|
|
50
|
+
</span>
|
|
51
|
+
{
|
|
52
|
+
showCount && count != null && (
|
|
53
|
+
<span class="font-mono text-[0.62rem] text-muted" aria-hidden="true">
|
|
54
|
+
({count})
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
</span>
|