@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,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AuthBadge — a small "signed in as … — <role>" pill shown on login-gated hosts.
|
|
4
|
+
*
|
|
5
|
+
* Renders only when `Astro.locals.viewer` is set (i.e. the login gate resolved a
|
|
6
|
+
* valid session). Surfaces the viewer's email + effective capability (with the
|
|
7
|
+
* full role list on hover) and a sign-out control — the "report back what role(s)
|
|
8
|
+
* they're logged in with" affordance. Absent on open/password-gated hosts.
|
|
9
|
+
*/
|
|
10
|
+
const viewer = Astro.locals.viewer;
|
|
11
|
+
|
|
12
|
+
const CAP_LABEL: Record<string, string> = {
|
|
13
|
+
owner: "Owner",
|
|
14
|
+
admin: "Admin",
|
|
15
|
+
member: "Member",
|
|
16
|
+
developer: "Developer",
|
|
17
|
+
inviter: "Inviter",
|
|
18
|
+
staff: "ToT Staff",
|
|
19
|
+
};
|
|
20
|
+
const label = viewer ? (CAP_LABEL[viewer.capability] ?? viewer.capability) : "";
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
{viewer && (
|
|
24
|
+
<div
|
|
25
|
+
data-auth-badge
|
|
26
|
+
title={`Signed in as ${viewer.email} — role(s): ${viewer.roles.join(", ") || "none"}`}
|
|
27
|
+
class="fixed bottom-3 left-3 z-50 flex items-center gap-2 rounded-full border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-1.5 text-xs shadow-sm"
|
|
28
|
+
>
|
|
29
|
+
<span aria-hidden="true">🔓</span>
|
|
30
|
+
<span class="text-[var(--color-muted)]">Signed in as</span>
|
|
31
|
+
<span class="font-semibold">{viewer.email}</span>
|
|
32
|
+
<span class="rounded-full bg-[var(--color-primary)] px-2 py-0.5 font-semibold text-[var(--color-primary-contrast)]">
|
|
33
|
+
{label}
|
|
34
|
+
</span>
|
|
35
|
+
<form method="post" action="/api/auth/logout" class="contents">
|
|
36
|
+
<button
|
|
37
|
+
type="submit"
|
|
38
|
+
class="text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-primary)]"
|
|
39
|
+
>
|
|
40
|
+
Sign out
|
|
41
|
+
</button>
|
|
42
|
+
</form>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <AnnouncementBar> — the top promo strip. Two variants:
|
|
4
|
+
* - "static" : centered single line.
|
|
5
|
+
* - "marquee" : a continuously scrolling track (duplicated for a seamless loop).
|
|
6
|
+
*
|
|
7
|
+
* Reduced-motion safe: `prefers-reduced-motion: reduce` freezes the marquee and
|
|
8
|
+
* shows the message statically (no infinite animation). Tenant-agnostic — colours
|
|
9
|
+
* come from theme tokens; `tone` picks which token pair.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
text: string;
|
|
13
|
+
variant?: "static" | "marquee";
|
|
14
|
+
/** How many times to repeat the phrase in the marquee track. */
|
|
15
|
+
repeat?: number;
|
|
16
|
+
tone?: "primary" | "sale" | "accent";
|
|
17
|
+
/** Accessible label for the region (marquee hides the visual copies). */
|
|
18
|
+
label?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
text,
|
|
24
|
+
variant = "static",
|
|
25
|
+
repeat = 6,
|
|
26
|
+
tone = "primary",
|
|
27
|
+
label,
|
|
28
|
+
class: className,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const toneClass: Record<NonNullable<Props["tone"]>, string> = {
|
|
32
|
+
primary: "bg-primary text-primary-contrast",
|
|
33
|
+
sale: "bg-[var(--color-sale)] text-white",
|
|
34
|
+
accent: "bg-accent text-white",
|
|
35
|
+
};
|
|
36
|
+
const copies = Array.from({ length: Math.max(2, repeat) });
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
class:list={["tot-announce", toneClass[tone], className]}
|
|
41
|
+
role="region"
|
|
42
|
+
aria-label={label ?? text}
|
|
43
|
+
>
|
|
44
|
+
{
|
|
45
|
+
variant === "marquee" ? (
|
|
46
|
+
<div class="tot-announce__viewport">
|
|
47
|
+
<div class="tot-announce__track" aria-hidden="true">
|
|
48
|
+
{copies.map(() => (
|
|
49
|
+
<span class="tot-announce__item">{text}</span>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
{/* Static fallback shown only when motion is reduced. */}
|
|
53
|
+
<p class="tot-announce__static">{text}</p>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<p class="tot-announce__line">{text}</p>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.tot-announce {
|
|
63
|
+
font-family: var(--font-mono);
|
|
64
|
+
font-size: 0.72rem;
|
|
65
|
+
letter-spacing: 0.08em;
|
|
66
|
+
}
|
|
67
|
+
.tot-announce__line {
|
|
68
|
+
text-align: center;
|
|
69
|
+
padding: 0.375rem 1rem;
|
|
70
|
+
}
|
|
71
|
+
.tot-announce__viewport {
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
padding: 0.375rem 0;
|
|
75
|
+
}
|
|
76
|
+
.tot-announce__track {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
gap: 3rem;
|
|
79
|
+
padding-left: 3rem;
|
|
80
|
+
will-change: transform;
|
|
81
|
+
animation: tot-marquee 22s linear infinite;
|
|
82
|
+
}
|
|
83
|
+
.tot-announce__item {
|
|
84
|
+
display: inline-block;
|
|
85
|
+
}
|
|
86
|
+
.tot-announce__static {
|
|
87
|
+
display: none;
|
|
88
|
+
text-align: center;
|
|
89
|
+
}
|
|
90
|
+
@keyframes tot-marquee {
|
|
91
|
+
from {
|
|
92
|
+
transform: translateX(0);
|
|
93
|
+
}
|
|
94
|
+
to {
|
|
95
|
+
/* Track is duplicated content; -50% loops seamlessly. */
|
|
96
|
+
transform: translateX(-50%);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
@media (prefers-reduced-motion: reduce) {
|
|
100
|
+
.tot-announce__track {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
.tot-announce__static {
|
|
104
|
+
display: block;
|
|
105
|
+
padding: 0 1rem;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <MegaMenu> — the primary navigation bar: a horizontal list of nav items, each
|
|
4
|
+
* rendered by <NavDropdown> (plain link, single-column dropdown, or multi-column
|
|
5
|
+
* mega panel). Tenant-agnostic; theme-token skinned; CSS-only reveal.
|
|
6
|
+
*
|
|
7
|
+
* Used by <SiteHeader> for the desktop nav, and registerable standalone in the
|
|
8
|
+
* style guide.
|
|
9
|
+
*/
|
|
10
|
+
import type { ChromeNavItem } from "@/lib/chrome/model";
|
|
11
|
+
import NavDropdown from "@/components/chrome/NavDropdown.astro";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
items: ChromeNavItem[];
|
|
15
|
+
ariaLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { items, ariaLabel = "Primary", class: className } = Astro.props;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<nav aria-label={ariaLabel} class:list={["hidden md:block", className]}>
|
|
23
|
+
<ul class="flex items-center gap-6">
|
|
24
|
+
{items.map((item) => <NavDropdown item={item} />)}
|
|
25
|
+
</ul>
|
|
26
|
+
</nav>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <NavDropdown> — one primary-nav item, in one of two delivery modes:
|
|
4
|
+
*
|
|
5
|
+
* mode="bar" (default) — desktop horizontal item. Renders as a plain link,
|
|
6
|
+
* a single-column dropdown (`children`), or a multi-column mega panel
|
|
7
|
+
* (`columns`), revealed on hover/focus via CSS-only group state (no JS,
|
|
8
|
+
* keyboard-accessible: focusing the trigger opens the panel).
|
|
9
|
+
*
|
|
10
|
+
* mode="accordion" — mobile/stacked item. The same item, but its dropdown or
|
|
11
|
+
* mega panel becomes an inline, collapsible disclosure built on a native
|
|
12
|
+
* <details>/<summary> (zero-JS, keyboard- and touch-operable). Mega
|
|
13
|
+
* `columns` stack as titled groups. Used inside <SiteHeader>'s mobile menu.
|
|
14
|
+
*
|
|
15
|
+
* With an optional caret, a "NEW"-style badge, and a highlighted `flag` variant.
|
|
16
|
+
* Tenant-agnostic; theme-token skinned.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChromeNavItem, ChromeLink } from "@/lib/chrome/model";
|
|
19
|
+
import { withBase } from "@/lib/basePath";
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
item: ChromeNavItem;
|
|
23
|
+
/**
|
|
24
|
+
* "bar" = desktop horizontal item with an absolute hover/focus panel (default);
|
|
25
|
+
* "accordion" = mobile stacked item using a native <details> disclosure.
|
|
26
|
+
*/
|
|
27
|
+
mode?: "bar" | "accordion";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { item, mode = "bar" } = Astro.props;
|
|
31
|
+
const { basePath } = Astro.locals;
|
|
32
|
+
|
|
33
|
+
const itemHref = withBase(basePath, item.href);
|
|
34
|
+
const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
|
|
35
|
+
const hasMenu =
|
|
36
|
+
(item.columns && item.columns.length > 0) ||
|
|
37
|
+
(item.children && item.children.length > 0);
|
|
38
|
+
const isMega = !!(item.columns && item.columns.length > 0);
|
|
39
|
+
// Normalize columns/children into titled groups for the accordion body so the
|
|
40
|
+
// same markup renders a mega panel (multiple titled columns) or a single-column
|
|
41
|
+
// dropdown (one untitled group).
|
|
42
|
+
const groups = isMega
|
|
43
|
+
? item.columns!.map((c) => ({ title: c.title, links: c.links }))
|
|
44
|
+
: item.children
|
|
45
|
+
? [{ title: undefined as string | undefined, links: item.children }]
|
|
46
|
+
: [];
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
mode === "accordion" ? (
|
|
51
|
+
hasMenu ? (
|
|
52
|
+
<li>
|
|
53
|
+
<details class="group border-b border-border last:border-b-0">
|
|
54
|
+
<summary class="flex cursor-pointer list-none items-center justify-between gap-2 py-2.5 text-sm font-medium text-text [&::-webkit-details-marker]:hidden">
|
|
55
|
+
<span class:list={["inline-flex items-center gap-1.5", item.flag && "text-accent"]}>
|
|
56
|
+
{item.label}
|
|
57
|
+
{item.badge && (
|
|
58
|
+
<span class="rounded-full bg-[var(--color-sale)] px-1.5 py-0.5 font-mono text-[0.55rem] uppercase leading-none tracking-[0.08em] text-white">
|
|
59
|
+
{item.badge}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</span>
|
|
63
|
+
<span aria-hidden="true" class="shrink-0 text-[0.7em] text-muted transition-transform duration-150 group-open:rotate-180">
|
|
64
|
+
▾
|
|
65
|
+
</span>
|
|
66
|
+
</summary>
|
|
67
|
+
<div class="pb-2 pl-1">
|
|
68
|
+
{groups.map((col) => (
|
|
69
|
+
<div class="mb-2 last:mb-0">
|
|
70
|
+
{col.title && (
|
|
71
|
+
<p class="px-2 pb-1 pt-1 font-mono text-[0.6rem] uppercase tracking-[0.12em] text-muted">
|
|
72
|
+
{col.title}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
<ul class="grid gap-0.5">
|
|
76
|
+
{col.links.map((child) => (
|
|
77
|
+
<li>
|
|
78
|
+
<a
|
|
79
|
+
href={hrefOf(child)}
|
|
80
|
+
class="flex items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
81
|
+
{...(child.external ? { rel: "noopener noreferrer" } : {})}
|
|
82
|
+
>
|
|
83
|
+
{child.label}
|
|
84
|
+
{child.badge && (
|
|
85
|
+
<span class="rounded-full bg-primary px-1 py-0.5 font-mono text-[0.5rem] uppercase leading-none text-primary-contrast">
|
|
86
|
+
{child.badge}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</a>
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ul>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</details>
|
|
97
|
+
</li>
|
|
98
|
+
) : (
|
|
99
|
+
<li>
|
|
100
|
+
<a
|
|
101
|
+
href={itemHref}
|
|
102
|
+
class:list={[
|
|
103
|
+
"flex items-center gap-1.5 border-b border-border py-2.5 text-sm font-medium text-text transition-colors last:border-b-0 hover:text-accent",
|
|
104
|
+
item.flag && "text-accent",
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
{item.label}
|
|
108
|
+
{item.badge && (
|
|
109
|
+
<span class="rounded-full bg-[var(--color-sale)] px-1.5 py-0.5 font-mono text-[0.55rem] uppercase leading-none tracking-[0.08em] text-white">
|
|
110
|
+
{item.badge}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</a>
|
|
114
|
+
</li>
|
|
115
|
+
)
|
|
116
|
+
) : hasMenu ? (
|
|
117
|
+
<li class="group relative">
|
|
118
|
+
<a
|
|
119
|
+
href={itemHref}
|
|
120
|
+
class:list={[
|
|
121
|
+
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent group-focus-within:text-accent",
|
|
122
|
+
item.flag && "font-semibold text-accent",
|
|
123
|
+
]}
|
|
124
|
+
aria-haspopup="true"
|
|
125
|
+
>
|
|
126
|
+
{item.label}
|
|
127
|
+
{item.badge && (
|
|
128
|
+
<span class="ml-1 rounded-full bg-[var(--color-sale)] px-1.5 py-0.5 font-mono text-[0.55rem] uppercase tracking-[0.08em] leading-none text-white">
|
|
129
|
+
{item.badge}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
<span aria-hidden="true" class="text-[0.7em] opacity-60">▾</span>
|
|
133
|
+
</a>
|
|
134
|
+
|
|
135
|
+
<div class="invisible absolute left-0 top-full z-50 translate-y-1 opacity-0 transition-all duration-150 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100">
|
|
136
|
+
{isMega ? (
|
|
137
|
+
<div class="mt-1 grid gap-6 rounded-md border border-border bg-surface p-5 shadow-md sm:grid-cols-2 lg:grid-cols-3">
|
|
138
|
+
{item.columns!.map((col) => (
|
|
139
|
+
<div class="min-w-44">
|
|
140
|
+
{col.title && (
|
|
141
|
+
<p class="mb-2 font-mono text-[0.62rem] uppercase tracking-[0.12em] text-muted">
|
|
142
|
+
{col.title}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
<ul class="grid gap-0.5">
|
|
146
|
+
{col.links.map((child) => (
|
|
147
|
+
<li>
|
|
148
|
+
<a
|
|
149
|
+
href={hrefOf(child)}
|
|
150
|
+
class="flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
151
|
+
{...(child.external ? { rel: "noopener noreferrer" } : {})}
|
|
152
|
+
>
|
|
153
|
+
{child.label}
|
|
154
|
+
{child.badge && (
|
|
155
|
+
<span class="rounded-full bg-primary px-1 py-0.5 font-mono text-[0.5rem] uppercase leading-none text-primary-contrast">
|
|
156
|
+
{child.badge}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</a>
|
|
160
|
+
</li>
|
|
161
|
+
))}
|
|
162
|
+
</ul>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<ul class="mt-1 grid min-w-56 gap-0.5 rounded-md border border-border bg-surface p-3 shadow-md">
|
|
168
|
+
{item.children!.map((child) => (
|
|
169
|
+
<li>
|
|
170
|
+
<a
|
|
171
|
+
href={hrefOf(child)}
|
|
172
|
+
class="block rounded-sm px-3 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
173
|
+
{...(child.external ? { rel: "noopener noreferrer" } : {})}
|
|
174
|
+
>
|
|
175
|
+
{child.label}
|
|
176
|
+
</a>
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</li>
|
|
183
|
+
) : (
|
|
184
|
+
<li>
|
|
185
|
+
<a
|
|
186
|
+
href={itemHref}
|
|
187
|
+
class:list={[
|
|
188
|
+
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent",
|
|
189
|
+
item.flag && "font-semibold text-accent",
|
|
190
|
+
]}
|
|
191
|
+
>
|
|
192
|
+
{item.label}
|
|
193
|
+
{item.badge && (
|
|
194
|
+
<span class="ml-1 rounded-full bg-[var(--color-sale)] px-1.5 py-0.5 font-mono text-[0.55rem] uppercase tracking-[0.08em] leading-none text-white">
|
|
195
|
+
{item.badge}
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
</a>
|
|
199
|
+
</li>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <SiteFooter> — brand block + link columns + newsletter signup + a payment-badge
|
|
4
|
+
* row + a trust/compliance line + legal bottom bar. Tenant-agnostic; theme-token
|
|
5
|
+
* skinned. Payment badges are text chips (no external images — CSP-clean).
|
|
6
|
+
*
|
|
7
|
+
* The newsletter is a plain POST form (progressive); it does not embed any cart
|
|
8
|
+
* or checkout logic (commerce boundary).
|
|
9
|
+
*/
|
|
10
|
+
import type { ChromeLink, FooterColumn, FooterNewsletter, SocialLink, ChromeLogo } from "@/lib/chrome/model";
|
|
11
|
+
import { withBase } from "@/lib/basePath";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
columns: FooterColumn[];
|
|
15
|
+
brand?: { logo?: ChromeLogo; wordmark?: string; contact?: string[] };
|
|
16
|
+
newsletter?: FooterNewsletter;
|
|
17
|
+
/** Payment method labels rendered as chips (e.g. "Visa", "PayPal"). */
|
|
18
|
+
payments?: string[];
|
|
19
|
+
social?: SocialLink[];
|
|
20
|
+
/** Trust / compliance line (plain text, e.g. an age/nicotine notice). */
|
|
21
|
+
compliance?: string;
|
|
22
|
+
/** Legal bottom-bar line (e.g. a composed "© 2026 Acme · some legal note" string). */
|
|
23
|
+
legal?: string;
|
|
24
|
+
/** Bottom-bar policy links (e.g. Privacy Policy, Terms), shown alongside `legal`. */
|
|
25
|
+
bottomLinks?: ChromeLink[];
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
columns,
|
|
31
|
+
brand,
|
|
32
|
+
newsletter,
|
|
33
|
+
payments = [],
|
|
34
|
+
social = [],
|
|
35
|
+
compliance,
|
|
36
|
+
legal,
|
|
37
|
+
bottomLinks = [],
|
|
38
|
+
class: className,
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
const { basePath } = Astro.locals;
|
|
42
|
+
const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
|
|
43
|
+
const showLogo = !!brand?.logo && /^(https?:|\/)/.test(brand.logo.src);
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<footer class:list={["border-t border-border bg-surface text-text", className]}>
|
|
47
|
+
<div class="container-prose py-12">
|
|
48
|
+
<div class="grid gap-10 md:grid-cols-[1.4fr_repeat(3,1fr)]">
|
|
49
|
+
<!-- Brand -->
|
|
50
|
+
<div>
|
|
51
|
+
{
|
|
52
|
+
showLogo ? (
|
|
53
|
+
<img
|
|
54
|
+
src={brand!.logo!.src}
|
|
55
|
+
alt={brand!.logo!.alt}
|
|
56
|
+
width={brand!.logo!.width ?? 160}
|
|
57
|
+
height={brand!.logo!.height ?? 48}
|
|
58
|
+
class="h-9 w-auto max-w-[180px] object-contain"
|
|
59
|
+
/>
|
|
60
|
+
) : (
|
|
61
|
+
brand?.wordmark && (
|
|
62
|
+
<span class="font-display text-lg font-bold tracking-tight">{brand.wordmark}</span>
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
{
|
|
67
|
+
brand?.contact && brand.contact.length > 0 && (
|
|
68
|
+
<p class="mt-3 text-sm text-muted">
|
|
69
|
+
{brand.contact.map((line, i) => (
|
|
70
|
+
<>
|
|
71
|
+
{i > 0 && <br />}
|
|
72
|
+
{line}
|
|
73
|
+
</>
|
|
74
|
+
))}
|
|
75
|
+
</p>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
{
|
|
79
|
+
social.length > 0 && (
|
|
80
|
+
<div class="mt-4 flex items-center gap-3">
|
|
81
|
+
{social.map((s) => (
|
|
82
|
+
<a
|
|
83
|
+
href={s.href}
|
|
84
|
+
aria-label={s.label}
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
class="text-muted transition-colors hover:text-primary"
|
|
87
|
+
>
|
|
88
|
+
{s.iconPath ? (
|
|
89
|
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
90
|
+
<path d={s.iconPath} />
|
|
91
|
+
</svg>
|
|
92
|
+
) : (
|
|
93
|
+
<span class="text-sm">{s.label}</span>
|
|
94
|
+
)}
|
|
95
|
+
</a>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Link columns -->
|
|
103
|
+
{
|
|
104
|
+
columns.map((col) => (
|
|
105
|
+
<div>
|
|
106
|
+
<h3 class="font-display text-sm font-semibold">{col.title}</h3>
|
|
107
|
+
<ul class="mt-3 grid gap-2">
|
|
108
|
+
{col.links.map((l) => (
|
|
109
|
+
<li>
|
|
110
|
+
<a
|
|
111
|
+
href={hrefOf(l)}
|
|
112
|
+
class="text-sm text-muted transition-colors hover:text-primary"
|
|
113
|
+
{...(l.external ? { rel: "noopener noreferrer" } : {})}
|
|
114
|
+
>
|
|
115
|
+
{l.label}
|
|
116
|
+
</a>
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
</div>
|
|
121
|
+
))
|
|
122
|
+
}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Newsletter -->
|
|
126
|
+
{
|
|
127
|
+
newsletter && (
|
|
128
|
+
<div class="mt-10 border-t border-border pt-8">
|
|
129
|
+
{newsletter.title && <h3 class="font-display text-base font-semibold">{newsletter.title}</h3>}
|
|
130
|
+
{newsletter.body && <p class="mt-1 text-sm text-muted">{newsletter.body}</p>}
|
|
131
|
+
<form
|
|
132
|
+
action={withBase(basePath, newsletter.action ?? "/api/newsletter")}
|
|
133
|
+
method="post"
|
|
134
|
+
class="mt-4 flex max-w-md flex-col gap-3 sm:flex-row"
|
|
135
|
+
>
|
|
136
|
+
<input type="hidden" name="source" value={newsletter.source ?? "footer"} />
|
|
137
|
+
<label class="sr-only" for="chrome-news-email">Email address</label>
|
|
138
|
+
<input
|
|
139
|
+
id="chrome-news-email"
|
|
140
|
+
type="email"
|
|
141
|
+
name="email"
|
|
142
|
+
required
|
|
143
|
+
placeholder={newsletter.placeholder ?? "you@example.com"}
|
|
144
|
+
class="flex-1 rounded-full border border-border bg-bg px-4 py-2.5 text-sm text-text placeholder:text-muted"
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
type="submit"
|
|
148
|
+
class="rounded-full bg-primary px-5 py-2.5 font-display text-sm font-medium text-primary-contrast transition-transform hover:-translate-y-0.5"
|
|
149
|
+
>
|
|
150
|
+
Subscribe
|
|
151
|
+
</button>
|
|
152
|
+
</form>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
<!-- Payment badges -->
|
|
158
|
+
{
|
|
159
|
+
payments.length > 0 && (
|
|
160
|
+
<div class="mt-8 flex flex-wrap items-center gap-2" aria-label="Accepted payment methods">
|
|
161
|
+
{payments.map((p) => (
|
|
162
|
+
<span class="rounded border border-border px-2 py-1 font-mono text-[0.62rem] uppercase tracking-[0.08em] text-muted">
|
|
163
|
+
{p}
|
|
164
|
+
</span>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
<!-- Compliance / trust line -->
|
|
171
|
+
{
|
|
172
|
+
compliance && (
|
|
173
|
+
<p class="mt-8 border-t border-border pt-6 text-xs text-muted">{compliance}</p>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
<!-- Legal bottom bar: text line + optional policy links -->
|
|
178
|
+
{
|
|
179
|
+
(legal || bottomLinks.length > 0) && (
|
|
180
|
+
<div class="mt-4 flex flex-col gap-3 border-t border-border pt-4 text-xs text-muted sm:flex-row sm:items-center sm:justify-between">
|
|
181
|
+
{legal && <p class="font-mono text-[0.68rem] uppercase tracking-[0.08em]">{legal}</p>}
|
|
182
|
+
{
|
|
183
|
+
bottomLinks.length > 0 && (
|
|
184
|
+
<nav aria-label="Legal">
|
|
185
|
+
<ul class="flex flex-wrap gap-x-5 gap-y-1">
|
|
186
|
+
{bottomLinks.map((l) => (
|
|
187
|
+
<li>
|
|
188
|
+
<a href={hrefOf(l)} class="transition-colors hover:text-primary">
|
|
189
|
+
{l.label}
|
|
190
|
+
</a>
|
|
191
|
+
</li>
|
|
192
|
+
))}
|
|
193
|
+
</ul>
|
|
194
|
+
</nav>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
</div>
|
|
201
|
+
</footer>
|