@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,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <TierCards> — the membership / pricing tier comparison card row (the
|
|
4
|
+
* Guest / Member / Autopilot pattern). Presentational + EDUCATION ONLY: it shows
|
|
5
|
+
* tiers, their headline figures, and feature bullets, but ships NO pricing,
|
|
6
|
+
* checkout, or subscription mechanics — each tier's CTA is a plain handoff link
|
|
7
|
+
* into the tenant's own (product-owned) subscribe/learn flow.
|
|
8
|
+
*
|
|
9
|
+
* Illustrative-figures affordance (decision compliance-widgets-platform-owned):
|
|
10
|
+
* a tier's figure is treated as an EXAMPLE by default (isIllustrative). When any
|
|
11
|
+
* shown tier is illustrative the group header carries a first-class
|
|
12
|
+
* <IllustrativeLabel> badge, each such figure is prefixed with an "e.g."
|
|
13
|
+
* affordance + data-illustrative hook, and a footnote spells it out — so an
|
|
14
|
+
* example number is never presented as a live offer. A tenant opts a tier out
|
|
15
|
+
* only by asserting `real: true` on confirmed live values.
|
|
16
|
+
*
|
|
17
|
+
* Token-styled only — re-skins entirely from the theme's contract tokens
|
|
18
|
+
* (--color-…, --radius-…, --shadow-…); no hardcoded brand color.
|
|
19
|
+
* data-widget="tier-cards" is the audit/test hook.
|
|
20
|
+
*/
|
|
21
|
+
import type { Tier } from "@/lib/membership/model";
|
|
22
|
+
import { isIllustrative } from "@/lib/membership/model";
|
|
23
|
+
import IllustrativeLabel from "@/components/membership/IllustrativeLabel.astro";
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
tiers: Tier[];
|
|
27
|
+
/** Optional small eyebrow above the heading. */
|
|
28
|
+
eyebrow?: string;
|
|
29
|
+
/** Optional section heading. */
|
|
30
|
+
heading?: string;
|
|
31
|
+
/** Optional supporting sentence under the heading. */
|
|
32
|
+
subhead?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { tiers, eyebrow, heading, subhead } = Astro.props;
|
|
36
|
+
|
|
37
|
+
// Any illustrative figure in the group drives the group-level affordances.
|
|
38
|
+
const anyIllustrative = tiers.some((t) => t.figure && isIllustrative(t));
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
tiers.length > 0 && (
|
|
43
|
+
<section class="container-prose" data-widget="tier-cards">
|
|
44
|
+
{(eyebrow || heading || subhead || anyIllustrative) && (
|
|
45
|
+
<header class="mb-8 text-center">
|
|
46
|
+
{eyebrow && (
|
|
47
|
+
<p class="mb-2 text-xs font-semibold uppercase tracking-[0.12em] text-[var(--color-accent-ink)]">
|
|
48
|
+
{eyebrow}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
{heading && (
|
|
52
|
+
<h2 class="font-display text-2xl font-bold text-[var(--color-text)] sm:text-3xl">
|
|
53
|
+
{heading}
|
|
54
|
+
</h2>
|
|
55
|
+
)}
|
|
56
|
+
{subhead && (
|
|
57
|
+
<p class="mx-auto mt-3 max-w-[60ch] text-[var(--color-muted)]">{subhead}</p>
|
|
58
|
+
)}
|
|
59
|
+
{anyIllustrative && (
|
|
60
|
+
<div class="mt-4">
|
|
61
|
+
<IllustrativeLabel as="badge" />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</header>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
68
|
+
{tiers.map((tier) => {
|
|
69
|
+
const illustrative = Boolean(tier.figure) && isIllustrative(tier);
|
|
70
|
+
return (
|
|
71
|
+
<article
|
|
72
|
+
class={`relative flex flex-col rounded-[var(--radius-lg)] border bg-[var(--color-surface)] p-6 ${
|
|
73
|
+
tier.featured
|
|
74
|
+
? "border-2 border-[var(--color-primary)] shadow-[var(--shadow-md)]"
|
|
75
|
+
: "border-[var(--color-border)]"
|
|
76
|
+
}`}
|
|
77
|
+
data-tier={tier.name}
|
|
78
|
+
>
|
|
79
|
+
{tier.featured && tier.badge && (
|
|
80
|
+
<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-[var(--radius-full)] bg-[var(--color-primary)] px-3 py-1 text-[0.65rem] font-bold uppercase tracking-[0.06em] text-[var(--color-primary-contrast)]">
|
|
81
|
+
{tier.badge}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<h3 class="font-display text-lg font-semibold text-[var(--color-text)]">
|
|
86
|
+
{tier.name}
|
|
87
|
+
</h3>
|
|
88
|
+
{tier.tagline && (
|
|
89
|
+
<p class="mt-1 text-sm text-[var(--color-muted)]">{tier.tagline}</p>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{tier.figure && (
|
|
93
|
+
<p
|
|
94
|
+
class="mt-4 flex items-baseline gap-1.5 text-[var(--color-text)]"
|
|
95
|
+
data-illustrative={illustrative ? "figure" : undefined}
|
|
96
|
+
>
|
|
97
|
+
{illustrative && (
|
|
98
|
+
<span
|
|
99
|
+
class="text-xs font-medium text-[var(--color-muted)]"
|
|
100
|
+
title="Illustrative example"
|
|
101
|
+
>
|
|
102
|
+
e.g.
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<span class="font-display text-3xl font-bold leading-none">
|
|
106
|
+
{tier.figure.value}
|
|
107
|
+
</span>
|
|
108
|
+
{tier.figure.caption && (
|
|
109
|
+
<span class="text-sm text-[var(--color-muted)]">{tier.figure.caption}</span>
|
|
110
|
+
)}
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{tier.features && tier.features.length > 0 && (
|
|
115
|
+
<ul class="mt-5 flex-1 space-y-2.5">
|
|
116
|
+
{tier.features.map((f) => (
|
|
117
|
+
<li
|
|
118
|
+
class={`flex items-start gap-2.5 text-sm ${
|
|
119
|
+
f.included === false
|
|
120
|
+
? "text-[var(--color-muted)]"
|
|
121
|
+
: "text-[var(--color-text)]"
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
<span
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
class={`mt-0.5 ${
|
|
127
|
+
f.included === false
|
|
128
|
+
? "text-[var(--color-muted)]"
|
|
129
|
+
: "text-[var(--color-success)]"
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
{f.included === false ? "–" : f.included === true ? "✓" : "•"}
|
|
133
|
+
</span>
|
|
134
|
+
<span class={f.included === false ? "line-through" : ""}>{f.label}</span>
|
|
135
|
+
</li>
|
|
136
|
+
))}
|
|
137
|
+
</ul>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{tier.cta && (
|
|
141
|
+
<a
|
|
142
|
+
href={tier.cta.href}
|
|
143
|
+
class={`mt-6 inline-flex min-h-[44px] items-center justify-center rounded-[var(--radius-full)] px-5 py-2.5 text-sm font-semibold ${
|
|
144
|
+
tier.featured
|
|
145
|
+
? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
|
|
146
|
+
: "border-2 border-[var(--color-primary)] text-[var(--color-primary)]"
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{tier.cta.label}
|
|
150
|
+
</a>
|
|
151
|
+
)}
|
|
152
|
+
</article>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{anyIllustrative && (
|
|
158
|
+
<footer class="mt-6 text-center">
|
|
159
|
+
<IllustrativeLabel as="note" />
|
|
160
|
+
</footer>
|
|
161
|
+
)}
|
|
162
|
+
</section>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <TierComparisonTable> — the membership/pricing feature matrix companion to
|
|
4
|
+
* <TierCards>: tiers across the top, features down the side, one cell per
|
|
5
|
+
* (feature × tier). Presentational + EDUCATION ONLY — no pricing/checkout/
|
|
6
|
+
* subscription mechanics; a tier's optional figure and CTA are display/handoff
|
|
7
|
+
* only.
|
|
8
|
+
*
|
|
9
|
+
* Illustrative-figures affordance (decision compliance-widgets-platform-owned):
|
|
10
|
+
* when any tier shows an illustrative figure the header carries a first-class
|
|
11
|
+
* <IllustrativeLabel> badge, each such figure gets an "e.g." affordance +
|
|
12
|
+
* data-illustrative hook, and a footnote spells it out — an example number is
|
|
13
|
+
* never presented as a live offer.
|
|
14
|
+
*
|
|
15
|
+
* Cells align to `tiers` by index (see ComparisonRow): boolean → check/dash,
|
|
16
|
+
* string → text, null/omitted → em dash. Token-styled only; horizontally
|
|
17
|
+
* scrollable so it never overflows the page on narrow viewports.
|
|
18
|
+
* data-widget="tier-comparison-table" is the audit/test hook.
|
|
19
|
+
*/
|
|
20
|
+
import type { Tier, ComparisonRow } from "@/lib/membership/model";
|
|
21
|
+
import { isIllustrative } from "@/lib/membership/model";
|
|
22
|
+
import IllustrativeLabel from "@/components/membership/IllustrativeLabel.astro";
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
tiers: Tier[];
|
|
26
|
+
rows: ComparisonRow[];
|
|
27
|
+
/** Optional small eyebrow above the heading. */
|
|
28
|
+
eyebrow?: string;
|
|
29
|
+
/** Optional section heading. */
|
|
30
|
+
heading?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { tiers, rows, eyebrow, heading } = Astro.props;
|
|
34
|
+
const anyIllustrative = tiers.some((t) => t.figure && isIllustrative(t));
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
tiers.length > 0 && (
|
|
39
|
+
<section class="container-prose" data-widget="tier-comparison-table">
|
|
40
|
+
{(eyebrow || heading || anyIllustrative) && (
|
|
41
|
+
<header class="mb-6 text-center">
|
|
42
|
+
{eyebrow && (
|
|
43
|
+
<p class="mb-2 text-xs font-semibold uppercase tracking-[0.12em] text-[var(--color-accent-ink)]">
|
|
44
|
+
{eyebrow}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
{heading && (
|
|
48
|
+
<h2 class="font-display text-2xl font-bold text-[var(--color-text)] sm:text-3xl">
|
|
49
|
+
{heading}
|
|
50
|
+
</h2>
|
|
51
|
+
)}
|
|
52
|
+
{anyIllustrative && (
|
|
53
|
+
<div class="mt-4">
|
|
54
|
+
<IllustrativeLabel as="badge" />
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</header>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<div class="overflow-x-auto">
|
|
61
|
+
<table class="w-full min-w-[32rem] border-collapse text-sm">
|
|
62
|
+
<caption class="sr-only">
|
|
63
|
+
{heading ?? "Membership tier comparison"}
|
|
64
|
+
</caption>
|
|
65
|
+
<thead>
|
|
66
|
+
<tr>
|
|
67
|
+
<th scope="col" class="w-1/3 p-3 text-left align-bottom" />
|
|
68
|
+
{tiers.map((tier) => {
|
|
69
|
+
const illustrative = Boolean(tier.figure) && isIllustrative(tier);
|
|
70
|
+
return (
|
|
71
|
+
<th
|
|
72
|
+
scope="col"
|
|
73
|
+
class={`p-3 text-center align-bottom ${
|
|
74
|
+
tier.featured
|
|
75
|
+
? "rounded-t-[var(--radius-md)] bg-[var(--color-surface-2)]"
|
|
76
|
+
: ""
|
|
77
|
+
}`}
|
|
78
|
+
data-tier={tier.name}
|
|
79
|
+
>
|
|
80
|
+
<span class="block font-display text-base font-semibold text-[var(--color-text)]">
|
|
81
|
+
{tier.name}
|
|
82
|
+
</span>
|
|
83
|
+
{tier.figure && (
|
|
84
|
+
<span
|
|
85
|
+
class="mt-1 block text-xs text-[var(--color-muted)]"
|
|
86
|
+
data-illustrative={illustrative ? "figure" : undefined}
|
|
87
|
+
>
|
|
88
|
+
{illustrative && <span title="Illustrative example">e.g. </span>}
|
|
89
|
+
{tier.figure.value}
|
|
90
|
+
{tier.figure.caption ? ` ${tier.figure.caption}` : ""}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
</th>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</tr>
|
|
97
|
+
</thead>
|
|
98
|
+
<tbody>
|
|
99
|
+
{rows.map((row) => (
|
|
100
|
+
<tr class="border-t border-[var(--color-border)]">
|
|
101
|
+
<th
|
|
102
|
+
scope="row"
|
|
103
|
+
class="p-3 text-left font-medium text-[var(--color-text)]"
|
|
104
|
+
>
|
|
105
|
+
{row.label}
|
|
106
|
+
</th>
|
|
107
|
+
{tiers.map((tier, i) => {
|
|
108
|
+
const cell = row.cells[i];
|
|
109
|
+
return (
|
|
110
|
+
<td
|
|
111
|
+
class={`p-3 text-center ${
|
|
112
|
+
tier.featured ? "bg-[var(--color-surface-2)]" : ""
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
{typeof cell === "boolean" ? (
|
|
116
|
+
cell ? (
|
|
117
|
+
<span class="text-[var(--color-success)]" aria-label="Included">
|
|
118
|
+
✓
|
|
119
|
+
</span>
|
|
120
|
+
) : (
|
|
121
|
+
<span class="text-[var(--color-muted)]" aria-label="Not included">
|
|
122
|
+
–
|
|
123
|
+
</span>
|
|
124
|
+
)
|
|
125
|
+
) : cell != null && cell !== "" ? (
|
|
126
|
+
<span class="text-[var(--color-text)]">{cell}</span>
|
|
127
|
+
) : (
|
|
128
|
+
<span class="text-[var(--color-muted)]" aria-label="Not applicable">
|
|
129
|
+
—
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</td>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</tr>
|
|
136
|
+
))}
|
|
137
|
+
</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{anyIllustrative && (
|
|
142
|
+
<footer class="mt-4 text-center">
|
|
143
|
+
<IllustrativeLabel as="note" />
|
|
144
|
+
</footer>
|
|
145
|
+
)}
|
|
146
|
+
</section>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Active-filter chips (Shopify-style). Shows what's applied as removable pills
|
|
4
|
+
* above the grid — each chip is a link that drops exactly one filter, so it's
|
|
5
|
+
* URL-addressable and back-button safe like the rest of faceting (src/lib/url.ts).
|
|
6
|
+
*/
|
|
7
|
+
import type { ListProductsQuery } from "@tot/public-runtime";
|
|
8
|
+
import { toggleFacet, buildQueryString } from "@/lib/url";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
query: ListProductsQuery;
|
|
12
|
+
basePath: string;
|
|
13
|
+
}
|
|
14
|
+
const { query, basePath } = Astro.props;
|
|
15
|
+
|
|
16
|
+
type Chip = { label: string; href: string };
|
|
17
|
+
const chips: Chip[] = [];
|
|
18
|
+
|
|
19
|
+
// Option + tag facet values (toggling an active value removes it).
|
|
20
|
+
for (const [name, values] of Object.entries(query.filters ?? {})) {
|
|
21
|
+
for (const v of values) {
|
|
22
|
+
chips.push({
|
|
23
|
+
label: name === "tags" ? `#${v}` : `${name}: ${v}`,
|
|
24
|
+
href: basePath + toggleFacet(query, name, v),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Price range.
|
|
30
|
+
if (query.priceMin != null || query.priceMax != null) {
|
|
31
|
+
const lo = query.priceMin != null ? `$${query.priceMin}` : "$0";
|
|
32
|
+
const hi = query.priceMax != null ? `$${query.priceMax}` : "any";
|
|
33
|
+
const { priceMin, priceMax, ...rest } = query;
|
|
34
|
+
chips.push({ label: `${lo}–${hi}`, href: basePath + buildQueryString(rest) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Availability (the meaningful filter is "in-stock"; "all" is the default).
|
|
38
|
+
if (query.availability === "in-stock") {
|
|
39
|
+
const { availability, ...rest } = query;
|
|
40
|
+
chips.push({ label: "In stock", href: basePath + buildQueryString(rest) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const clearHref =
|
|
44
|
+
basePath +
|
|
45
|
+
buildQueryString({ sort: query.sort, pageSize: query.pageSize });
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
chips.length > 0 && (
|
|
50
|
+
<div class="mb-5 flex flex-wrap items-center gap-2" data-active-filters>
|
|
51
|
+
<span class="sr-only">Active filters:</span>
|
|
52
|
+
{chips.map((chip) => (
|
|
53
|
+
<a
|
|
54
|
+
href={chip.href}
|
|
55
|
+
rel="nofollow"
|
|
56
|
+
class="group inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] py-1 pl-3 pr-2 text-xs transition-colors hover:border-[var(--color-accent-ink)] hover:text-[var(--color-accent-ink)]"
|
|
57
|
+
>
|
|
58
|
+
<span>{chip.label}</span>
|
|
59
|
+
<span
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
class="grid h-4 w-4 place-items-center rounded-full text-[var(--color-muted)] transition-colors group-hover:bg-[var(--color-accent-ink)] group-hover:text-[var(--color-primary-contrast)]"
|
|
62
|
+
>
|
|
63
|
+
×
|
|
64
|
+
</span>
|
|
65
|
+
<span class="sr-only">Remove filter</span>
|
|
66
|
+
</a>
|
|
67
|
+
))}
|
|
68
|
+
{chips.length > 1 && (
|
|
69
|
+
<a
|
|
70
|
+
href={clearHref}
|
|
71
|
+
rel="nofollow"
|
|
72
|
+
class="ml-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]"
|
|
73
|
+
>
|
|
74
|
+
Clear all
|
|
75
|
+
</a>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Faceted filtering UI (brief §5). URL-ADDRESSABLE and back-button safe: every
|
|
4
|
+
* facet is a real link that toggles a querystring param (no JS required for
|
|
5
|
+
* filtering). The optional mobile drawer toggle is the only island.
|
|
6
|
+
*
|
|
7
|
+
* Encoding handled by src/lib/url.ts (opt.<Name>=<value>, tag=, price, avail).
|
|
8
|
+
*/
|
|
9
|
+
import type { Facet, ListProductsQuery } from "@tot/public-runtime";
|
|
10
|
+
import { toggleFacet, buildQueryString, isPriceBucketActive, togglePriceBucket } from "@/lib/url";
|
|
11
|
+
import { PRICE_BUCKETS, bucketByKey } from "@/lib/priceBuckets";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
facets: Facet[];
|
|
15
|
+
query: ListProductsQuery;
|
|
16
|
+
basePath: string; // e.g. /collections/bestsellers
|
|
17
|
+
}
|
|
18
|
+
const { facets, query, basePath } = Astro.props;
|
|
19
|
+
|
|
20
|
+
const selected = (name: string, value: string) =>
|
|
21
|
+
(query.filters?.[name] ?? []).includes(value);
|
|
22
|
+
|
|
23
|
+
// "price" is rendered separately as a bucketed quick-select (below), not a
|
|
24
|
+
// generic query.filters facet — pull it out before ordering the rest.
|
|
25
|
+
const priceFacet = facets.find((f) => f.name === "price");
|
|
26
|
+
|
|
27
|
+
// Order facets: option facets first, then tags, availability last.
|
|
28
|
+
const ordered = [...facets]
|
|
29
|
+
.filter((f) => f.name !== "price")
|
|
30
|
+
.sort((a, b) => {
|
|
31
|
+
const rank = (n: string) => (n === "availability" ? 2 : n === "tags" ? 1 : 0);
|
|
32
|
+
return rank(a.name) - rank(b.name);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const hasActiveFilters =
|
|
36
|
+
!!query.filters && Object.keys(query.filters).length > 0;
|
|
37
|
+
|
|
38
|
+
const clearHref =
|
|
39
|
+
basePath +
|
|
40
|
+
buildQueryString({ sort: query.sort, q: query.q, pageSize: query.pageSize });
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<aside aria-label="Filters" class="text-sm">
|
|
44
|
+
<div class="mb-4 flex items-center justify-between">
|
|
45
|
+
<h2 class="eyebrow !text-[var(--color-text)]">Filter</h2>
|
|
46
|
+
{hasActiveFilters && (
|
|
47
|
+
<a href={clearHref} class="text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-[var(--color-accent-ink)]">
|
|
48
|
+
Clear all
|
|
49
|
+
</a>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="space-y-7">
|
|
54
|
+
{
|
|
55
|
+
ordered.map((facet) => (
|
|
56
|
+
<fieldset>
|
|
57
|
+
<legend class="mb-2.5 font-display text-[0.95rem] font-medium">
|
|
58
|
+
{facet.name === "tags"
|
|
59
|
+
? "Tags"
|
|
60
|
+
: facet.name === "availability"
|
|
61
|
+
? "Availability"
|
|
62
|
+
: facet.name}
|
|
63
|
+
</legend>
|
|
64
|
+
<ul class="space-y-1.5">
|
|
65
|
+
{facet.values.map((fv) => {
|
|
66
|
+
const isSel = selected(facet.name, fv.value);
|
|
67
|
+
const href =
|
|
68
|
+
basePath + toggleFacet(query, facet.name, fv.value);
|
|
69
|
+
return (
|
|
70
|
+
<li>
|
|
71
|
+
<a
|
|
72
|
+
href={href}
|
|
73
|
+
rel="nofollow"
|
|
74
|
+
aria-pressed={isSel}
|
|
75
|
+
class:list={[
|
|
76
|
+
"flex items-center justify-between gap-3 rounded-[var(--radius-sm)] px-2 py-1 transition-colors",
|
|
77
|
+
isSel
|
|
78
|
+
? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
|
|
79
|
+
: "hover:bg-[color-mix(in_srgb,var(--color-primary)_8%,transparent)]",
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
82
|
+
<span class="flex items-center gap-2">
|
|
83
|
+
<span
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
class:list={[
|
|
86
|
+
"grid h-3.5 w-3.5 place-items-center rounded-[2px] border",
|
|
87
|
+
isSel
|
|
88
|
+
? "border-[var(--color-primary-contrast)] bg-[var(--color-primary-contrast)] text-[var(--color-primary)]"
|
|
89
|
+
: "border-[var(--color-border)]",
|
|
90
|
+
]}
|
|
91
|
+
>
|
|
92
|
+
{isSel ? "✓" : ""}
|
|
93
|
+
</span>
|
|
94
|
+
{fv.value === "all" ? "All items" : fv.value}
|
|
95
|
+
</span>
|
|
96
|
+
<span class:list={["text-xs", isSel ? "opacity-80" : "text-[var(--color-muted)]"]}>
|
|
97
|
+
{fv.count}
|
|
98
|
+
</span>
|
|
99
|
+
</a>
|
|
100
|
+
</li>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</ul>
|
|
104
|
+
</fieldset>
|
|
105
|
+
))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
{/* Price range — bucketed quick-select (live counts) + a free-text form
|
|
109
|
+
for an arbitrary range. Both write the same priceMin/priceMax params,
|
|
110
|
+
so a bucket and the form stay in sync (selecting a bucket fills the
|
|
111
|
+
form; a custom range that matches a bucket shows it as active). */}
|
|
112
|
+
<fieldset class="border-t border-[var(--color-border)] pt-5">
|
|
113
|
+
<legend class="mb-2.5 font-display text-[0.95rem] font-medium">Price</legend>
|
|
114
|
+
{priceFacet && (
|
|
115
|
+
<ul class="mb-4 space-y-1.5">
|
|
116
|
+
{priceFacet.values.map((fv) => {
|
|
117
|
+
const bucket = bucketByKey(fv.value);
|
|
118
|
+
if (!bucket) return null;
|
|
119
|
+
const isSel = isPriceBucketActive(query, bucket);
|
|
120
|
+
const href = basePath + togglePriceBucket(query, bucket);
|
|
121
|
+
return (
|
|
122
|
+
<li>
|
|
123
|
+
<a
|
|
124
|
+
href={href}
|
|
125
|
+
rel="nofollow"
|
|
126
|
+
aria-pressed={isSel}
|
|
127
|
+
class:list={[
|
|
128
|
+
"flex items-center justify-between gap-3 rounded-[var(--radius-sm)] px-2 py-1 transition-colors",
|
|
129
|
+
isSel
|
|
130
|
+
? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
|
|
131
|
+
: "hover:bg-[color-mix(in_srgb,var(--color-primary)_8%,transparent)]",
|
|
132
|
+
]}
|
|
133
|
+
>
|
|
134
|
+
<span class="flex items-center gap-2">
|
|
135
|
+
<span
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
class:list={[
|
|
138
|
+
"grid h-3.5 w-3.5 place-items-center rounded-[2px] border",
|
|
139
|
+
isSel
|
|
140
|
+
? "border-[var(--color-primary-contrast)] bg-[var(--color-primary-contrast)] text-[var(--color-primary)]"
|
|
141
|
+
: "border-[var(--color-border)]",
|
|
142
|
+
]}
|
|
143
|
+
>
|
|
144
|
+
{isSel ? "✓" : ""}
|
|
145
|
+
</span>
|
|
146
|
+
{bucket.label}
|
|
147
|
+
</span>
|
|
148
|
+
<span class:list={["text-xs", isSel ? "opacity-80" : "text-[var(--color-muted)]"]}>
|
|
149
|
+
{fv.count}
|
|
150
|
+
</span>
|
|
151
|
+
</a>
|
|
152
|
+
</li>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</ul>
|
|
156
|
+
)}
|
|
157
|
+
<form method="get" action={basePath}>
|
|
158
|
+
{query.sort && <input type="hidden" name="sort" value={query.sort} />}
|
|
159
|
+
{query.availability && query.availability !== "all" && (
|
|
160
|
+
<input type="hidden" name="availability" value={query.availability} />
|
|
161
|
+
)}
|
|
162
|
+
{Object.entries(query.filters ?? {}).flatMap(([name, values]) =>
|
|
163
|
+
values.map((v) => (
|
|
164
|
+
<input type="hidden" name={name === "tags" ? "tag" : `opt.${name}`} value={v} />
|
|
165
|
+
)),
|
|
166
|
+
)}
|
|
167
|
+
<div class="flex items-center gap-2">
|
|
168
|
+
<label class="sr-only" for="priceMin">Minimum price</label>
|
|
169
|
+
<input
|
|
170
|
+
id="priceMin"
|
|
171
|
+
type="number"
|
|
172
|
+
name="priceMin"
|
|
173
|
+
min="0"
|
|
174
|
+
placeholder="Min"
|
|
175
|
+
value={query.priceMin ?? ""}
|
|
176
|
+
class="w-20 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1"
|
|
177
|
+
/>
|
|
178
|
+
<span aria-hidden="true" class="text-[var(--color-muted)]">–</span>
|
|
179
|
+
<label class="sr-only" for="priceMax">Maximum price</label>
|
|
180
|
+
<input
|
|
181
|
+
id="priceMax"
|
|
182
|
+
type="number"
|
|
183
|
+
name="priceMax"
|
|
184
|
+
min="0"
|
|
185
|
+
placeholder="Max"
|
|
186
|
+
value={query.priceMax ?? ""}
|
|
187
|
+
class="w-20 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1"
|
|
188
|
+
/>
|
|
189
|
+
<button
|
|
190
|
+
type="submit"
|
|
191
|
+
class="rounded-[var(--radius-sm)] bg-[var(--color-primary)] px-3 py-1 text-xs font-medium text-[var(--color-primary-contrast)]"
|
|
192
|
+
>
|
|
193
|
+
Go
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</form>
|
|
197
|
+
</fieldset>
|
|
198
|
+
</div>
|
|
199
|
+
</aside>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Numbered pagination (SEO-friendly, brief §5 default) + a "Load more" link.
|
|
4
|
+
* Both are plain anchors carrying the full query, so they're crawlable and
|
|
5
|
+
* back-button safe. "Load more" simply links to the next page; with JS it could
|
|
6
|
+
* be progressively enhanced to append, but the no-JS path stays correct.
|
|
7
|
+
*/
|
|
8
|
+
import type { ListProductsQuery } from "@tot/public-runtime";
|
|
9
|
+
import { buildQueryString } from "@/lib/url";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
query: ListProductsQuery;
|
|
13
|
+
basePath: string;
|
|
14
|
+
page: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
const { query, basePath, page, pageSize, total } = Astro.props;
|
|
19
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
20
|
+
if (pageCount <= 1) {
|
|
21
|
+
// nothing to render
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const href = (p: number) => basePath + buildQueryString(query, { page: p });
|
|
25
|
+
|
|
26
|
+
// Compact page-number window.
|
|
27
|
+
const windowSize = 5;
|
|
28
|
+
let start = Math.max(1, page - Math.floor(windowSize / 2));
|
|
29
|
+
let end = Math.min(pageCount, start + windowSize - 1);
|
|
30
|
+
start = Math.max(1, end - windowSize + 1);
|
|
31
|
+
const pages = Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
32
|
+
const hasNext = page < pageCount;
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
{pageCount > 1 && (
|
|
36
|
+
<nav aria-label="Pagination" class="mt-12 flex flex-col items-center gap-6">
|
|
37
|
+
{hasNext && (
|
|
38
|
+
<a
|
|
39
|
+
href={href(page + 1)}
|
|
40
|
+
rel="next"
|
|
41
|
+
class="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-8 py-3 font-display text-sm font-medium transition-colors hover:border-[var(--color-accent)] hover:text-[var(--color-accent-ink)]"
|
|
42
|
+
>
|
|
43
|
+
Load more
|
|
44
|
+
</a>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<ul class="flex items-center gap-1">
|
|
48
|
+
<li>
|
|
49
|
+
<a
|
|
50
|
+
href={href(Math.max(1, page - 1))}
|
|
51
|
+
rel="prev"
|
|
52
|
+
aria-label="Previous page"
|
|
53
|
+
aria-disabled={page === 1}
|
|
54
|
+
class:list={[
|
|
55
|
+
"grid h-9 w-9 place-items-center rounded-[var(--radius-sm)]",
|
|
56
|
+
page === 1 ? "pointer-events-none text-[var(--color-muted)] opacity-40" : "hover:bg-[var(--color-surface)]",
|
|
57
|
+
]}
|
|
58
|
+
>‹</a>
|
|
59
|
+
</li>
|
|
60
|
+
{start > 1 && (
|
|
61
|
+
<>
|
|
62
|
+
<li><a href={href(1)} class="grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] hover:bg-[var(--color-surface)]">1</a></li>
|
|
63
|
+
{start > 2 && <li aria-hidden="true" class="px-1 text-[var(--color-muted)]">…</li>}
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
{pages.map((p) => (
|
|
67
|
+
<li>
|
|
68
|
+
<a
|
|
69
|
+
href={href(p)}
|
|
70
|
+
aria-current={p === page ? "page" : undefined}
|
|
71
|
+
class:list={[
|
|
72
|
+
"grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] font-display text-sm",
|
|
73
|
+
p === page
|
|
74
|
+
? "bg-[var(--color-primary)] text-[var(--color-primary-contrast)]"
|
|
75
|
+
: "hover:bg-[var(--color-surface)]",
|
|
76
|
+
]}
|
|
77
|
+
>{p}</a>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
{end < pageCount && (
|
|
81
|
+
<>
|
|
82
|
+
{end < pageCount - 1 && <li aria-hidden="true" class="px-1 text-[var(--color-muted)]">…</li>}
|
|
83
|
+
<li><a href={href(pageCount)} class="grid h-9 w-9 place-items-center rounded-[var(--radius-sm)] hover:bg-[var(--color-surface)]">{pageCount}</a></li>
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
<li>
|
|
87
|
+
<a
|
|
88
|
+
href={href(Math.min(pageCount, page + 1))}
|
|
89
|
+
aria-label="Next page"
|
|
90
|
+
aria-disabled={!hasNext}
|
|
91
|
+
class:list={[
|
|
92
|
+
"grid h-9 w-9 place-items-center rounded-[var(--radius-sm)]",
|
|
93
|
+
!hasNext ? "pointer-events-none text-[var(--color-muted)] opacity-40" : "hover:bg-[var(--color-surface)]",
|
|
94
|
+
]}
|
|
95
|
+
>›</a>
|
|
96
|
+
</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</nav>
|
|
99
|
+
)}
|