@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,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /tenants/<id>/<path> — serve a tenant's versioned static asset from R2.
|
|
3
|
+
*
|
|
4
|
+
* The version-scoped replacement for the build-time `copy-tenant-assets.mjs`
|
|
5
|
+
* baking (decision `assets-r2-versioned`). Resolves the tenant's active
|
|
6
|
+
* channel→snapshot versionId from the `cust:assets-pointer:<id>` KV record and
|
|
7
|
+
* streams `tenants/<id>/<versionId>/<rel>` from the TENANT_ASSETS R2 bucket with
|
|
8
|
+
* the right content-type + a revalidating cache header + a versionId ETag.
|
|
9
|
+
*
|
|
10
|
+
* BACK-COMPAT: this SSR route only runs when NO baked static file matched — the
|
|
11
|
+
* Cloudflare/Vite static layer serves `public/tenants/<id>/…` first. So a
|
|
12
|
+
* NON-versioned tenant (still baked by copy-tenant-assets) never reaches here,
|
|
13
|
+
* and a versioned tenant (skipped from baking via a `.tot/assets-versioned`
|
|
14
|
+
* marker) falls through to R2. When the tenant has no published asset version,
|
|
15
|
+
* `serveTenantAsset` returns null → 404 (nothing published yet).
|
|
16
|
+
*/
|
|
17
|
+
import type { APIContext } from "astro";
|
|
18
|
+
import {
|
|
19
|
+
serveTenantAsset,
|
|
20
|
+
type R2BucketLike,
|
|
21
|
+
} from "@tot/public-runtime";
|
|
22
|
+
import { readR2, readKv } from "@/lib/env";
|
|
23
|
+
|
|
24
|
+
export const prerender = false;
|
|
25
|
+
|
|
26
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
27
|
+
const [bucket, kv] = await Promise.all([
|
|
28
|
+
readR2("TENANT_ASSETS"),
|
|
29
|
+
readKv("TENANT_CACHE"),
|
|
30
|
+
]);
|
|
31
|
+
// No R2/KV binding (plain `astro dev`, or unconfigured) → nothing to serve.
|
|
32
|
+
if (!bucket || !kv) {
|
|
33
|
+
return new Response("Not found", {
|
|
34
|
+
status: 404,
|
|
35
|
+
headers: { "cache-control": "no-store" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const res = await serveTenantAsset(
|
|
40
|
+
{
|
|
41
|
+
// Runtime binding structurally satisfies the shared-runtime R2 seam.
|
|
42
|
+
bucket: bucket as unknown as R2BucketLike,
|
|
43
|
+
kvGet: (key) => kv.get(key),
|
|
44
|
+
channel: context.locals.customizationEnv ?? "live",
|
|
45
|
+
ifNoneMatch: context.request.headers.get("if-none-match"),
|
|
46
|
+
},
|
|
47
|
+
new URL(context.request.url).pathname,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
res ??
|
|
52
|
+
new Response("Not found", {
|
|
53
|
+
status: 404,
|
|
54
|
+
headers: { "cache-control": "no-store" },
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Theme bridge (brief §6/§7).
|
|
5
|
+
*
|
|
6
|
+
* The per-tenant identity arrives as CSS custom properties in a :root{} block
|
|
7
|
+
* injected by the middleware (src/themes/schema.ts -> Layout.astro). Here we map
|
|
8
|
+
* those vars into Tailwind v4 theme tokens so utilities like `bg-bg`,
|
|
9
|
+
* `text-primary`, `font-display`, `rounded-md` resolve to the LIVE tenant value.
|
|
10
|
+
* Components stay theme-agnostic: they use utilities, the utilities read vars,
|
|
11
|
+
* the vars come from the tenant. Swap a tenant -> the whole UI reskins, no
|
|
12
|
+
* component edits.
|
|
13
|
+
*
|
|
14
|
+
* Fallback values below keep the design coherent before/without the injected
|
|
15
|
+
* block (e.g. error pages, prerendered routes).
|
|
16
|
+
*/
|
|
17
|
+
@theme {
|
|
18
|
+
--color-bg: var(--color-bg, #f4f1ea);
|
|
19
|
+
--color-surface: var(--color-surface, #fcfaf5);
|
|
20
|
+
--color-text: var(--color-text, #1c2624);
|
|
21
|
+
--color-muted: var(--color-muted, #5c6663);
|
|
22
|
+
--color-primary: var(--color-primary, #1f3a34);
|
|
23
|
+
--color-primary-contrast: var(--color-primary-contrast, #f4f1ea);
|
|
24
|
+
--color-accent: var(--color-accent, #b5612f);
|
|
25
|
+
/* Darkened accent for SMALL text/links on light bg/surface (the bright
|
|
26
|
+
`accent` is reserved for large display type on the spruce hero, where it
|
|
27
|
+
reads as the brass spark). Derived from the tenant's own accent so it stays
|
|
28
|
+
theme-agnostic. Reference value resolves to ~#985127 (≥5:1 on bg/surface).
|
|
29
|
+
A tenant MAY hand-tune it for exact AA by emitting --color-accent-ink; else
|
|
30
|
+
it derives from the tenant's own accent so it stays theme-agnostic. */
|
|
31
|
+
--color-accent-ink: var(--color-accent-ink, color-mix(in srgb, var(--color-accent) 84%, #000));
|
|
32
|
+
--color-border: var(--color-border, #dcd6c9);
|
|
33
|
+
--color-success: var(--color-success, #3f6b4f);
|
|
34
|
+
--color-sale: var(--color-sale, #9e3b2e);
|
|
35
|
+
|
|
36
|
+
/* Semantic/state extensions (token-contract.md). Platform defaults kept
|
|
37
|
+
resolvable here so components can always read them; a tenant theme overrides
|
|
38
|
+
any by emitting the matching --color-* via themeToCssVars. */
|
|
39
|
+
--color-surface-2: var(--color-surface-2, #ece7db);
|
|
40
|
+
--color-warning: var(--color-warning, #8a5717);
|
|
41
|
+
--color-error: var(--color-error, #a22014);
|
|
42
|
+
--color-info: var(--color-info, #2c5b73);
|
|
43
|
+
|
|
44
|
+
/* Hover policy: components read --color-{primary,accent}-hover; the default is
|
|
45
|
+
a derived darken of the base, overridable per tenant for hand-tuned values. */
|
|
46
|
+
--color-primary-hover: var(--color-primary-hover, color-mix(in srgb, var(--color-primary) 88%, #000));
|
|
47
|
+
--color-accent-hover: var(--color-accent-hover, color-mix(in srgb, var(--color-accent) 88%, #000));
|
|
48
|
+
|
|
49
|
+
--font-display: var(--font-display, "Space Grotesk"), system-ui, sans-serif;
|
|
50
|
+
--font-body: var(--font-body, "Source Serif 4"), Georgia, serif;
|
|
51
|
+
--font-mono: var(--font-mono, "Space Mono"), ui-monospace, monospace;
|
|
52
|
+
|
|
53
|
+
--radius-sm: var(--radius-sm, 3px);
|
|
54
|
+
--radius-md: var(--radius-md, 6px);
|
|
55
|
+
--radius-lg: var(--radius-lg, 12px);
|
|
56
|
+
--radius-full: var(--radius-full, 999px);
|
|
57
|
+
|
|
58
|
+
--shadow-sm: var(--shadow-sm, 0 1px 2px rgba(28, 38, 36, 0.06));
|
|
59
|
+
--shadow-md: var(--shadow-md, 0 6px 24px -8px rgba(28, 38, 36, 0.14));
|
|
60
|
+
--shadow-lg: var(--shadow-lg, 0 24px 60px -20px rgba(28, 38, 36, 0.22));
|
|
61
|
+
|
|
62
|
+
--container-max: var(--container-max, 1240px);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ---- base ---------------------------------------------------------------- */
|
|
66
|
+
@layer base {
|
|
67
|
+
:root {
|
|
68
|
+
/* Fluid modular type scale; overridden by injected --fs-* when present. */
|
|
69
|
+
color-scheme: light;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
html {
|
|
73
|
+
font-family: var(--font-body);
|
|
74
|
+
color: var(--color-text);
|
|
75
|
+
background: var(--color-bg);
|
|
76
|
+
-webkit-font-smoothing: antialiased;
|
|
77
|
+
text-rendering: optimizeLegibility;
|
|
78
|
+
scroll-behavior: smooth;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
body {
|
|
82
|
+
font-size: var(--fs-base, 1.0625rem);
|
|
83
|
+
line-height: 1.6;
|
|
84
|
+
letter-spacing: var(--type-tracking, -0.011em);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
h1, h2, h3, h4, h5 {
|
|
88
|
+
font-family: var(--font-display);
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
line-height: 1.05;
|
|
91
|
+
letter-spacing: -0.02em;
|
|
92
|
+
color: var(--color-text);
|
|
93
|
+
text-wrap: balance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Body copy rhythm: a comfortable measure + paragraph spacing for prose-y
|
|
97
|
+
blocks that aren't inside .rte (e.g. editorial subheads). */
|
|
98
|
+
p { text-wrap: pretty; }
|
|
99
|
+
|
|
100
|
+
a {
|
|
101
|
+
color: inherit;
|
|
102
|
+
text-decoration: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Visible keyboard focus everywhere (WCAG 2.2 AA, brief §8). */
|
|
106
|
+
:focus-visible {
|
|
107
|
+
outline: var(--border-thick, 2px) solid var(--color-accent);
|
|
108
|
+
outline-offset: 2px;
|
|
109
|
+
border-radius: 2px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
::selection {
|
|
113
|
+
background: var(--color-primary);
|
|
114
|
+
color: var(--color-primary-contrast);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
img { max-width: 100%; height: auto; }
|
|
118
|
+
|
|
119
|
+
/* Honor reduced motion (brief §6/§8). */
|
|
120
|
+
@media (prefers-reduced-motion: reduce) {
|
|
121
|
+
*, *::before, *::after {
|
|
122
|
+
animation-duration: 0.001ms !important;
|
|
123
|
+
animation-iteration-count: 1 !important;
|
|
124
|
+
transition-duration: 0.001ms !important;
|
|
125
|
+
scroll-behavior: auto !important;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ---- shared component primitives ----------------------------------------- */
|
|
131
|
+
@layer components {
|
|
132
|
+
.container-prose {
|
|
133
|
+
width: 100%;
|
|
134
|
+
max-width: var(--container-max);
|
|
135
|
+
margin-inline: auto;
|
|
136
|
+
padding-inline: clamp(1rem, 4vw, 2.5rem);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.eyebrow {
|
|
140
|
+
font-family: var(--font-mono);
|
|
141
|
+
font-size: 0.72rem;
|
|
142
|
+
letter-spacing: 0.16em;
|
|
143
|
+
text-transform: uppercase;
|
|
144
|
+
color: var(--color-muted);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Eyebrow with the signature leading tick — a single short amber rule, the
|
|
148
|
+
"one brass rivet" motif from DESIGN.md, applied as a quiet structural mark.
|
|
149
|
+
Opt-in via .eyebrow.eyebrow--tick so it never fires where it'd read as noise. */
|
|
150
|
+
.eyebrow--tick {
|
|
151
|
+
display: inline-flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 0.6em;
|
|
154
|
+
}
|
|
155
|
+
.eyebrow--tick::before {
|
|
156
|
+
content: "";
|
|
157
|
+
width: 1.6em;
|
|
158
|
+
height: 2px;
|
|
159
|
+
background: var(--color-accent);
|
|
160
|
+
flex: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Fluid display scale, derived from the tenant's emitted modular --fs-* steps
|
|
164
|
+
(the major-third scale in DESIGN.md). One rhythm across every page instead
|
|
165
|
+
of ad-hoc per-component clamps. min = small step, max = large step. */
|
|
166
|
+
.display-hero {
|
|
167
|
+
font-family: var(--font-display);
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
line-height: 0.95;
|
|
170
|
+
letter-spacing: -0.03em;
|
|
171
|
+
font-size: clamp(var(--fs-3xl, 3.05rem), 8vw, var(--fs-5xl, 4.77rem));
|
|
172
|
+
}
|
|
173
|
+
.display-xl {
|
|
174
|
+
font-family: var(--font-display);
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
line-height: 1.02;
|
|
177
|
+
letter-spacing: -0.025em;
|
|
178
|
+
font-size: clamp(var(--fs-2xl, 2.44rem), 5vw, var(--fs-4xl, 3.81rem));
|
|
179
|
+
}
|
|
180
|
+
.display-lg {
|
|
181
|
+
font-family: var(--font-display);
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
line-height: 1.05;
|
|
184
|
+
letter-spacing: -0.02em;
|
|
185
|
+
font-size: clamp(var(--fs-xl, 1.95rem), 3.5vw, var(--fs-3xl, 3.05rem));
|
|
186
|
+
}
|
|
187
|
+
.display-md {
|
|
188
|
+
font-family: var(--font-display);
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
line-height: 1.1;
|
|
191
|
+
letter-spacing: -0.018em;
|
|
192
|
+
font-size: clamp(var(--fs-lg, 1.56rem), 3vw, var(--fs-2xl, 2.44rem));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Tasteful scroll-reveal; opt-in via [data-reveal], JS toggles .is-in. */
|
|
196
|
+
[data-reveal] {
|
|
197
|
+
opacity: 0;
|
|
198
|
+
transform: translateY(14px);
|
|
199
|
+
transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
200
|
+
}
|
|
201
|
+
[data-reveal].is-in {
|
|
202
|
+
opacity: 1;
|
|
203
|
+
transform: none;
|
|
204
|
+
}
|
|
205
|
+
@media (prefers-reduced-motion: reduce) {
|
|
206
|
+
[data-reveal] { opacity: 1; transform: none; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Island animations (MobileNav drawer slide-in). Globally neutralized by the
|
|
210
|
+
reduced-motion guard above; islands also guard in JS. */
|
|
211
|
+
@keyframes tot-slidein {
|
|
212
|
+
from { transform: translateX(100%); }
|
|
213
|
+
to { transform: translateX(0); }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Rich text from CMS/description. */
|
|
217
|
+
.rte > * + * { margin-top: 1em; }
|
|
218
|
+
.rte h2 { font-size: var(--fs-xl, 1.6rem); margin-top: 1.6em; }
|
|
219
|
+
.rte h3 { font-size: var(--fs-lg, 1.3rem); margin-top: 1.3em; }
|
|
220
|
+
.rte ul, .rte ol { padding-left: 1.4em; }
|
|
221
|
+
.rte ul { list-style: disc; }
|
|
222
|
+
.rte ol { list-style: decimal; }
|
|
223
|
+
.rte li + li { margin-top: 0.35em; }
|
|
224
|
+
.rte strong { font-weight: 600; }
|
|
225
|
+
.rte a { color: var(--color-accent-ink); text-decoration: underline; text-underline-offset: 3px; }
|
|
226
|
+
|
|
227
|
+
/* Accent text-link with an animated underline — restrained micro-interaction
|
|
228
|
+
for inline CTAs. Uses accent-ink (AA on light surfaces). */
|
|
229
|
+
.link-accent {
|
|
230
|
+
color: var(--color-text);
|
|
231
|
+
background-image: linear-gradient(var(--color-accent-ink), var(--color-accent-ink));
|
|
232
|
+
background-position: 0 100%;
|
|
233
|
+
background-repeat: no-repeat;
|
|
234
|
+
background-size: 100% 1.5px;
|
|
235
|
+
transition: color 0.2s ease, background-size 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
|
236
|
+
}
|
|
237
|
+
.link-accent:hover,
|
|
238
|
+
.link-accent:focus-visible {
|
|
239
|
+
color: var(--color-accent-ink);
|
|
240
|
+
}
|
|
241
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
242
|
+
.link-accent { background-size: 0% 1.5px; }
|
|
243
|
+
.link-accent:hover,
|
|
244
|
+
.link-accent:focus-visible { background-size: 100% 1.5px; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Collapsible PDP info sections (native <details>). Zero-JS, accessible. */
|
|
248
|
+
.accordion {
|
|
249
|
+
border-bottom: 1px solid var(--color-border);
|
|
250
|
+
}
|
|
251
|
+
.accordion__summary {
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
justify-content: space-between;
|
|
255
|
+
gap: 1rem;
|
|
256
|
+
padding: 1rem 0;
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
list-style: none;
|
|
259
|
+
font-family: var(--font-display);
|
|
260
|
+
font-weight: 500;
|
|
261
|
+
font-size: 0.95rem;
|
|
262
|
+
color: var(--color-text);
|
|
263
|
+
transition: color 0.15s ease;
|
|
264
|
+
}
|
|
265
|
+
.accordion__summary::-webkit-details-marker { display: none; }
|
|
266
|
+
.accordion__summary:hover { color: var(--color-primary); }
|
|
267
|
+
.accordion__chevron {
|
|
268
|
+
font-family: var(--font-mono);
|
|
269
|
+
font-size: 1.1rem;
|
|
270
|
+
line-height: 1;
|
|
271
|
+
color: var(--color-muted);
|
|
272
|
+
transition: transform 0.2s ease;
|
|
273
|
+
}
|
|
274
|
+
.accordion[open] .accordion__chevron { transform: rotate(45deg); }
|
|
275
|
+
.accordion__body { padding-bottom: 1.1rem; }
|
|
276
|
+
|
|
277
|
+
/* Wishlist saved state (robust, no JS text swap): fill the heart and relabel
|
|
278
|
+
when aria-pressed. Applies to card hearts and the PDP "Save for later". */
|
|
279
|
+
.wishlist-toggle[aria-pressed="true"] .wishlist-heart { fill: currentColor; }
|
|
280
|
+
.wishlist-toggle[aria-pressed="true"] .wl-label { font-size: 0; }
|
|
281
|
+
.wishlist-toggle[aria-pressed="true"] .wl-label::after {
|
|
282
|
+
content: "Saved";
|
|
283
|
+
font-size: 0.875rem;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* Quick-view dialog entrance (reduced-motion neutralized by the base guard). */
|
|
287
|
+
.quickview[open] { animation: tot-pop 0.18s cubic-bezier(0.22, 1, 0.36, 1); }
|
|
288
|
+
@keyframes tot-pop {
|
|
289
|
+
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
|
290
|
+
to { opacity: 1; transform: none; }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference theme — "Northwind" pilot identity. See DESIGN.md for full rationale.
|
|
3
|
+
*
|
|
4
|
+
* Thesis: a Pacific-Northwest maker of considered apparel + home goods.
|
|
5
|
+
* The voice is overcast-day calm with one sharp accent — like a single brass
|
|
6
|
+
* rivet on a waxed-canvas bag.
|
|
7
|
+
*
|
|
8
|
+
* Deliberate anti-AI-default moves (brief §6):
|
|
9
|
+
* - Palette is INK + SPRUCE on a warm GREIGE surface — NOT cream/terracotta,
|
|
10
|
+
* NOT near-black/acid-green. Sale red is a muted brick, not a siren.
|
|
11
|
+
* - Type pairing INVERTS the tired "serif headline" cliché: a tight geometric
|
|
12
|
+
* grotesque (Space Grotesk) carries display; a warm humanist serif
|
|
13
|
+
* (Source Serif 4) carries body copy. Personality up top, readability below.
|
|
14
|
+
* - Boldness is spent in ONE place: the homepage hero's oversized split-type
|
|
15
|
+
* lockup. Everything else stays quiet.
|
|
16
|
+
*/
|
|
17
|
+
import type { ThemeTokens } from "@tot/public-runtime";
|
|
18
|
+
|
|
19
|
+
export const referenceTheme: ThemeTokens = {
|
|
20
|
+
color: {
|
|
21
|
+
bg: "#F4F1EA", // warm greige paper — NOT cream, lower chroma + cooler
|
|
22
|
+
surface: "#FCFAF5", // raised card surface, a touch lighter than bg
|
|
23
|
+
text: "#1C2624", // spruce-ink, near-black with a green undertone
|
|
24
|
+
muted: "#5C6663", // slate-sage for secondary text
|
|
25
|
+
primary: "#1F3A34", // deep spruce green — the brand anchor
|
|
26
|
+
"primary-contrast": "#F4F1EA",
|
|
27
|
+
accent: "#B5612F", // burnt amber / brass — the single warm spark
|
|
28
|
+
border: "#DCD6C9", // soft sand hairline (NOT broadsheet black)
|
|
29
|
+
success: "#3F6B4F",
|
|
30
|
+
sale: "#9E3B2E", // muted brick red, not fire-engine
|
|
31
|
+
// Semantic/state extensions (contract optional; stated here as the canonical
|
|
32
|
+
// reference values). accent-ink + *-hover are intentionally omitted so the
|
|
33
|
+
// reference identity exercises the derived @theme defaults.
|
|
34
|
+
"surface-2": "#ECE7DB", // inset greige well, a step deeper than bg
|
|
35
|
+
warning: "#8A5717", // deep amber — AA on the light greige surfaces
|
|
36
|
+
error: "#A22014", // deep true-red, kept distinct from the brick `sale`
|
|
37
|
+
info: "#2C5B73", // calm slate-blue, distinct from the spruce `primary`
|
|
38
|
+
},
|
|
39
|
+
type: {
|
|
40
|
+
display: { family: "Space Grotesk", weights: [500, 600, 700] },
|
|
41
|
+
body: { family: "Source Serif 4", weights: [400, 500, 600] },
|
|
42
|
+
mono: { family: "Space Mono", weights: [400] },
|
|
43
|
+
scale: 1.25, // major third — generous but not theatrical
|
|
44
|
+
tracking: "-0.011em",
|
|
45
|
+
},
|
|
46
|
+
space: {
|
|
47
|
+
base: 4,
|
|
48
|
+
scale: [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64],
|
|
49
|
+
containerMaxWidth: "1240px",
|
|
50
|
+
sectionRhythm: "clamp(4rem, 9vw, 8rem)",
|
|
51
|
+
},
|
|
52
|
+
shape: {
|
|
53
|
+
radius: { sm: "3px", md: "6px", lg: "12px", full: "999px" },
|
|
54
|
+
borderWeights: { thin: "1px", medium: "1.5px", thick: "2px" },
|
|
55
|
+
shadow: {
|
|
56
|
+
sm: "0 1px 2px rgba(28, 38, 36, 0.06)",
|
|
57
|
+
md: "0 6px 24px -8px rgba(28, 38, 36, 0.14)",
|
|
58
|
+
lg: "0 24px 60px -20px rgba(28, 38, 36, 0.22)",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
brand: {
|
|
62
|
+
logoLight: "/brand/northwind-logo-light.svg",
|
|
63
|
+
logoDark: "/brand/northwind-logo-dark.svg",
|
|
64
|
+
favicon: "/favicon.svg",
|
|
65
|
+
ogImage: "/brand/northwind-og.svg",
|
|
66
|
+
motion: "full",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme token -> CSS custom property serialization.
|
|
3
|
+
*
|
|
4
|
+
* Components are theme-AGNOSTIC: they only ever read var(--color-primary),
|
|
5
|
+
* var(--font-display), etc. This module turns a resolved ThemeTokens object
|
|
6
|
+
* into the `<style>` block the middleware injects per request, so one component
|
|
7
|
+
* library renders every tenant's identity. See DESIGN.md.
|
|
8
|
+
*/
|
|
9
|
+
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
10
|
+
|
|
11
|
+
/** Deep-merge tenant overrides over the reference defaults. */
|
|
12
|
+
export function mergeTheme(
|
|
13
|
+
base: ThemeTokens,
|
|
14
|
+
override?: DeepPartial<ThemeTokens>,
|
|
15
|
+
): ThemeTokens {
|
|
16
|
+
if (!override) return base;
|
|
17
|
+
return deepMerge(base, override) as ThemeTokens;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function deepMerge<T>(base: T, over: DeepPartial<T>): T {
|
|
21
|
+
if (Array.isArray(base)) return (over as unknown as T) ?? base;
|
|
22
|
+
if (typeof base === "object" && base !== null) {
|
|
23
|
+
const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
|
24
|
+
for (const [k, v] of Object.entries(over as Record<string, unknown>)) {
|
|
25
|
+
if (v === undefined) continue;
|
|
26
|
+
const bv = (base as Record<string, unknown>)[k];
|
|
27
|
+
out[k] =
|
|
28
|
+
bv && typeof bv === "object" && !Array.isArray(bv) && typeof v === "object"
|
|
29
|
+
? deepMerge(bv, v as DeepPartial<unknown>)
|
|
30
|
+
: v;
|
|
31
|
+
}
|
|
32
|
+
return out as T;
|
|
33
|
+
}
|
|
34
|
+
return (over as unknown as T) ?? base;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Serialize ThemeTokens into a `:root { --x: y }` declaration block.
|
|
39
|
+
* Returns the INNER CSS (no <style> wrapper) so the caller controls nonce/attrs.
|
|
40
|
+
*/
|
|
41
|
+
export function themeToCssVars(t: ThemeTokens): string {
|
|
42
|
+
const lines: string[] = [];
|
|
43
|
+
const push = (name: string, value: string | number) =>
|
|
44
|
+
lines.push(` --${name}: ${value};`);
|
|
45
|
+
|
|
46
|
+
// Color
|
|
47
|
+
for (const [k, v] of Object.entries(t.color)) push(`color-${k}`, v);
|
|
48
|
+
|
|
49
|
+
// Type
|
|
50
|
+
push("font-display", `"${t.type.display.family}"`);
|
|
51
|
+
push("font-body", `"${t.type.body.family}"`);
|
|
52
|
+
if (t.type.mono) push("font-mono", `"${t.type.mono.family}"`);
|
|
53
|
+
push("type-scale", t.type.scale);
|
|
54
|
+
if (t.type.tracking) push("type-tracking", t.type.tracking);
|
|
55
|
+
// Emit a modular type scale (rem) from the ratio.
|
|
56
|
+
const ratio = t.type.scale;
|
|
57
|
+
const steps: Record<string, number> = {
|
|
58
|
+
"xs": -2, "sm": -1, "base": 0, "md": 1, "lg": 2,
|
|
59
|
+
"xl": 3, "2xl": 4, "3xl": 5, "4xl": 6, "5xl": 7,
|
|
60
|
+
};
|
|
61
|
+
for (const [name, step] of Object.entries(steps)) {
|
|
62
|
+
push(`fs-${name}`, `${(1 * Math.pow(ratio, step)).toFixed(4)}rem`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Space
|
|
66
|
+
push("space-base", `${t.space.base}px`);
|
|
67
|
+
t.space.scale.forEach((mult, i) =>
|
|
68
|
+
push(`space-${i}`, `${(t.space.base * mult).toFixed(2)}px`),
|
|
69
|
+
);
|
|
70
|
+
push("container-max", t.space.containerMaxWidth);
|
|
71
|
+
push("section-rhythm", t.space.sectionRhythm);
|
|
72
|
+
|
|
73
|
+
// Shape
|
|
74
|
+
push("radius-sm", t.shape.radius.sm);
|
|
75
|
+
push("radius-md", t.shape.radius.md);
|
|
76
|
+
push("radius-lg", t.shape.radius.lg);
|
|
77
|
+
push("radius-full", t.shape.radius.full);
|
|
78
|
+
push("border-thin", t.shape.borderWeights.thin);
|
|
79
|
+
push("border-medium", t.shape.borderWeights.medium);
|
|
80
|
+
push("border-thick", t.shape.borderWeights.thick);
|
|
81
|
+
push("shadow-sm", t.shape.shadow.sm);
|
|
82
|
+
push("shadow-md", t.shape.shadow.md);
|
|
83
|
+
push("shadow-lg", t.shape.shadow.lg);
|
|
84
|
+
|
|
85
|
+
return `:root{\n${lines.join("\n")}\n}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Google Fonts <link> for a theme's display+body+mono families (if Google-hosted). */
|
|
89
|
+
export function themeFontHref(t: ThemeTokens): string | null {
|
|
90
|
+
const families = new Map<string, number[]>();
|
|
91
|
+
families.set(t.type.display.family, t.type.display.weights);
|
|
92
|
+
families.set(t.type.body.family, t.type.body.weights);
|
|
93
|
+
if (t.type.mono) families.set(t.type.mono.family, t.type.mono.weights);
|
|
94
|
+
|
|
95
|
+
const params = [...families]
|
|
96
|
+
.filter(([fam]) => !SYSTEM_FONTS.has(fam))
|
|
97
|
+
.map(([fam, weights]) => {
|
|
98
|
+
const w = [...new Set(weights)].sort((a, b) => a - b).join(";");
|
|
99
|
+
return `family=${encodeURIComponent(fam).replace(/%20/g, "+")}:wght@${w}`;
|
|
100
|
+
});
|
|
101
|
+
if (!params.length) return null;
|
|
102
|
+
return `https://fonts.googleapis.com/css2?${params.join("&")}&display=swap`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SYSTEM_FONTS = new Set([
|
|
106
|
+
"system-ui",
|
|
107
|
+
"-apple-system",
|
|
108
|
+
"Arial",
|
|
109
|
+
"Helvetica",
|
|
110
|
+
"Georgia",
|
|
111
|
+
]);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-vendor theme registry.
|
|
3
|
+
*
|
|
4
|
+
* A tenant's theme override lives at the colocated repo-root path
|
|
5
|
+
* `tenants/<tenant_id>/theme.json` (a DeepPartial<ThemeTokens> — only the tokens
|
|
6
|
+
* you want to override; the reference theme fills the rest) and it's
|
|
7
|
+
* auto-discovered at build time. This is the seam the brand adopter targets: its
|
|
8
|
+
* `out/branding/<tenant>.theme.json` output is written to `tenants/<id>/theme.json`.
|
|
9
|
+
*
|
|
10
|
+
* Files may include documentation keys (`_vendor`, `_note`, `$schema`); those are
|
|
11
|
+
* stripped so only real tokens reach the theme merge.
|
|
12
|
+
*
|
|
13
|
+
* Depth: from src/themes/tenants/ the repo root is five levels up
|
|
14
|
+
* (tenants→themes→src→storefront→apps→root).
|
|
15
|
+
*/
|
|
16
|
+
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
17
|
+
|
|
18
|
+
const modules = import.meta.glob<Record<string, unknown>>(
|
|
19
|
+
"../../../../../tenants/*/theme.json",
|
|
20
|
+
{
|
|
21
|
+
eager: true,
|
|
22
|
+
import: "default",
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function stripMeta(obj: Record<string, unknown>): DeepPartial<ThemeTokens> {
|
|
27
|
+
const out: Record<string, unknown> = {};
|
|
28
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
29
|
+
if (k.startsWith("_") || k.startsWith("$")) continue;
|
|
30
|
+
out[k] = v;
|
|
31
|
+
}
|
|
32
|
+
return out as DeepPartial<ThemeTokens>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const THEMES: Record<string, DeepPartial<ThemeTokens>> = {};
|
|
36
|
+
for (const [path, mod] of Object.entries(modules)) {
|
|
37
|
+
// Path looks like ".../tenants/<id>/theme.json" — the id is the dir segment.
|
|
38
|
+
const id = /\/tenants\/([^/]+)\/theme\.json$/.exec(path)?.[1];
|
|
39
|
+
if (!id) continue;
|
|
40
|
+
THEMES[id] = stripMeta(mod as Record<string, unknown>);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Theme-token overrides for a tenant, or {} if the vendor has no theme file. */
|
|
44
|
+
export function getTenantTheme(tenantId: string): DeepPartial<ThemeTokens> {
|
|
45
|
+
return THEMES[tenantId] ?? {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Tenant ids that ship a theme file (for diagnostics / listing). */
|
|
49
|
+
export function tenantsWithThemes(): string[] {
|
|
50
|
+
return Object.keys(THEMES);
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "astro/tsconfigs/strict",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"strict": true,
|
|
5
|
+
"noUncheckedIndexedAccess": true,
|
|
6
|
+
"verbatimModuleSyntax": false,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "preact",
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["src/*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"include": [".astro/types.d.ts", "src/**/*", "*.config.mjs", "env.d.ts"],
|
|
15
|
+
"exclude": ["dist", "node_modules"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
// Storefront unit tests — pure TS logic (tenant routing, search, query engine,
|
|
5
|
+
// image-attr resolution, edge cache, etc.). We deliberately do NOT render .astro
|
|
6
|
+
// components here: the Astro Container API needs Astro's Vite pipeline, whose
|
|
7
|
+
// vite-plugin-head crashes under vitest@2 + astro@7 (configureServer reads an
|
|
8
|
+
// undefined `ssr`). Render *logic* is extracted into testable modules instead
|
|
9
|
+
// (e.g. lib/imageAttrs.ts), and rendered output is verified on a live preview.
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
// Mirror the `@` -> src alias from tsconfig so tests can import aliased modules
|
|
12
|
+
// (e.g. src/config/tenants.ts, which pulls the theme registry).
|
|
13
|
+
resolve: {
|
|
14
|
+
alias: {
|
|
15
|
+
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
test: {
|
|
19
|
+
include: ["src/**/*.test.ts"],
|
|
20
|
+
environment: "node",
|
|
21
|
+
},
|
|
22
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
|
+
"packageManager": "pnpm@11.9.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "pnpm --filter @tot/storefront-runner dev",
|
|
12
|
+
"build": "pnpm --filter @tot/storefront-runner build",
|
|
13
|
+
"typecheck": "pnpm -r typecheck",
|
|
14
|
+
"depcruise": "pnpm --filter @tot/storefront-runner depcruise"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.6.3"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"storefront-runner": "scripts/tot-dev.mjs"
|
|
25
|
+
}
|
|
26
|
+
}
|