@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,55 @@
|
|
|
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
|
+
/** Extra, lower-emphasis recovery links (e.g. "clear just the price range"),
|
|
14
|
+
* shown under the primary CTA. Optional — most callers have only one CTA. */
|
|
15
|
+
secondaryActions?: { label: string; href: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { title, body, ctaLabel, ctaHref, secondaryActions } = Astro.props;
|
|
19
|
+
const { basePath } = Astro.locals;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
class="mx-auto flex max-w-md flex-col items-center px-6 py-20 text-center sm:py-28"
|
|
24
|
+
data-reveal
|
|
25
|
+
>
|
|
26
|
+
<h2 class="display-md">{title}</h2>
|
|
27
|
+
{body && <p class="mt-3 max-w-prose text-muted">{body}</p>}
|
|
28
|
+
{
|
|
29
|
+
ctaLabel && ctaHref && (
|
|
30
|
+
<a
|
|
31
|
+
href={withBase(basePath, ctaHref)}
|
|
32
|
+
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"
|
|
33
|
+
>
|
|
34
|
+
{ctaLabel}
|
|
35
|
+
</a>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
{
|
|
39
|
+
secondaryActions && secondaryActions.length > 0 && (
|
|
40
|
+
<ul class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5">
|
|
41
|
+
{secondaryActions.map((action) => (
|
|
42
|
+
<li>
|
|
43
|
+
<a
|
|
44
|
+
href={withBase(basePath, action.href)}
|
|
45
|
+
rel="nofollow"
|
|
46
|
+
class="text-sm text-muted underline underline-offset-2 hover:text-[var(--color-accent-ink)]"
|
|
47
|
+
>
|
|
48
|
+
{action.label}
|
|
49
|
+
</a>
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
</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>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Seo> — per-page title/meta, Open Graph, Twitter card, canonical, JSON-LD.
|
|
4
|
+
* (brief §8). Tenant-aware: OG image + site name come from the resolved tenant.
|
|
5
|
+
*/
|
|
6
|
+
interface JsonLd {
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
/** Path-only canonical, e.g. /products/foo. Joined to tenant canonical base. */
|
|
14
|
+
path: string;
|
|
15
|
+
ogType?: "website" | "product" | "article";
|
|
16
|
+
ogImage?: string;
|
|
17
|
+
noindex?: boolean;
|
|
18
|
+
/** One or more JSON-LD graphs (Product, BreadcrumbList, Organization…). */
|
|
19
|
+
jsonLd?: JsonLd | JsonLd[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
title,
|
|
24
|
+
description,
|
|
25
|
+
path,
|
|
26
|
+
ogType = "website",
|
|
27
|
+
ogImage,
|
|
28
|
+
noindex = false,
|
|
29
|
+
jsonLd,
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
|
|
32
|
+
const { tenant, canonicalBase } = Astro.locals;
|
|
33
|
+
const canonical = new URL(path, canonicalBase + "/").toString();
|
|
34
|
+
// Don't double-append the brand: merchant SEO titles (title_tag) often already
|
|
35
|
+
// end with the store name.
|
|
36
|
+
const fullTitle = title.includes(tenant.displayName)
|
|
37
|
+
? title
|
|
38
|
+
: `${title} — ${tenant.displayName}`;
|
|
39
|
+
const image = ogImage
|
|
40
|
+
? new URL(ogImage, canonicalBase + "/").toString()
|
|
41
|
+
: new URL(tenant.theme_tokens?.brand?.ogImage ?? "/brand/northwind-og.svg", canonicalBase + "/").toString();
|
|
42
|
+
|
|
43
|
+
const graphs = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<title>{fullTitle}</title>
|
|
47
|
+
{description && <meta name="description" content={description} />}
|
|
48
|
+
<link rel="canonical" href={canonical} />
|
|
49
|
+
{noindex && <meta name="robots" content="noindex,nofollow" />}
|
|
50
|
+
|
|
51
|
+
<meta property="og:type" content={ogType} />
|
|
52
|
+
<meta property="og:site_name" content={tenant.displayName} />
|
|
53
|
+
<meta property="og:title" content={fullTitle} />
|
|
54
|
+
{description && <meta property="og:description" content={description} />}
|
|
55
|
+
<meta property="og:url" content={canonical} />
|
|
56
|
+
<meta property="og:image" content={image} />
|
|
57
|
+
|
|
58
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
59
|
+
<meta name="twitter:title" content={fullTitle} />
|
|
60
|
+
{description && <meta name="twitter:description" content={description} />}
|
|
61
|
+
<meta name="twitter:image" content={image} />
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
graphs.map((g) => (
|
|
65
|
+
<script
|
|
66
|
+
type="application/ld+json"
|
|
67
|
+
set:html={JSON.stringify(g)}
|
|
68
|
+
/>
|
|
69
|
+
))
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <WidgetFrame> — renders a tenant-registered widget (isolation:"sandbox") in a
|
|
4
|
+
* sandboxed iframe. The sandbox token list (`allow-scripts`, no
|
|
5
|
+
* `allow-same-origin`) puts the widget in an opaque origin, so it cannot read the
|
|
6
|
+
* page's cookies/DOM or its own storage, even though the bundle is served from our
|
|
7
|
+
* own origin. The widget bundle is hash-pinned in CSP and placed into a
|
|
8
|
+
* platform-defined slot.
|
|
9
|
+
*/
|
|
10
|
+
import { widgetFrameAttrs, type ScriptStrategy } from "@tot/public-runtime";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Same-origin URL that loads the tenant's widget bundle. */
|
|
14
|
+
src: string;
|
|
15
|
+
/** Accessible title for the iframe (required for a11y). */
|
|
16
|
+
title: string;
|
|
17
|
+
strategy?: ScriptStrategy;
|
|
18
|
+
/** Optional fixed height; widgets may also self-size via postMessage later. */
|
|
19
|
+
height?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { src, title, strategy = "on-idle", height = "auto" } = Astro.props;
|
|
23
|
+
const attrs = widgetFrameAttrs(strategy);
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<iframe
|
|
27
|
+
src={src}
|
|
28
|
+
title={title}
|
|
29
|
+
sandbox={attrs.sandbox}
|
|
30
|
+
loading={attrs.loading}
|
|
31
|
+
referrerpolicy={attrs.referrerpolicy}
|
|
32
|
+
class="tot-widget-frame"
|
|
33
|
+
style={`border:0;width:100%;height:${height};`}
|
|
34
|
+
></iframe>
|