create-tanqory-theme 0.0.0-stage → 0.2.3
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 +21 -0
- package/README.md +55 -2
- package/bin/create-tanqory-theme.mjs +13 -0
- package/lib/agents.mjs +118 -0
- package/lib/create.mjs +245 -0
- package/package.json +39 -4
- package/templates/blank/README.md +13 -0
- package/templates/blank/config/settings.json +3 -0
- package/templates/blank/locales/en.json +22 -0
- package/templates/blank/locales/th.json +22 -0
- package/templates/blank/package.json +34 -0
- package/templates/blank/regions/footer.json +35 -0
- package/templates/blank/regions/header.json +13 -0
- package/templates/blank/scripts/build.mjs +193 -0
- package/templates/blank/scripts/probe.ts +4 -0
- package/templates/blank/src/bundle.ts +15 -0
- package/templates/blank/src/components/parts.tsx +32 -0
- package/templates/blank/src/entry.client.tsx +6 -0
- package/templates/blank/src/entry.server.tsx +13 -0
- package/templates/blank/src/sections/chrome.tsx +132 -0
- package/templates/blank/src/sections/commerce.tsx +242 -0
- package/templates/blank/src/sections/content.tsx +111 -0
- package/templates/blank/src/sections/editorial.tsx +107 -0
- package/templates/blank/src/settings.ts +15 -0
- package/templates/blank/src/styles/theme.css +195 -0
- package/templates/blank/src/templates.ts +14 -0
- package/templates/blank/src/theme.tsx +76 -0
- package/templates/blank/template.json +24 -0
- package/templates/blank/templates/404.json +14 -0
- package/templates/blank/templates/account.json +14 -0
- package/templates/blank/templates/article.json +14 -0
- package/templates/blank/templates/blog.json +14 -0
- package/templates/blank/templates/cart.json +14 -0
- package/templates/blank/templates/collection.json +14 -0
- package/templates/blank/templates/index.json +36 -0
- package/templates/blank/templates/page.contact.json +18 -0
- package/templates/blank/templates/page.json +14 -0
- package/templates/blank/templates/product.json +18 -0
- package/templates/blank/templates/search.json +14 -0
- package/templates/blank/tests/theme.test.ts +49 -0
- package/templates/blank/tsconfig.json +17 -0
- package/templates/blank/vite.client.config.ts +17 -0
- package/templates/blank/vite.probe.config.ts +16 -0
- package/templates/blank/vite.server.config.ts +32 -0
- package/templates/blank/vitest.config.ts +9 -0
- package/templates/common/.claude/settings.json +8 -0
- package/templates/common/.codex/config.toml +4 -0
- package/templates/common/.gemini/settings.json +12 -0
- package/templates/common/.github/workflows/theme-check.yml +44 -0
- package/templates/common/.mcp.json +12 -0
- package/templates/common/.vscode/mcp.json +9 -0
- package/templates/common/_gitignore +9 -0
- package/templates/common/scripts/check.mjs +44 -0
- package/templates/common/scripts/dev.mjs +30 -0
- package/templates/common/tanqory.config.json +6 -0
- package/templates/nova/.gitattributes +3 -0
- package/templates/nova/.tanqory/contracts/LOCK.json +66 -0
- package/templates/nova/.tanqory/contracts/nova-01.json +1092 -0
- package/templates/nova/.tanqory/contracts/nova-12A.json +2674 -0
- package/templates/nova/.tanqory/contracts/nova-12B.json +2285 -0
- package/templates/nova/.tanqory/contracts/nova-12C.json +2848 -0
- package/templates/nova/.tanqory/contracts/nova-12D.json +3039 -0
- package/templates/nova/.tanqory/contracts/nova-12E.json +1473 -0
- package/templates/nova/.tanqory/contracts/nova-12F.json +1670 -0
- package/templates/nova/.tanqory/contracts/nova-12G.json +1661 -0
- package/templates/nova/.tanqory/contracts/nova-13.json +301 -0
- package/templates/nova/.tanqory/contracts/nova-14.json +172 -0
- package/templates/nova/.tanqory/legacy-settings.json +1121 -0
- package/templates/nova/README.md +150 -0
- package/templates/nova/_gitignore +28 -0
- package/templates/nova/assets/ai-tokens.css +6 -0
- package/templates/nova/assets/starter/starter-art-1200x800-0b773ac4.svg +1 -0
- package/templates/nova/assets/starter/starter-art-1200x900-59600290.svg +1 -0
- package/templates/nova/assets/starter/starter-art-1200x900-c549d496.svg +1 -0
- package/templates/nova/assets/starter/starter-art-1600x900-8f979fcb.svg +1 -0
- package/templates/nova/assets/starter/starter-art-2100x900-cbb71bfd.svg +1 -0
- package/templates/nova/config/legacy-settings.json +438 -0
- package/templates/nova/config/promotion-popup.schema.ts +29 -0
- package/templates/nova/config/settings.json +23 -0
- package/templates/nova/config/settings.schema.ts +231 -0
- package/templates/nova/entry.abi.tsx +27 -0
- package/templates/nova/groups/footer.json +39 -0
- package/templates/nova/groups/header.json +11 -0
- package/templates/nova/lib/abi/abi.css +41 -0
- package/templates/nova/lib/bundle-core.ts +35 -0
- package/templates/nova/lib/bundle.client.ts +15 -0
- package/templates/nova/lib/bundle.ts +15 -0
- package/templates/nova/lib/legacy-settings.ts +104 -0
- package/templates/nova/locales/ar.json +590 -0
- package/templates/nova/locales/en.json +590 -0
- package/templates/nova/locales/th.json +590 -0
- package/templates/nova/main.abi.tsx +12 -0
- package/templates/nova/package.json +44 -0
- package/templates/nova/scripts/abi/build.mjs +145 -0
- package/templates/nova/scripts/abi/check-content.mjs +99 -0
- package/templates/nova/scripts/abi/jcs.mjs +27 -0
- package/templates/nova/scripts/abi/local-host.mjs +294 -0
- package/templates/nova/scripts/abi/manifest.mjs +392 -0
- package/templates/nova/scripts/abi/starter-assets.mjs +32 -0
- package/templates/nova/scripts/compiler/checks/check-a11y.mjs +88 -0
- package/templates/nova/scripts/compiler/checks/check-layout-sanity.mjs +169 -0
- package/templates/nova/scripts/compiler/checks/check-page-headings.mjs +67 -0
- package/templates/nova/scripts/compiler/checks/check-product-jsonld.mjs +80 -0
- package/templates/nova/scripts/compiler/checks/theme-spec/check.mjs +88 -0
- package/templates/nova/scripts/compiler/contracts/extract.mjs +188 -0
- package/templates/nova/scripts/compiler/contracts/index.ts +125 -0
- package/templates/nova/scripts/compiler/contracts/validate.ts +347 -0
- package/templates/nova/scripts/compiler/manifest/gen-manifest.mjs +156 -0
- package/templates/nova/scripts/compiler/manifest/manifest-entry.ts +386 -0
- package/templates/nova/scripts/compiler/manifest/manifest-sections.ts +247 -0
- package/templates/nova/scripts/compiler/migrate/migrate-content.mjs +171 -0
- package/templates/nova/scripts/compiler/migrate/migrate-groups.mjs +159 -0
- package/templates/nova/scripts/parity/case-shim.ts +14 -0
- package/templates/nova/scripts/parity/emit.ts +206 -0
- package/templates/nova/scripts/parity/index.html +12 -0
- package/templates/nova/scripts/parity/main.tsx +23 -0
- package/templates/nova/scripts/parity/pages.tsx +123 -0
- package/templates/nova/scripts/parity/vite.config.ts +29 -0
- package/templates/nova/src/components/base/Accordion.css +20 -0
- package/templates/nova/src/components/base/Accordion.tsx +50 -0
- package/templates/nova/src/components/base/Badge.css +17 -0
- package/templates/nova/src/components/base/Badge.tsx +26 -0
- package/templates/nova/src/components/base/Button.css +73 -0
- package/templates/nova/src/components/base/Button.tsx +64 -0
- package/templates/nova/src/components/base/Checkbox.tsx +18 -0
- package/templates/nova/src/components/base/Chip.css +20 -0
- package/templates/nova/src/components/base/Chip.tsx +33 -0
- package/templates/nova/src/components/base/Divider.css +6 -0
- package/templates/nova/src/components/base/Divider.tsx +28 -0
- package/templates/nova/src/components/base/IconButton.css +34 -0
- package/templates/nova/src/components/base/IconButton.tsx +41 -0
- package/templates/nova/src/components/base/Input.css +21 -0
- package/templates/nova/src/components/base/Input.tsx +24 -0
- package/templates/nova/src/components/base/Link.css +15 -0
- package/templates/nova/src/components/base/Link.tsx +23 -0
- package/templates/nova/src/components/base/Radio.tsx +13 -0
- package/templates/nova/src/components/base/Select.css +18 -0
- package/templates/nova/src/components/base/Select.tsx +34 -0
- package/templates/nova/src/components/base/Skeleton.css +17 -0
- package/templates/nova/src/components/base/Skeleton.tsx +23 -0
- package/templates/nova/src/components/base/Spinner.css +13 -0
- package/templates/nova/src/components/base/Spinner.tsx +8 -0
- package/templates/nova/src/components/base/Switch.tsx +16 -0
- package/templates/nova/src/components/base/Tabs.css +24 -0
- package/templates/nova/src/components/base/Tabs.tsx +61 -0
- package/templates/nova/src/components/base/Textarea.css +1 -0
- package/templates/nova/src/components/base/Textarea.tsx +8 -0
- package/templates/nova/src/components/base/choice-props.ts +9 -0
- package/templates/nova/src/components/base/choice.css +69 -0
- package/templates/nova/src/components/commerce/AddToCart.css +4 -0
- package/templates/nova/src/components/commerce/AddToCart.tsx +77 -0
- package/templates/nova/src/components/commerce/CartLine.css +26 -0
- package/templates/nova/src/components/commerce/CartLine.tsx +82 -0
- package/templates/nova/src/components/commerce/CartSummary.css +23 -0
- package/templates/nova/src/components/commerce/CartSummary.tsx +120 -0
- package/templates/nova/src/components/commerce/CollectionCard.css +66 -0
- package/templates/nova/src/components/commerce/CollectionCard.tsx +57 -0
- package/templates/nova/src/components/commerce/ComparisonTable.css +33 -0
- package/templates/nova/src/components/commerce/ComparisonTable.tsx +113 -0
- package/templates/nova/src/components/commerce/EarnHint.css +3 -0
- package/templates/nova/src/components/commerce/EarnHint.tsx +31 -0
- package/templates/nova/src/components/commerce/Hotspot.css +16 -0
- package/templates/nova/src/components/commerce/Hotspot.tsx +64 -0
- package/templates/nova/src/components/commerce/ListingParts.css +79 -0
- package/templates/nova/src/components/commerce/ListingParts.tsx +211 -0
- package/templates/nova/src/components/commerce/LookItem.css +19 -0
- package/templates/nova/src/components/commerce/LookItem.tsx +41 -0
- package/templates/nova/src/components/commerce/MiniCartLine.css +20 -0
- package/templates/nova/src/components/commerce/MiniCartLine.tsx +93 -0
- package/templates/nova/src/components/commerce/PaymentMark.css +2 -0
- package/templates/nova/src/components/commerce/PaymentMark.tsx +55 -0
- package/templates/nova/src/components/commerce/Price.css +27 -0
- package/templates/nova/src/components/commerce/Price.tsx +55 -0
- package/templates/nova/src/components/commerce/ProductCard.css +66 -0
- package/templates/nova/src/components/commerce/ProductCard.tsx +117 -0
- package/templates/nova/src/components/commerce/ProductGallery.css +72 -0
- package/templates/nova/src/components/commerce/ProductGallery.tsx +98 -0
- package/templates/nova/src/components/commerce/QuantitySelector.css +23 -0
- package/templates/nova/src/components/commerce/QuantitySelector.tsx +45 -0
- package/templates/nova/src/components/commerce/Reviews.css +34 -0
- package/templates/nova/src/components/commerce/Reviews.tsx +112 -0
- package/templates/nova/src/components/commerce/Stars.css +7 -0
- package/templates/nova/src/components/commerce/Stars.tsx +38 -0
- package/templates/nova/src/components/commerce/VariantPicker.css +31 -0
- package/templates/nova/src/components/commerce/VariantPicker.tsx +122 -0
- package/templates/nova/src/components/commerce/blocks/Badge.css +6 -0
- package/templates/nova/src/components/commerce/blocks/Badge.tsx +33 -0
- package/templates/nova/src/components/commerce/blocks/PaymentIcons.tsx +43 -0
- package/templates/nova/src/components/commerce/blocks/Swatches.tsx +17 -0
- package/templates/nova/src/components/commerce/blocks/payment.css +2 -0
- package/templates/nova/src/components/commerce/cart-limit.ts +11 -0
- package/templates/nova/src/components/commerce/money.ts +34 -0
- package/templates/nova/src/components/commerce/swatch.ts +54 -0
- package/templates/nova/src/components/content/ArticleCard.css +22 -0
- package/templates/nova/src/components/content/ArticleCard.tsx +53 -0
- package/templates/nova/src/components/content/BeforeAfterSlider.css +38 -0
- package/templates/nova/src/components/content/BeforeAfterSlider.tsx +87 -0
- package/templates/nova/src/components/content/CardRow.css +80 -0
- package/templates/nova/src/components/content/CardRow.tsx +140 -0
- package/templates/nova/src/components/content/CarouselControls.css +61 -0
- package/templates/nova/src/components/content/CarouselControls.tsx +90 -0
- package/templates/nova/src/components/content/ContentsNav.css +16 -0
- package/templates/nova/src/components/content/ContentsNav.tsx +55 -0
- package/templates/nova/src/components/content/Countdown.css +24 -0
- package/templates/nova/src/components/content/Countdown.tsx +115 -0
- package/templates/nova/src/components/content/HeroCopy.css +92 -0
- package/templates/nova/src/components/content/HeroCopy.tsx +148 -0
- package/templates/nova/src/components/content/HeroFrame.css +75 -0
- package/templates/nova/src/components/content/HeroFrame.tsx +49 -0
- package/templates/nova/src/components/content/HeroMedia.css +40 -0
- package/templates/nova/src/components/content/HeroMedia.tsx +170 -0
- package/templates/nova/src/components/content/LongForm.css +40 -0
- package/templates/nova/src/components/content/LongForm.tsx +56 -0
- package/templates/nova/src/components/content/MediaFrame.css +5 -0
- package/templates/nova/src/components/content/MediaFrame.tsx +52 -0
- package/templates/nova/src/components/content/RichText.tsx +30 -0
- package/templates/nova/src/components/content/SectionHead.css +8 -0
- package/templates/nova/src/components/content/SectionHead.tsx +34 -0
- package/templates/nova/src/components/content/ShareButtons.css +2 -0
- package/templates/nova/src/components/content/ShareButtons.tsx +40 -0
- package/templates/nova/src/components/content/StoreCard.css +14 -0
- package/templates/nova/src/components/content/StoreCard.tsx +60 -0
- package/templates/nova/src/components/content/TestimonialCard.css +36 -0
- package/templates/nova/src/components/content/TestimonialCard.tsx +102 -0
- package/templates/nova/src/components/content/UgcTile.css +8 -0
- package/templates/nova/src/components/content/UgcTile.tsx +33 -0
- package/templates/nova/src/components/content/blocks/Button.tsx +36 -0
- package/templates/nova/src/components/content/blocks/Column.tsx +30 -0
- package/templates/nova/src/components/content/blocks/Heading.tsx +31 -0
- package/templates/nova/src/components/content/blocks/Icon.tsx +25 -0
- package/templates/nova/src/components/content/blocks/Image.tsx +42 -0
- package/templates/nova/src/components/content/blocks/Text.tsx +26 -0
- package/templates/nova/src/components/content/blocks/Video.tsx +44 -0
- package/templates/nova/src/components/content/blocks/content.css +25 -0
- package/templates/nova/src/components/content/blocks/shared.ts +5 -0
- package/templates/nova/src/components/forms/ContactForm.css +19 -0
- package/templates/nova/src/components/forms/ContactForm.tsx +139 -0
- package/templates/nova/src/components/forms/FormField.css +15 -0
- package/templates/nova/src/components/forms/FormField.tsx +77 -0
- package/templates/nova/src/components/forms/GatewayFields.tsx +79 -0
- package/templates/nova/src/components/forms/NewsletterForm.css +9 -0
- package/templates/nova/src/components/forms/NewsletterForm.tsx +141 -0
- package/templates/nova/src/components/forms/SearchField.css +7 -0
- package/templates/nova/src/components/forms/SearchField.tsx +68 -0
- package/templates/nova/src/components/forms/blocks/Newsletter.css +5 -0
- package/templates/nova/src/components/forms/blocks/Newsletter.tsx +71 -0
- package/templates/nova/src/components/forms/validate.ts +4 -0
- package/templates/nova/src/components/listing/ProductListing.tsx +200 -0
- package/templates/nova/src/components/navigation/BrandLogo.tsx +35 -0
- package/templates/nova/src/components/navigation/Breadcrumb.css +16 -0
- package/templates/nova/src/components/navigation/Breadcrumb.tsx +59 -0
- package/templates/nova/src/components/navigation/LineAddFriend.css +5 -0
- package/templates/nova/src/components/navigation/LineAddFriend.tsx +42 -0
- package/templates/nova/src/components/navigation/LocaleSelector.css +33 -0
- package/templates/nova/src/components/navigation/LocaleSelector.tsx +64 -0
- package/templates/nova/src/components/navigation/Menu.tsx +26 -0
- package/templates/nova/src/components/navigation/Pagination.css +24 -0
- package/templates/nova/src/components/navigation/Pagination.tsx +74 -0
- package/templates/nova/src/components/navigation/RecoveryLinks.css +8 -0
- package/templates/nova/src/components/navigation/RecoveryLinks.tsx +23 -0
- package/templates/nova/src/components/navigation/SocialLinks.css +5 -0
- package/templates/nova/src/components/navigation/SocialLinks.tsx +51 -0
- package/templates/nova/src/components/navigation/blocks/SocialLinks.tsx +23 -0
- package/templates/nova/src/components/overlays/AccountMenu.css +11 -0
- package/templates/nova/src/components/overlays/AccountMenu.tsx +91 -0
- package/templates/nova/src/components/overlays/CartDrawer.css +17 -0
- package/templates/nova/src/components/overlays/CartDrawer.tsx +118 -0
- package/templates/nova/src/components/overlays/ConfirmationDialog.tsx +49 -0
- package/templates/nova/src/components/overlays/CookieConsent.css +56 -0
- package/templates/nova/src/components/overlays/CookieConsent.tsx +170 -0
- package/templates/nova/src/components/overlays/Drawer.css +38 -0
- package/templates/nova/src/components/overlays/Drawer.tsx +66 -0
- package/templates/nova/src/components/overlays/FriendOfferPanel.tsx +79 -0
- package/templates/nova/src/components/overlays/MarketSelector.css +111 -0
- package/templates/nova/src/components/overlays/MarketSelector.tsx +450 -0
- package/templates/nova/src/components/overlays/Menu.css +9 -0
- package/templates/nova/src/components/overlays/Menu.tsx +124 -0
- package/templates/nova/src/components/overlays/MobileNavDrawer.css +28 -0
- package/templates/nova/src/components/overlays/MobileNavDrawer.tsx +166 -0
- package/templates/nova/src/components/overlays/Modal.css +52 -0
- package/templates/nova/src/components/overlays/Modal.tsx +88 -0
- package/templates/nova/src/components/overlays/Popover.css +4 -0
- package/templates/nova/src/components/overlays/Popover.tsx +115 -0
- package/templates/nova/src/components/overlays/ProductLightbox.css +20 -0
- package/templates/nova/src/components/overlays/ProductLightbox.tsx +70 -0
- package/templates/nova/src/components/overlays/PromoPopup.css +32 -0
- package/templates/nova/src/components/overlays/PromoPopup.tsx +198 -0
- package/templates/nova/src/components/overlays/QuickView.css +31 -0
- package/templates/nova/src/components/overlays/QuickView.tsx +127 -0
- package/templates/nova/src/components/overlays/ReferralPanel.css +16 -0
- package/templates/nova/src/components/overlays/ReferralPanel.tsx +138 -0
- package/templates/nova/src/components/overlays/RewardsLauncher.css +22 -0
- package/templates/nova/src/components/overlays/RewardsLauncher.tsx +270 -0
- package/templates/nova/src/components/overlays/SearchModal.css +48 -0
- package/templates/nova/src/components/overlays/SearchModal.tsx +302 -0
- package/templates/nova/src/components/overlays/Sheet.css +16 -0
- package/templates/nova/src/components/overlays/Sheet.tsx +48 -0
- package/templates/nova/src/components/overlays/SizeGuide.css +8 -0
- package/templates/nova/src/components/overlays/SizeGuide.tsx +74 -0
- package/templates/nova/src/components/overlays/Toast.css +20 -0
- package/templates/nova/src/components/overlays/Toast.tsx +164 -0
- package/templates/nova/src/components/overlays/Tooltip.css +6 -0
- package/templates/nova/src/components/overlays/Tooltip.tsx +59 -0
- package/templates/nova/src/components/overlays/use-dialog.ts +74 -0
- package/templates/nova/src/components/section/shared.css +42 -0
- package/templates/nova/src/components/section/shared.ts +120 -0
- package/templates/nova/src/components/section/shell.tsx +29 -0
- package/templates/nova/src/components/states/Alert.css +10 -0
- package/templates/nova/src/components/states/Alert.tsx +50 -0
- package/templates/nova/src/components/states/EditorFrame.css +31 -0
- package/templates/nova/src/components/states/EditorFrame.tsx +43 -0
- package/templates/nova/src/components/states/MediaState.css +1 -0
- package/templates/nova/src/components/states/MediaState.tsx +49 -0
- package/templates/nova/src/components/states/OfflineNotice.css +4 -0
- package/templates/nova/src/components/states/OfflineNotice.tsx +24 -0
- package/templates/nova/src/components/states/SkeletonLayout.css +22 -0
- package/templates/nova/src/components/states/SkeletonLayout.tsx +104 -0
- package/templates/nova/src/components/states/StateBlock.css +22 -0
- package/templates/nova/src/components/states/StateBlock.tsx +39 -0
- package/templates/nova/src/components/states/media/MediaError.css +6 -0
- package/templates/nova/src/components/states/media/MediaError.tsx +49 -0
- package/templates/nova/src/components/states/media/MediaPlaceholder.css +24 -0
- package/templates/nova/src/components/states/media/MediaPlaceholder.tsx +101 -0
- package/templates/nova/src/components/states/media/placeholder-art.ts +142 -0
- package/templates/nova/src/layouts/Main.tsx +80 -0
- package/templates/nova/src/sections/Account/Account.tsx +109 -0
- package/templates/nova/src/sections/Account/index.ts +22 -0
- package/templates/nova/src/sections/Account/presets.ts +6 -0
- package/templates/nova/src/sections/Account/schema.ts +29 -0
- package/templates/nova/src/sections/AnnouncementBar/AnnouncementBar.css +37 -0
- package/templates/nova/src/sections/AnnouncementBar/AnnouncementBar.tsx +151 -0
- package/templates/nova/src/sections/AnnouncementBar/index.ts +20 -0
- package/templates/nova/src/sections/AnnouncementBar/presets.ts +10 -0
- package/templates/nova/src/sections/AnnouncementBar/schema.ts +26 -0
- package/templates/nova/src/sections/ArticleBody/ArticleBody.tsx +129 -0
- package/templates/nova/src/sections/ArticleBody/index.ts +21 -0
- package/templates/nova/src/sections/ArticleBody/presets.ts +10 -0
- package/templates/nova/src/sections/ArticleBody/schema.ts +40 -0
- package/templates/nova/src/sections/BeforeAfter/BeforeAfter.tsx +47 -0
- package/templates/nova/src/sections/BeforeAfter/blocks/Comparison.tsx +20 -0
- package/templates/nova/src/sections/BeforeAfter/index.ts +28 -0
- package/templates/nova/src/sections/BeforeAfter/presets.ts +10 -0
- package/templates/nova/src/sections/BeforeAfter/schema.ts +34 -0
- package/templates/nova/src/sections/BlogPosts/BlogPosts.tsx +106 -0
- package/templates/nova/src/sections/BlogPosts/index.ts +30 -0
- package/templates/nova/src/sections/BlogPosts/presets.ts +10 -0
- package/templates/nova/src/sections/BlogPosts/schema.ts +42 -0
- package/templates/nova/src/sections/Breadcrumbs/Breadcrumbs.css +10 -0
- package/templates/nova/src/sections/Breadcrumbs/Breadcrumbs.tsx +41 -0
- package/templates/nova/src/sections/Breadcrumbs/index.ts +20 -0
- package/templates/nova/src/sections/Breadcrumbs/presets.ts +6 -0
- package/templates/nova/src/sections/Breadcrumbs/schema.ts +24 -0
- package/templates/nova/src/sections/CartItems/CartItems.tsx +170 -0
- package/templates/nova/src/sections/CartItems/blocks/CartBadge.tsx +14 -0
- package/templates/nova/src/sections/CartItems/index.ts +37 -0
- package/templates/nova/src/sections/CartItems/presets.ts +8 -0
- package/templates/nova/src/sections/CartItems/schema.ts +34 -0
- package/templates/nova/src/sections/CollectionBanner/CollectionBanner.tsx +120 -0
- package/templates/nova/src/sections/CollectionBanner/index.ts +25 -0
- package/templates/nova/src/sections/CollectionBanner/presets.ts +10 -0
- package/templates/nova/src/sections/CollectionBanner/schema.ts +37 -0
- package/templates/nova/src/sections/CollectionLinks/CollectionLinks.tsx +47 -0
- package/templates/nova/src/sections/CollectionLinks/index.ts +20 -0
- package/templates/nova/src/sections/CollectionLinks/presets.ts +8 -0
- package/templates/nova/src/sections/CollectionLinks/schema.ts +33 -0
- package/templates/nova/src/sections/CollectionList/CollectionList.tsx +89 -0
- package/templates/nova/src/sections/CollectionList/blocks/CollectionItem.tsx +25 -0
- package/templates/nova/src/sections/CollectionList/index.ts +33 -0
- package/templates/nova/src/sections/CollectionList/presets.ts +10 -0
- package/templates/nova/src/sections/CollectionList/schema.ts +41 -0
- package/templates/nova/src/sections/ComparisonTable/ComparisonTable.tsx +52 -0
- package/templates/nova/src/sections/ComparisonTable/blocks/CompareItem.tsx +26 -0
- package/templates/nova/src/sections/ComparisonTable/index.ts +31 -0
- package/templates/nova/src/sections/ComparisonTable/presets.ts +8 -0
- package/templates/nova/src/sections/ComparisonTable/schema.ts +31 -0
- package/templates/nova/src/sections/ContactForm/ContactForm.tsx +83 -0
- package/templates/nova/src/sections/ContactForm/blocks/ContactDetail.tsx +13 -0
- package/templates/nova/src/sections/ContactForm/index.ts +29 -0
- package/templates/nova/src/sections/ContactForm/presets.ts +8 -0
- package/templates/nova/src/sections/ContactForm/schema.ts +34 -0
- package/templates/nova/src/sections/Divider/Divider.css +17 -0
- package/templates/nova/src/sections/Divider/Divider.tsx +21 -0
- package/templates/nova/src/sections/Divider/index.ts +20 -0
- package/templates/nova/src/sections/Divider/presets.ts +10 -0
- package/templates/nova/src/sections/Divider/schema.ts +24 -0
- package/templates/nova/src/sections/Faq/Faq.tsx +51 -0
- package/templates/nova/src/sections/Faq/blocks/FaqItem.tsx +13 -0
- package/templates/nova/src/sections/Faq/index.ts +28 -0
- package/templates/nova/src/sections/Faq/presets.ts +8 -0
- package/templates/nova/src/sections/Faq/schema.ts +32 -0
- package/templates/nova/src/sections/FeatureGridBlocks/FeatureGridBlocks.tsx +90 -0
- package/templates/nova/src/sections/FeatureGridBlocks/index.ts +35 -0
- package/templates/nova/src/sections/FeatureGridBlocks/presets.ts +8 -0
- package/templates/nova/src/sections/FeatureGridBlocks/schema.ts +35 -0
- package/templates/nova/src/sections/FeatureHighlights/FeatureHighlights.tsx +56 -0
- package/templates/nova/src/sections/FeatureHighlights/index.ts +20 -0
- package/templates/nova/src/sections/FeatureHighlights/presets.ts +6 -0
- package/templates/nova/src/sections/FeatureHighlights/schema.ts +27 -0
- package/templates/nova/src/sections/FeaturedCollection/FeaturedCollection.tsx +67 -0
- package/templates/nova/src/sections/FeaturedCollection/index.ts +21 -0
- package/templates/nova/src/sections/FeaturedCollection/presets.ts +10 -0
- package/templates/nova/src/sections/FeaturedCollection/schema.ts +39 -0
- package/templates/nova/src/sections/FeaturedProduct/FeaturedProduct.tsx +107 -0
- package/templates/nova/src/sections/FeaturedProduct/index.ts +23 -0
- package/templates/nova/src/sections/FeaturedProduct/presets.ts +8 -0
- package/templates/nova/src/sections/FeaturedProduct/schema.ts +35 -0
- package/templates/nova/src/sections/Footer/Footer.css +81 -0
- package/templates/nova/src/sections/Footer/Footer.tsx +256 -0
- package/templates/nova/src/sections/Footer/blocks/FooterBrand.tsx +40 -0
- package/templates/nova/src/sections/Footer/blocks/FooterLine.tsx +11 -0
- package/templates/nova/src/sections/Footer/blocks/FooterMenu.tsx +14 -0
- package/templates/nova/src/sections/Footer/blocks/FooterText.tsx +8 -0
- package/templates/nova/src/sections/Footer/blocks/footer.css +7 -0
- package/templates/nova/src/sections/Footer/index.ts +38 -0
- package/templates/nova/src/sections/Footer/presets.ts +10 -0
- package/templates/nova/src/sections/Footer/schema.ts +27 -0
- package/templates/nova/src/sections/Group/Group.css +59 -0
- package/templates/nova/src/sections/Group/Group.tsx +167 -0
- package/templates/nova/src/sections/Group/blocks/Accordion.tsx +26 -0
- package/templates/nova/src/sections/Group/blocks/JumboText.tsx +20 -0
- package/templates/nova/src/sections/Group/blocks/Spacer.tsx +16 -0
- package/templates/nova/src/sections/Group/index.ts +36 -0
- package/templates/nova/src/sections/Group/presets.ts +12 -0
- package/templates/nova/src/sections/Group/schema.ts +35 -0
- package/templates/nova/src/sections/Header/Header.css +185 -0
- package/templates/nova/src/sections/Header/Header.tsx +478 -0
- package/templates/nova/src/sections/Header/index.ts +32 -0
- package/templates/nova/src/sections/Header/presets.ts +10 -0
- package/templates/nova/src/sections/Header/schema.ts +32 -0
- package/templates/nova/src/sections/Hero/Hero.tsx +128 -0
- package/templates/nova/src/sections/Hero/index.ts +24 -0
- package/templates/nova/src/sections/Hero/presets.ts +16 -0
- package/templates/nova/src/sections/Hero/schema.ts +56 -0
- package/templates/nova/src/sections/ImageGallery/ImageGallery.tsx +86 -0
- package/templates/nova/src/sections/ImageGallery/index.ts +28 -0
- package/templates/nova/src/sections/ImageGallery/presets.ts +10 -0
- package/templates/nova/src/sections/ImageGallery/schema.ts +34 -0
- package/templates/nova/src/sections/ImageWithText/ImageWithText.tsx +62 -0
- package/templates/nova/src/sections/ImageWithText/index.ts +23 -0
- package/templates/nova/src/sections/ImageWithText/presets.ts +12 -0
- package/templates/nova/src/sections/ImageWithText/schema.ts +45 -0
- package/templates/nova/src/sections/LineAddFriend/LineAddFriend.tsx +25 -0
- package/templates/nova/src/sections/LineAddFriend/index.ts +20 -0
- package/templates/nova/src/sections/LineAddFriend/presets.ts +3 -0
- package/templates/nova/src/sections/LineAddFriend/schema.ts +25 -0
- package/templates/nova/src/sections/LogoList/LogoList.tsx +40 -0
- package/templates/nova/src/sections/LogoList/blocks/Logo.tsx +18 -0
- package/templates/nova/src/sections/LogoList/index.ts +29 -0
- package/templates/nova/src/sections/LogoList/presets.ts +8 -0
- package/templates/nova/src/sections/LogoList/schema.ts +29 -0
- package/templates/nova/src/sections/MainCollection/MainCollection.tsx +51 -0
- package/templates/nova/src/sections/MainCollection/index.ts +26 -0
- package/templates/nova/src/sections/MainCollection/presets.ts +10 -0
- package/templates/nova/src/sections/MainCollection/schema.ts +29 -0
- package/templates/nova/src/sections/Marquee/Marquee.tsx +114 -0
- package/templates/nova/src/sections/Marquee/index.ts +20 -0
- package/templates/nova/src/sections/Marquee/presets.ts +10 -0
- package/templates/nova/src/sections/Marquee/schema.ts +23 -0
- package/templates/nova/src/sections/Multicolumn/Multicolumn.tsx +56 -0
- package/templates/nova/src/sections/Multicolumn/index.ts +25 -0
- package/templates/nova/src/sections/Multicolumn/presets.ts +8 -0
- package/templates/nova/src/sections/Multicolumn/schema.ts +34 -0
- package/templates/nova/src/sections/Newsletter/Newsletter.tsx +64 -0
- package/templates/nova/src/sections/Newsletter/index.ts +20 -0
- package/templates/nova/src/sections/Newsletter/presets.ts +10 -0
- package/templates/nova/src/sections/Newsletter/schema.ts +36 -0
- package/templates/nova/src/sections/NotFound/NotFound.tsx +41 -0
- package/templates/nova/src/sections/NotFound/index.ts +20 -0
- package/templates/nova/src/sections/NotFound/presets.ts +6 -0
- package/templates/nova/src/sections/NotFound/schema.ts +30 -0
- package/templates/nova/src/sections/PageBody/PageBody.tsx +35 -0
- package/templates/nova/src/sections/PageBody/index.ts +21 -0
- package/templates/nova/src/sections/PageBody/presets.ts +6 -0
- package/templates/nova/src/sections/PageBody/schema.ts +29 -0
- package/templates/nova/src/sections/PolicyPage/PolicyPage.tsx +62 -0
- package/templates/nova/src/sections/PolicyPage/index.ts +23 -0
- package/templates/nova/src/sections/PolicyPage/presets.ts +6 -0
- package/templates/nova/src/sections/PolicyPage/schema.ts +27 -0
- package/templates/nova/src/sections/ProductDetails/ProductDetails.tsx +269 -0
- package/templates/nova/src/sections/ProductDetails/blocks/AddToCartBlock.tsx +15 -0
- package/templates/nova/src/sections/ProductDetails/blocks/ProductDescription.tsx +15 -0
- package/templates/nova/src/sections/ProductDetails/blocks/ProductInventory.tsx +38 -0
- package/templates/nova/src/sections/ProductDetails/blocks/ProductPrice.tsx +19 -0
- package/templates/nova/src/sections/ProductDetails/blocks/ProductSku.tsx +15 -0
- package/templates/nova/src/sections/ProductDetails/blocks/ProductTitle.tsx +17 -0
- package/templates/nova/src/sections/ProductDetails/blocks/Quantity.tsx +14 -0
- package/templates/nova/src/sections/ProductDetails/blocks/Tab.tsx +23 -0
- package/templates/nova/src/sections/ProductDetails/blocks/VariantPickerBlock.tsx +44 -0
- package/templates/nova/src/sections/ProductDetails/blocks/commerce-blocks.css +13 -0
- package/templates/nova/src/sections/ProductDetails/index.ts +44 -0
- package/templates/nova/src/sections/ProductDetails/presets.ts +10 -0
- package/templates/nova/src/sections/ProductDetails/schema.ts +44 -0
- package/templates/nova/src/sections/ProductGrid/ProductGrid.tsx +38 -0
- package/templates/nova/src/sections/ProductGrid/index.ts +25 -0
- package/templates/nova/src/sections/ProductGrid/presets.ts +6 -0
- package/templates/nova/src/sections/ProductGrid/schema.ts +37 -0
- package/templates/nova/src/sections/ProductRecommendations/ProductRecommendations.tsx +45 -0
- package/templates/nova/src/sections/ProductRecommendations/index.ts +20 -0
- package/templates/nova/src/sections/ProductRecommendations/presets.ts +8 -0
- package/templates/nova/src/sections/ProductRecommendations/schema.ts +28 -0
- package/templates/nova/src/sections/ProductReviews/ProductReviews.tsx +136 -0
- package/templates/nova/src/sections/ProductReviews/index.ts +22 -0
- package/templates/nova/src/sections/ProductReviews/presets.ts +6 -0
- package/templates/nova/src/sections/ProductReviews/schema.ts +31 -0
- package/templates/nova/src/sections/PromoBanner/PromoBanner.tsx +78 -0
- package/templates/nova/src/sections/PromoBanner/index.ts +20 -0
- package/templates/nova/src/sections/PromoBanner/presets.ts +10 -0
- package/templates/nova/src/sections/PromoBanner/schema.ts +38 -0
- package/templates/nova/src/sections/RecentlyViewed/RecentlyViewed.tsx +53 -0
- package/templates/nova/src/sections/RecentlyViewed/index.ts +20 -0
- package/templates/nova/src/sections/RecentlyViewed/presets.ts +6 -0
- package/templates/nova/src/sections/RecentlyViewed/schema.ts +26 -0
- package/templates/nova/src/sections/RichText/RichText.tsx +38 -0
- package/templates/nova/src/sections/RichText/index.ts +20 -0
- package/templates/nova/src/sections/RichText/presets.ts +10 -0
- package/templates/nova/src/sections/RichText/schema.ts +37 -0
- package/templates/nova/src/sections/SearchResults/SearchResults.tsx +112 -0
- package/templates/nova/src/sections/SearchResults/index.ts +26 -0
- package/templates/nova/src/sections/SearchResults/presets.ts +8 -0
- package/templates/nova/src/sections/SearchResults/schema.ts +30 -0
- package/templates/nova/src/sections/ShopTheLook/ShopTheLook.tsx +112 -0
- package/templates/nova/src/sections/ShopTheLook/blocks/Hotspot.tsx +22 -0
- package/templates/nova/src/sections/ShopTheLook/index.ts +33 -0
- package/templates/nova/src/sections/ShopTheLook/presets.ts +8 -0
- package/templates/nova/src/sections/ShopTheLook/schema.ts +31 -0
- package/templates/nova/src/sections/Slideshow/Slideshow.tsx +199 -0
- package/templates/nova/src/sections/Slideshow/blocks/Slide.tsx +93 -0
- package/templates/nova/src/sections/Slideshow/index.ts +30 -0
- package/templates/nova/src/sections/Slideshow/presets.ts +10 -0
- package/templates/nova/src/sections/Slideshow/schema.ts +36 -0
- package/templates/nova/src/sections/StoreLocator/StoreLocator.tsx +66 -0
- package/templates/nova/src/sections/StoreLocator/index.ts +20 -0
- package/templates/nova/src/sections/StoreLocator/presets.ts +6 -0
- package/templates/nova/src/sections/StoreLocator/schema.ts +34 -0
- package/templates/nova/src/sections/TeamFounders/TeamFounders.tsx +74 -0
- package/templates/nova/src/sections/TeamFounders/blocks/Person.tsx +28 -0
- package/templates/nova/src/sections/TeamFounders/index.ts +28 -0
- package/templates/nova/src/sections/TeamFounders/presets.ts +10 -0
- package/templates/nova/src/sections/TeamFounders/schema.ts +36 -0
- package/templates/nova/src/sections/Testimonials/Testimonials.tsx +83 -0
- package/templates/nova/src/sections/Testimonials/blocks/Testimonial.tsx +30 -0
- package/templates/nova/src/sections/Testimonials/index.ts +29 -0
- package/templates/nova/src/sections/Testimonials/presets.ts +10 -0
- package/templates/nova/src/sections/Testimonials/schema.ts +35 -0
- package/templates/nova/src/sections/Timeline/Timeline.tsx +43 -0
- package/templates/nova/src/sections/Timeline/blocks/Step.tsx +19 -0
- package/templates/nova/src/sections/Timeline/index.ts +28 -0
- package/templates/nova/src/sections/Timeline/presets.ts +8 -0
- package/templates/nova/src/sections/Timeline/schema.ts +31 -0
- package/templates/nova/src/sections/TrustBadges/TrustBadges.tsx +42 -0
- package/templates/nova/src/sections/TrustBadges/index.ts +29 -0
- package/templates/nova/src/sections/TrustBadges/presets.ts +8 -0
- package/templates/nova/src/sections/TrustBadges/schema.ts +29 -0
- package/templates/nova/src/sections/UgcGallery/UgcGallery.tsx +58 -0
- package/templates/nova/src/sections/UgcGallery/blocks/UgcImage.tsx +27 -0
- package/templates/nova/src/sections/UgcGallery/index.ts +31 -0
- package/templates/nova/src/sections/UgcGallery/presets.ts +8 -0
- package/templates/nova/src/sections/UgcGallery/schema.ts +35 -0
- package/templates/nova/src/sections/VideoHero/VideoHero.tsx +75 -0
- package/templates/nova/src/sections/VideoHero/index.ts +22 -0
- package/templates/nova/src/sections/VideoHero/presets.ts +8 -0
- package/templates/nova/src/sections/VideoHero/schema.ts +43 -0
- package/templates/nova/src/sections/data.ts +115 -0
- package/templates/nova/src/sections/shipped-copy.ts +25 -0
- package/templates/nova/src/sections/starter.ts +49 -0
- package/templates/nova/src/styles/Container.css +6 -0
- package/templates/nova/src/styles/Container.tsx +13 -0
- package/templates/nova/src/styles/Icon.css +2 -0
- package/templates/nova/src/styles/Icon.tsx +101 -0
- package/templates/nova/src/styles/NovaProvider.tsx +91 -0
- package/templates/nova/src/styles/base.css +142 -0
- package/templates/nova/src/styles/blocks.css +6 -0
- package/templates/nova/src/styles/fonts/OFL.txt +93 -0
- package/templates/nova/src/styles/fonts/geist-cyrillic-ext.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-cyrillic.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-latin-ext.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-latin.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-mono-cyrillic-ext.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-mono-cyrillic.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-mono-latin-ext.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-mono-latin.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-mono-vietnamese.woff2 +0 -0
- package/templates/nova/src/styles/fonts/geist-vietnamese.woff2 +0 -0
- package/templates/nova/src/styles/fonts.css +95 -0
- package/templates/nova/src/styles/global-settings.css +88 -0
- package/templates/nova/src/styles/index.css +6 -0
- package/templates/nova/src/styles/motion.ts +53 -0
- package/templates/nova/src/styles/scroll.ts +24 -0
- package/templates/nova/src/styles/sections/commerce.css +147 -0
- package/templates/nova/src/styles/sections/content.css +304 -0
- package/templates/nova/src/styles/sections/editorial.css +131 -0
- package/templates/nova/src/styles/sections/promotion.css +336 -0
- package/templates/nova/src/styles/sections/trust.css +117 -0
- package/templates/nova/src/styles/sections/utility.css +190 -0
- package/templates/nova/src/styles/strings.ts +492 -0
- package/templates/nova/src/styles/text.ts +4 -0
- package/templates/nova/src/styles/theme.ts +171 -0
- package/templates/nova/src/styles/tokens.css +600 -0
- package/templates/nova/src/styles/viewport.ts +23 -0
- package/templates/nova/template.json +94 -0
- package/templates/nova/templates/404.json +30 -0
- package/templates/nova/templates/account.json +15 -0
- package/templates/nova/templates/article.json +41 -0
- package/templates/nova/templates/article.longform.json +17 -0
- package/templates/nova/templates/blog.featured.json +22 -0
- package/templates/nova/templates/blog.json +29 -0
- package/templates/nova/templates/cart.json +67 -0
- package/templates/nova/templates/collection.featured.json +43 -0
- package/templates/nova/templates/collection.json +30 -0
- package/templates/nova/templates/contact.json +65 -0
- package/templates/nova/templates/index.json +167 -0
- package/templates/nova/templates/list-collections.json +19 -0
- package/templates/nova/templates/page.about.json +134 -0
- package/templates/nova/templates/page.contact.json +27 -0
- package/templates/nova/templates/page.json +27 -0
- package/templates/nova/templates/policy.json +23 -0
- package/templates/nova/templates/product.bundle.json +145 -0
- package/templates/nova/templates/product.json +119 -0
- package/templates/nova/templates/search.json +17 -0
- package/templates/nova/tests/abi-ai-contract.test.ts +99 -0
- package/templates/nova/tests/abi-forms.test.tsx +269 -0
- package/templates/nova/tests/abi-inventory.test.tsx +197 -0
- package/templates/nova/tests/abi-legacy-settings.test.tsx +105 -0
- package/templates/nova/tests/abi-opt-in.test.tsx +100 -0
- package/templates/nova/tests/abi-parity.test.tsx +95 -0
- package/templates/nova/tests/abi-r5-store-settings.test.tsx +337 -0
- package/templates/nova/tests/abi-render.test.tsx +178 -0
- package/templates/nova/tests/abi-shop-links.test.tsx +141 -0
- package/templates/nova/tests/abi-storefront-basics.test.tsx +233 -0
- package/templates/nova/tests/abi-variant-url.test.tsx +73 -0
- package/templates/nova/tests/add-refused-no-success.test.tsx +193 -0
- package/templates/nova/tests/architecture.test.ts +63 -0
- package/templates/nova/tests/architecture.ts +160 -0
- package/templates/nova/tests/audit-guards.test.ts +31 -0
- package/templates/nova/tests/bundle-split.test.ts +29 -0
- package/templates/nova/tests/cart-limit-message.test.tsx +212 -0
- package/templates/nova/tests/cart-link-message.test.tsx +98 -0
- package/templates/nova/tests/cases/components/Overview.cases.tsx +8 -0
- package/templates/nova/tests/cases/components/base/Accordion.cases.tsx +40 -0
- package/templates/nova/tests/cases/components/base/Badge.cases.tsx +30 -0
- package/templates/nova/tests/cases/components/base/Button.cases.tsx +73 -0
- package/templates/nova/tests/cases/components/base/Checkbox.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/base/Chip.cases.tsx +37 -0
- package/templates/nova/tests/cases/components/base/Divider.cases.tsx +26 -0
- package/templates/nova/tests/cases/components/base/IconButton.cases.tsx +28 -0
- package/templates/nova/tests/cases/components/base/Input.cases.tsx +28 -0
- package/templates/nova/tests/cases/components/base/Link.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/base/Radio.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/base/Select.cases.tsx +26 -0
- package/templates/nova/tests/cases/components/base/Skeleton.cases.tsx +29 -0
- package/templates/nova/tests/cases/components/base/Spinner.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/base/Switch.cases.tsx +26 -0
- package/templates/nova/tests/cases/components/base/Tabs.cases.tsx +39 -0
- package/templates/nova/tests/cases/components/base/Textarea.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/commerce/AddToCart.cases.tsx +36 -0
- package/templates/nova/tests/cases/components/commerce/CartLine.cases.tsx +20 -0
- package/templates/nova/tests/cases/components/commerce/CartSummary.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/commerce/CollectionCard.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/commerce/ComparisonTable.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/commerce/Hotspot.cases.tsx +25 -0
- package/templates/nova/tests/cases/components/commerce/LookItem.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/commerce/PaymentMark.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/commerce/Price.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/commerce/ProductCard.cases.tsx +67 -0
- package/templates/nova/tests/cases/components/commerce/ProductGallery.cases.tsx +39 -0
- package/templates/nova/tests/cases/components/commerce/QuantitySelector.cases.tsx +31 -0
- package/templates/nova/tests/cases/components/commerce/Reviews.cases.tsx +35 -0
- package/templates/nova/tests/cases/components/commerce/Stars.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/commerce/VariantPicker.cases.tsx +38 -0
- package/templates/nova/tests/cases/components/commerce/blocks/Badge.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/commerce/blocks/PaymentIcons.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/commerce/blocks/Swatches.cases.tsx +22 -0
- package/templates/nova/tests/cases/components/content/ArticleCard.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/content/BeforeAfterSlider.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/content/CardRow.cases.tsx +23 -0
- package/templates/nova/tests/cases/components/content/CarouselControls.cases.tsx +48 -0
- package/templates/nova/tests/cases/components/content/ContentsNav.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/content/Countdown.cases.tsx +28 -0
- package/templates/nova/tests/cases/components/content/HeroCopy.cases.tsx +42 -0
- package/templates/nova/tests/cases/components/content/HeroFrame.cases.tsx +22 -0
- package/templates/nova/tests/cases/components/content/HeroMedia.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/content/LongForm.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/content/MediaFrame.cases.tsx +32 -0
- package/templates/nova/tests/cases/components/content/RichText.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/content/SectionHead.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/content/ShareButtons.cases.tsx +24 -0
- package/templates/nova/tests/cases/components/content/StoreCard.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/content/TestimonialCard.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/content/UgcTile.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/content/blocks/Button.cases.tsx +20 -0
- package/templates/nova/tests/cases/components/content/blocks/Column.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/content/blocks/Heading.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/content/blocks/Icon.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/content/blocks/Image.cases.tsx +22 -0
- package/templates/nova/tests/cases/components/content/blocks/Text.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/content/blocks/Video.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/forms/ContactForm.cases.tsx +29 -0
- package/templates/nova/tests/cases/components/forms/FormField.cases.tsx +61 -0
- package/templates/nova/tests/cases/components/forms/NewsletterForm.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/forms/SearchField.cases.tsx +25 -0
- package/templates/nova/tests/cases/components/forms/blocks/Newsletter.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/navigation/BrandLogo.cases.tsx +29 -0
- package/templates/nova/tests/cases/components/navigation/Breadcrumb.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/navigation/LocaleSelector.cases.tsx +32 -0
- package/templates/nova/tests/cases/components/navigation/Menu.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/navigation/Pagination.cases.tsx +25 -0
- package/templates/nova/tests/cases/components/navigation/RecoveryLinks.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/navigation/SocialLinks.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/navigation/blocks/SocialLinks.cases.tsx +17 -0
- package/templates/nova/tests/cases/components/overlays/AccountMenu.cases.tsx +28 -0
- package/templates/nova/tests/cases/components/overlays/CartDrawer.cases.tsx +30 -0
- package/templates/nova/tests/cases/components/overlays/ConfirmationDialog.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/overlays/CookieConsent.cases.tsx +23 -0
- package/templates/nova/tests/cases/components/overlays/CookiePreferences.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/overlays/Drawer.cases.tsx +52 -0
- package/templates/nova/tests/cases/components/overlays/MarketSelector.cases.tsx +82 -0
- package/templates/nova/tests/cases/components/overlays/Menu.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/overlays/MobileNavDrawer.cases.tsx +32 -0
- package/templates/nova/tests/cases/components/overlays/Modal.cases.tsx +71 -0
- package/templates/nova/tests/cases/components/overlays/Popover.cases.tsx +50 -0
- package/templates/nova/tests/cases/components/overlays/PromoPopup.cases.tsx +19 -0
- package/templates/nova/tests/cases/components/overlays/QuickView.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/overlays/SearchModal.cases.tsx +60 -0
- package/templates/nova/tests/cases/components/overlays/Sheet.cases.tsx +32 -0
- package/templates/nova/tests/cases/components/overlays/SizeGuide.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/overlays/Toast.cases.tsx +48 -0
- package/templates/nova/tests/cases/components/overlays/Tooltip.cases.tsx +33 -0
- package/templates/nova/tests/cases/components/states/Alert.cases.tsx +27 -0
- package/templates/nova/tests/cases/components/states/EditorFrame.cases.tsx +36 -0
- package/templates/nova/tests/cases/components/states/Empty.cases.tsx +30 -0
- package/templates/nova/tests/cases/components/states/Error.cases.tsx +38 -0
- package/templates/nova/tests/cases/components/states/Loading.cases.tsx +15 -0
- package/templates/nova/tests/cases/components/states/MediaState.cases.tsx +29 -0
- package/templates/nova/tests/cases/components/states/SkeletonLayout.cases.tsx +20 -0
- package/templates/nova/tests/cases/components/states/Unavailable.cases.tsx +16 -0
- package/templates/nova/tests/cases/components/states/media/MediaError.cases.tsx +18 -0
- package/templates/nova/tests/cases/components/states/media/MediaPlaceholder.cases.tsx +87 -0
- package/templates/nova/tests/cases/sections/Overview.cases.tsx +8 -0
- package/templates/nova/tests/cases/sections/commerce/collection-list/CollectionList.cases.tsx +46 -0
- package/templates/nova/tests/cases/sections/commerce/collection-list/blocks/CollectionItem.cases.tsx +18 -0
- package/templates/nova/tests/cases/sections/commerce/featured-collection/FeaturedCollection.cases.tsx +46 -0
- package/templates/nova/tests/cases/sections/commerce/featured-product/FeaturedProduct.cases.tsx +40 -0
- package/templates/nova/tests/cases/sections/commerce/main-collection/MainCollection.cases.tsx +81 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/ProductDetails.cases.tsx +110 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/AddToCartBlock.cases.tsx +21 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductDescription.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductInventory.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductPrice.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductSku.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductTitle.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/Quantity.cases.tsx +20 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/Tab.cases.tsx +29 -0
- package/templates/nova/tests/cases/sections/commerce/product-details/blocks/VariantPickerBlock.cases.tsx +32 -0
- package/templates/nova/tests/cases/sections/commerce/product-grid/ProductGrid.cases.tsx +48 -0
- package/templates/nova/tests/cases/sections/commerce/product-recommendations/ProductRecommendations.cases.tsx +27 -0
- package/templates/nova/tests/cases/sections/commerce/recently-viewed/RecentlyViewed.cases.tsx +36 -0
- package/templates/nova/tests/cases/sections/content/article-body/ArticleBody.cases.tsx +32 -0
- package/templates/nova/tests/cases/sections/content/before-after/BeforeAfter.cases.tsx +40 -0
- package/templates/nova/tests/cases/sections/content/before-after/blocks/Comparison.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/content/blog-posts/BlogPosts.cases.tsx +47 -0
- package/templates/nova/tests/cases/sections/content/collection-links/CollectionLinks.cases.tsx +36 -0
- package/templates/nova/tests/cases/sections/content/faq/Faq.cases.tsx +38 -0
- package/templates/nova/tests/cases/sections/content/faq/blocks/FaqItem.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/content/feature-grid-blocks/FeatureGridBlocks.cases.tsx +31 -0
- package/templates/nova/tests/cases/sections/content/feature-highlights/FeatureHighlights.cases.tsx +26 -0
- package/templates/nova/tests/cases/sections/content/image-gallery/ImageGallery.cases.tsx +31 -0
- package/templates/nova/tests/cases/sections/content/image-with-text/ImageWithText.cases.tsx +35 -0
- package/templates/nova/tests/cases/sections/content/multicolumn/Multicolumn.cases.tsx +32 -0
- package/templates/nova/tests/cases/sections/content/page-body/PageBody.cases.tsx +31 -0
- package/templates/nova/tests/cases/sections/content/rich-text/RichTextSection.cases.tsx +33 -0
- package/templates/nova/tests/cases/sections/content/team-founders/TeamFounders.cases.tsx +31 -0
- package/templates/nova/tests/cases/sections/content/team-founders/blocks/Person.cases.tsx +18 -0
- package/templates/nova/tests/cases/sections/content/timeline/Timeline.cases.tsx +27 -0
- package/templates/nova/tests/cases/sections/content/timeline/blocks/Step.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/layout/announcement-bar/AnnouncementBar.cases.tsx +119 -0
- package/templates/nova/tests/cases/sections/layout/breadcrumbs/Breadcrumbs.cases.tsx +73 -0
- package/templates/nova/tests/cases/sections/layout/divider/Divider.cases.tsx +81 -0
- package/templates/nova/tests/cases/sections/layout/footer/Footer.cases.tsx +116 -0
- package/templates/nova/tests/cases/sections/layout/footer/blocks/FooterBrand.cases.tsx +27 -0
- package/templates/nova/tests/cases/sections/layout/footer/blocks/FooterMenu.cases.tsx +22 -0
- package/templates/nova/tests/cases/sections/layout/footer/blocks/FooterText.cases.tsx +20 -0
- package/templates/nova/tests/cases/sections/layout/group/Group.cases.tsx +185 -0
- package/templates/nova/tests/cases/sections/layout/group/blocks/Accordion.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/layout/group/blocks/JumboText.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/layout/group/blocks/Spacer.cases.tsx +33 -0
- package/templates/nova/tests/cases/sections/layout/header/Header.cases.tsx +277 -0
- package/templates/nova/tests/cases/sections/marketing/collection-banner/CollectionBanner.cases.tsx +62 -0
- package/templates/nova/tests/cases/sections/marketing/hero/Hero.cases.tsx +138 -0
- package/templates/nova/tests/cases/sections/marketing/marquee/Marquee.cases.tsx +63 -0
- package/templates/nova/tests/cases/sections/marketing/promo-banner/PromoBanner.cases.tsx +87 -0
- package/templates/nova/tests/cases/sections/marketing/slideshow/Slideshow.cases.tsx +113 -0
- package/templates/nova/tests/cases/sections/marketing/slideshow/blocks/Slide.cases.tsx +26 -0
- package/templates/nova/tests/cases/sections/marketing/video-hero/VideoHero.cases.tsx +72 -0
- package/templates/nova/tests/cases/sections/trust/comparison-table/ComparisonTableSection.cases.tsx +42 -0
- package/templates/nova/tests/cases/sections/trust/comparison-table/blocks/CompareItem.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/trust/logo-list/LogoList.cases.tsx +29 -0
- package/templates/nova/tests/cases/sections/trust/logo-list/blocks/Logo.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/trust/product-reviews/ProductReviews.cases.tsx +51 -0
- package/templates/nova/tests/cases/sections/trust/shop-the-look/ShopTheLook.cases.tsx +51 -0
- package/templates/nova/tests/cases/sections/trust/shop-the-look/blocks/Hotspot.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/trust/testimonials/Testimonials.cases.tsx +61 -0
- package/templates/nova/tests/cases/sections/trust/testimonials/blocks/Testimonial.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/trust/trust-badges/TrustBadges.cases.tsx +30 -0
- package/templates/nova/tests/cases/sections/trust/ugc-gallery/UgcGallery.cases.tsx +32 -0
- package/templates/nova/tests/cases/sections/trust/ugc-gallery/blocks/UgcImage.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/utility/account/Account.cases.tsx +44 -0
- package/templates/nova/tests/cases/sections/utility/cart-items/CartItems.cases.tsx +43 -0
- package/templates/nova/tests/cases/sections/utility/cart-items/blocks/CartBadge.cases.tsx +16 -0
- package/templates/nova/tests/cases/sections/utility/contact-form/ContactFormSection.cases.tsx +52 -0
- package/templates/nova/tests/cases/sections/utility/contact-form/blocks/ContactDetail.cases.tsx +17 -0
- package/templates/nova/tests/cases/sections/utility/newsletter/NewsletterSection.cases.tsx +109 -0
- package/templates/nova/tests/cases/sections/utility/not-found/NotFound.cases.tsx +28 -0
- package/templates/nova/tests/cases/sections/utility/policy-page/PolicyPage.cases.tsx +29 -0
- package/templates/nova/tests/cases/sections/utility/search-results/SearchResultsSection.cases.tsx +39 -0
- package/templates/nova/tests/cases/sections/utility/store-locator/StoreLocator.cases.tsx +40 -0
- package/templates/nova/tests/cases/styles/Borders.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/Breakpoints.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/Colors.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/Elevation.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/Icon.cases.tsx +23 -0
- package/templates/nova/tests/cases/styles/Layout.cases.tsx +8 -0
- package/templates/nova/tests/cases/styles/Motion.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/Overview.cases.tsx +8 -0
- package/templates/nova/tests/cases/styles/Radius.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/SharedThemeSettings.cases.tsx +19 -0
- package/templates/nova/tests/cases/styles/Spacing.cases.tsx +10 -0
- package/templates/nova/tests/cases/styles/Typography.cases.tsx +10 -0
- package/templates/nova/tests/cases.test.tsx +38 -0
- package/templates/nova/tests/components/PromoPopup.test.ts +21 -0
- package/templates/nova/tests/components/RichText.test.tsx +65 -0
- package/templates/nova/tests/components/formatDate.test.ts +10 -0
- package/templates/nova/tests/contract-locks.test.ts +23 -0
- package/templates/nova/tests/contracts.test.ts +153 -0
- package/templates/nova/tests/cookie-consent-privacy.test.tsx +128 -0
- package/templates/nova/tests/empty-store.test.tsx +81 -0
- package/templates/nova/tests/fixtures/commerce.ts +128 -0
- package/templates/nova/tests/fixtures/content.ts +58 -0
- package/templates/nova/tests/fixtures/index.ts +8 -0
- package/templates/nova/tests/fixtures/merch.ts +161 -0
- package/templates/nova/tests/fixtures/navigation.ts +125 -0
- package/templates/nova/tests/fixtures/promotion.ts +43 -0
- package/templates/nova/tests/fixtures/sample-store.json +1674 -0
- package/templates/nova/tests/fixtures/shop.ts +88 -0
- package/templates/nova/tests/fixtures/stress.ts +21 -0
- package/templates/nova/tests/inert.test.tsx +285 -0
- package/templates/nova/tests/line-add-friend.test.tsx +108 -0
- package/templates/nova/tests/localized-copy.test.ts +330 -0
- package/templates/nova/tests/product-template-blocks.test.ts +28 -0
- package/templates/nova/tests/rewards-launcher.test.tsx +401 -0
- package/templates/nova/tests/schema.test.ts +65 -0
- package/templates/nova/tests/schema.typetest.ts +7 -0
- package/templates/nova/tests/section-groups.test.ts +189 -0
- package/templates/nova/tests/sections/Group.test.tsx +37 -0
- package/templates/nova/tests/setup.ts +30 -0
- package/templates/nova/tests/starter.test.ts +56 -0
- package/templates/nova/tests/storefront-click-path.test.tsx +157 -0
- package/templates/nova/tests/styles/button-brand.test.ts +24 -0
- package/templates/nova/tests/styles/theme.test.ts +43 -0
- package/templates/nova/tests/styles/tokens.test.ts +35 -0
- package/templates/nova/tests/support/abi-ctx.ts +136 -0
- package/templates/nova/tests/support/block-stories.tsx +13 -0
- package/templates/nova/tests/support/case.tsx +102 -0
- package/templates/nova/tests/support/contracts.ts +30 -0
- package/templates/nova/tests/support/foundation-pages.tsx +260 -0
- package/templates/nova/tests/support/ia-pages.tsx +154 -0
- package/templates/nova/tests/support/listing-demo.tsx +29 -0
- package/templates/nova/tests/support/presets.ts +14 -0
- package/templates/nova/tests/support/sections.ts +36 -0
- package/templates/nova/tests/support/story-content.ts +321 -0
- package/templates/nova/tests/support/story-helpers.tsx +37 -0
- package/templates/nova/tests/support/with-nova.tsx +26 -0
- package/templates/nova/tests/theme-config.test.ts +82 -0
- package/templates/nova/tests/theme-integrity.test.ts +315 -0
- package/templates/nova/theme.config.ts +134 -0
- package/templates/nova/theme.manifest.json +17495 -0
- package/templates/nova/tsconfig.json +19 -0
- package/templates/nova/vite.abi-ssr.config.ts +37 -0
- package/templates/nova/vite.abi.config.ts +26 -0
- package/templates/nova/vitest.config.ts +20 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A Tanqory storefront theme forked from Nova V3 (Theme ABI v2) on @tanqory/theme-kit.",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.6.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "node scripts/dev.mjs",
|
|
12
|
+
"build": "node scripts/abi/build.mjs",
|
|
13
|
+
"check": "node scripts/check.mjs",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"manifest": "node --experimental-strip-types scripts/compiler/manifest/gen-manifest.mjs",
|
|
17
|
+
"build:abi": "node scripts/abi/build.mjs",
|
|
18
|
+
"verify": "npm run build:abi && npm run typecheck && npm run test",
|
|
19
|
+
"host": "node scripts/abi/local-host.mjs"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@tanqory/theme-kit": "2.0.0-alpha.3",
|
|
23
|
+
"react": "18.3.1",
|
|
24
|
+
"react-dom": "18.3.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tanqory/cli": "2.0.0-alpha.3",
|
|
28
|
+
"@tanqory/theme-abi": "2.0.0-alpha.14",
|
|
29
|
+
"@tanqory/theme-conformance": "0.1.0-alpha.15",
|
|
30
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
31
|
+
"@testing-library/react": "^16.3.0",
|
|
32
|
+
"@testing-library/user-event": "^14.6.1",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"@types/react": "^18.3.12",
|
|
35
|
+
"@types/react-dom": "^18.3.1",
|
|
36
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
37
|
+
"axe-core": "^4.13.0",
|
|
38
|
+
"jsdom": "^25.0.1",
|
|
39
|
+
"playwright-core": "^1.63.0",
|
|
40
|
+
"typescript": "^5.6.3",
|
|
41
|
+
"vite": "^5.4.10",
|
|
42
|
+
"vitest": "^2.1.9"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Nova as a Theme ABI v2 theme directory — the layout @tanqory/theme-conformance runs and
|
|
3
|
+
* the platform build-service packages into an artifact (theme-artifact-v1.md §1):
|
|
4
|
+
*
|
|
5
|
+
* dist/abi/manifest.json ThemeManifest v2, JCS + "\n"
|
|
6
|
+
* dist/abi/server/entry.mjs render(request, ctx) + manifest — one file, zero imports
|
|
7
|
+
* dist/abi/client/client-manifest.json neutral asset manifest, sha256 digest + SRI per file
|
|
8
|
+
* dist/abi/client/assets/* content-hashed browser files
|
|
9
|
+
* dist/abi/defaults/{settings.json, regions/*.json, templates/*.json}
|
|
10
|
+
* dist/abi/locales/<lang>.json
|
|
11
|
+
*
|
|
12
|
+
* `artifact.json`, the SBOM and the signed statement are the build-service's and signer's (they
|
|
13
|
+
* bind builder identity and inputs this script cannot attest), so they are not written here.
|
|
14
|
+
*
|
|
15
|
+
* node scripts/abi/build.mjs build + validate; exit 1 on any contract problem
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process'
|
|
18
|
+
import { createRequire } from 'node:module'
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { dirname, join, relative } from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
import { gzipSync } from 'node:zlib'
|
|
24
|
+
import { LIMITS } from '@tanqory/theme-abi/limits'
|
|
25
|
+
import { PLATFORM_ROUTES } from '@tanqory/theme-abi'
|
|
26
|
+
import { buildManifest, STARTER_ASSET_DIR } from './manifest.mjs'
|
|
27
|
+
import { canonicalize } from './jcs.mjs'
|
|
28
|
+
|
|
29
|
+
const theme = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
30
|
+
const out = join(theme, 'dist', 'abi')
|
|
31
|
+
// Vite as the theme resolves it (its own node_modules, a workspace's hoisted one, or a standalone
|
|
32
|
+
// checkout), run from that package's own bin — never a path that assumes the monorepo layout.
|
|
33
|
+
const vitePkgJson = createRequire(join(theme, 'package.json')).resolve('vite/package.json')
|
|
34
|
+
const vite = join(dirname(vitePkgJson), JSON.parse(readFileSync(vitePkgJson, 'utf8')).bin.vite)
|
|
35
|
+
const fail = (msg, list = []) => {
|
|
36
|
+
console.error(`✗ ${msg}`)
|
|
37
|
+
for (const l of list) console.error(` - ${l}`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
const writeJcs = (path, value) => {
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
42
|
+
writeFileSync(path, `${canonicalize(value)}\n`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The kit's wire client (@tanqory/theme-kit, abi/wire.ts) hard-codes the two browser prefixes; they must be the contract's.
|
|
46
|
+
if (PLATFORM_ROUTES.data !== '/_tq/data/' || PLATFORM_ROUTES.actions !== '/_tq/actions/') fail('PLATFORM_ROUTES changed — update @tanqory/theme-kit abi/wire.ts')
|
|
47
|
+
|
|
48
|
+
// 1. the v1 catalogue is generated from the defineSection schemas — the one source of truth
|
|
49
|
+
execFileSync(process.execPath, [join(theme, 'scripts', 'compiler', 'manifest', 'gen-manifest.mjs')], { cwd: theme, stdio: ['ignore', 'ignore', 'inherit'] })
|
|
50
|
+
|
|
51
|
+
// 2. manifest v2 + defaults, every stored value through validateFieldValue
|
|
52
|
+
const { manifest, defaults, problems, starterFiles } = buildManifest(theme)
|
|
53
|
+
if (problems.length) fail(`${problems.length} contract problem(s) in the theme's schemas or starter content`, problems)
|
|
54
|
+
writeJcs(join(theme, 'lib', 'abi', 'generated', 'manifest.json'), manifest)
|
|
55
|
+
|
|
56
|
+
// 3. browser bundle, then the serve entry (which imports the generated manifest)
|
|
57
|
+
rmSync(out, { recursive: true, force: true })
|
|
58
|
+
execFileSync(process.execPath, [vite, 'build', '--config', 'vite.abi.config.ts', '--logLevel', 'warn'], { cwd: theme, stdio: 'inherit' })
|
|
59
|
+
execFileSync(process.execPath, [vite, 'build', '--config', 'vite.abi-ssr.config.ts', '--logLevel', 'warn'], { cwd: theme, stdio: 'inherit' })
|
|
60
|
+
|
|
61
|
+
// 4. server/entry.mjs
|
|
62
|
+
mkdirSync(join(out, 'server'), { recursive: true })
|
|
63
|
+
cpSync(join(theme, 'dist', 'abi-ssr', 'entry.mjs'), join(out, 'server', 'entry.mjs'))
|
|
64
|
+
writeJcs(join(out, 'manifest.json'), manifest)
|
|
65
|
+
|
|
66
|
+
// 5. client manifest: every emitted file, digest + SRI; one isEntry script with its css + imports
|
|
67
|
+
const clientDir = join(out, 'client')
|
|
68
|
+
// Starter images the defaults/presets name as `asset:client/assets/<file>` (ABI Appendix B.2): the
|
|
69
|
+
// files must be in the artifact (the signer refuses an `asset:` with no matching file).
|
|
70
|
+
for (const f of starterFiles) cpSync(join(theme, STARTER_ASSET_DIR, f), join(clientDir, 'assets', f))
|
|
71
|
+
const viteManifestPath = join(clientDir, '.vite', 'manifest.json')
|
|
72
|
+
const viteManifest = JSON.parse(readFileSync(viteManifestPath, 'utf8'))
|
|
73
|
+
rmSync(join(clientDir, '.vite'), { recursive: true, force: true })
|
|
74
|
+
const files = []
|
|
75
|
+
const walk = (dir) => {
|
|
76
|
+
for (const name of readdirSync(dir)) {
|
|
77
|
+
const p = join(dir, name)
|
|
78
|
+
if (statSync(p).isDirectory()) walk(p)
|
|
79
|
+
else files.push(relative(clientDir, p).split('\\').join('/'))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
walk(clientDir)
|
|
83
|
+
const kindOf = (f) => (/\.m?js$/.test(f) ? 'script' : /\.css$/.test(f) ? 'style' : /\.(woff2?|ttf|otf)$/.test(f) ? 'font' : /\.(png|jpe?g|gif|svg|webp|avif|ico)$/.test(f) ? 'image' : 'other')
|
|
84
|
+
const entryKey = Object.keys(viteManifest).find((k) => viteManifest[k].isEntry)
|
|
85
|
+
if (!entryKey) fail('vite produced no entry chunk')
|
|
86
|
+
const importsOf = (key, seen = new Set()) => {
|
|
87
|
+
for (const imp of viteManifest[key]?.imports ?? []) {
|
|
88
|
+
if (seen.has(imp)) continue
|
|
89
|
+
seen.add(imp)
|
|
90
|
+
importsOf(imp, seen)
|
|
91
|
+
}
|
|
92
|
+
return seen
|
|
93
|
+
}
|
|
94
|
+
const entryImports = [...importsOf(entryKey)].map((k) => viteManifest[k].file)
|
|
95
|
+
const entryCss = [...new Set([entryKey, ...importsOf(entryKey)].flatMap((k) => viteManifest[k].css ?? []))]
|
|
96
|
+
const clientManifest = {}
|
|
97
|
+
for (const f of files.sort()) {
|
|
98
|
+
const bytes = readFileSync(join(clientDir, f))
|
|
99
|
+
const sha = createHash('sha256').update(bytes).digest()
|
|
100
|
+
const isEntry = f === viteManifest[entryKey].file
|
|
101
|
+
clientManifest[isEntry ? 'main' : f] = {
|
|
102
|
+
file: f,
|
|
103
|
+
kind: kindOf(f),
|
|
104
|
+
...(isEntry ? { isEntry: true, css: entryCss, imports: entryImports } : {}),
|
|
105
|
+
size: bytes.length,
|
|
106
|
+
digest: `sha256:${sha.toString('hex')}`,
|
|
107
|
+
integrity: `sha256-${sha.toString('base64')}`,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
writeJcs(join(clientDir, 'client-manifest.json'), clientManifest)
|
|
111
|
+
|
|
112
|
+
// 6. defaults/ + locales/
|
|
113
|
+
writeJcs(join(out, 'defaults', 'settings.json'), defaults.settings)
|
|
114
|
+
for (const [name, doc] of Object.entries(defaults.regions)) writeJcs(join(out, 'defaults', 'regions', `${name}.json`), doc)
|
|
115
|
+
for (const [name, doc] of Object.entries(defaults.templates)) writeJcs(join(out, 'defaults', 'templates', `${name}.json`), doc)
|
|
116
|
+
for (const f of readdirSync(join(theme, 'locales')).filter((n) => n.endsWith('.json'))) {
|
|
117
|
+
const map = JSON.parse(readFileSync(join(theme, 'locales', f), 'utf8'))
|
|
118
|
+
const nonString = Object.entries(map).filter(([, v]) => typeof v !== 'string').map(([k]) => k)
|
|
119
|
+
if (nonString.length) fail(`locales/${f} is not a flat string map`, nonString)
|
|
120
|
+
writeJcs(join(out, 'locales', f), map)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 7. sizes against the limits table (§8): budget warns, hard fails
|
|
124
|
+
const entry = readFileSync(join(out, 'server', 'entry.mjs'))
|
|
125
|
+
const gz = gzipSync(entry, { level: 9 }).length
|
|
126
|
+
const manifestBytes = readFileSync(join(out, 'manifest.json')).length
|
|
127
|
+
const initial = [viteManifest[entryKey].file, ...entryImports, ...entryCss].reduce((n, f) => n + gzipSync(readFileSync(join(clientDir, f)), { level: 9 }).length, 0)
|
|
128
|
+
const totalRaw = files.reduce((n, f) => n + statSync(join(clientDir, f)).size, 0)
|
|
129
|
+
const rows = [
|
|
130
|
+
['ssrEntryRawBytes', entry.length],
|
|
131
|
+
['ssrEntryGzipBytes', gz],
|
|
132
|
+
['manifestBytes', manifestBytes],
|
|
133
|
+
['clientInitialGzipBytes', initial],
|
|
134
|
+
['clientTotalRawBytes', totalRaw],
|
|
135
|
+
]
|
|
136
|
+
let over = false
|
|
137
|
+
for (const [k, v] of rows) {
|
|
138
|
+
const { budget, hard } = LIMITS[k]
|
|
139
|
+
const verdict = v > hard ? 'OVER HARD' : budget !== null && v > budget ? 'over budget' : 'ok'
|
|
140
|
+
if (v > hard) over = true
|
|
141
|
+
console.log(`${k.padEnd(24)} ${String(v).padStart(10)} budget ${String(budget).padStart(10)} hard ${String(hard).padStart(10)} ${verdict}`)
|
|
142
|
+
}
|
|
143
|
+
if (over) fail('a hard size limit is exceeded')
|
|
144
|
+
if (!existsSync(join(out, 'server', 'entry.mjs'))) fail('no server entry')
|
|
145
|
+
console.log(`✓ ${relative(process.cwd(), out) || out}`)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Will a store's existing content carry over to Nova v3 unchanged?
|
|
3
|
+
*
|
|
4
|
+
* node scripts/abi/check-content.mjs <content.json | defaults-dir> [--manifest dist/abi/manifest.json]
|
|
5
|
+
*
|
|
6
|
+
* Input: a ContentRevision ({ settings: { values }, regions, templates }) — e.g. a store's live revision —
|
|
7
|
+
* or an ABI `defaults/` directory (another theme's signed starter content, read as if a store had saved
|
|
8
|
+
* all of it). Every setting key and value is checked against Nova v3's ABI manifest with the rules
|
|
9
|
+
* storefront-control's publish validator applies (content-document.ts checkFieldValue: declared key,
|
|
10
|
+
* value type, option membership, colour / URL / media rules, '' per type). Content carries over 1:1 —
|
|
11
|
+
* no key or value is rewritten — so "nothing the shop set is lost" means: zero issues here.
|
|
12
|
+
*
|
|
13
|
+
* Prints a summary and exits 1 when any value would be refused.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
16
|
+
import { join, dirname } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const theme = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
20
|
+
const arg = (n, d) => { const i = process.argv.indexOf(n); return i > 0 ? process.argv[i + 1] : d }
|
|
21
|
+
const input = process.argv[2]
|
|
22
|
+
if (!input) { console.error('usage: check-content.mjs <content.json | defaults-dir>'); process.exit(2) }
|
|
23
|
+
const manifest = JSON.parse(readFileSync(arg('--manifest', join(theme, 'dist', 'abi', 'manifest.json')), 'utf8'))
|
|
24
|
+
const readJson = (p) => JSON.parse(readFileSync(p, 'utf8'))
|
|
25
|
+
|
|
26
|
+
function contentOf(path) {
|
|
27
|
+
if (!statSync(path).isDirectory()) return readJson(path)
|
|
28
|
+
const out = { settings: { values: readJson(join(path, 'settings.json')).values ?? {} }, regions: {}, templates: {} }
|
|
29
|
+
for (const f of readdirSync(join(path, 'regions'))) out.regions[f.replace(/\.json$/, '')] = { sections: readJson(join(path, 'regions', f)).sections }
|
|
30
|
+
for (const f of readdirSync(join(path, 'templates'))) out.templates[f.replace(/\.json$/, '')] = { regions: readJson(join(path, 'templates', f)).regions }
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const COLOR = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
|
|
35
|
+
const urlOk = (raw, kind) => {
|
|
36
|
+
const v = raw.trim()
|
|
37
|
+
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(v)?.[1]?.toLowerCase()
|
|
38
|
+
if (kind === 'media') return scheme === 'https' || v.startsWith('asset:client/assets/')
|
|
39
|
+
if (scheme) return ['https', 'http', 'mailto', 'tel'].includes(scheme)
|
|
40
|
+
return v === '/' || (v.length >= 2 && v[0] === '/' && v[1] !== '/' && v[1] !== '\\')
|
|
41
|
+
}
|
|
42
|
+
/** storefront-control content-document.ts checkFieldValue, as a predicate (null = cleared = ok). */
|
|
43
|
+
function valueIssue(spec, v) {
|
|
44
|
+
if (v === null) return null
|
|
45
|
+
if (spec.dynamic === true && v && typeof v === 'object') return null
|
|
46
|
+
const s = typeof v === 'string'
|
|
47
|
+
switch (spec.type) {
|
|
48
|
+
case 'text': case 'textarea': case 'richtext': case 'html': case 'collection': case 'product': case 'page': case 'blog': case 'article': case 'menu':
|
|
49
|
+
return s ? null : 'must be text'
|
|
50
|
+
case 'number': case 'range':
|
|
51
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) return 'must be a number'
|
|
52
|
+
if (typeof spec.min === 'number' && v < spec.min) return `below ${spec.min}`
|
|
53
|
+
if (typeof spec.max === 'number' && v > spec.max) return `above ${spec.max}`
|
|
54
|
+
return null
|
|
55
|
+
case 'boolean': return typeof v === 'boolean' ? null : 'must be true/false'
|
|
56
|
+
case 'color': return s && COLOR.test(v) ? null : `"${v}" is not a colour`
|
|
57
|
+
case 'select': case 'radio': return (spec.options ?? []).some((o) => o.value === v) ? null : `"${v}" is not an option`
|
|
58
|
+
case 'text_alignment': return ['left', 'center', 'right', 'start', 'end'].includes(v) ? null : `"${v}" is not an alignment`
|
|
59
|
+
case 'url': return s && urlOk(v, 'url') ? null : `"${v}" is not a link`
|
|
60
|
+
case 'image': case 'video': return s && urlOk(v, 'media') ? null : `"${v}" is not https media`
|
|
61
|
+
default: return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const settingSpecs = new Map(manifest.settingsSchema.flatMap((g) => g.items.map((i) => [i.key, i])))
|
|
66
|
+
const issues = []
|
|
67
|
+
const stats = { settings: 0, nodes: 0, values: 0 }
|
|
68
|
+
const content = contentOf(input)
|
|
69
|
+
for (const [k, v] of Object.entries(content.settings?.values ?? {})) {
|
|
70
|
+
stats.settings++
|
|
71
|
+
const spec = settingSpecs.get(k)
|
|
72
|
+
if (!spec) issues.push(`settings.${k}: not a theme setting of Nova v3`)
|
|
73
|
+
else { const why = valueIssue(spec, v); if (why) issues.push(`settings.${k}: ${why}`) }
|
|
74
|
+
}
|
|
75
|
+
function nodes(list, at, region) {
|
|
76
|
+
for (const [i, n] of (list ?? []).entries()) {
|
|
77
|
+
stats.nodes++
|
|
78
|
+
const c = manifest.components[n.type]
|
|
79
|
+
const p = `${at}[${i}] ${n.type}#${n.id}`
|
|
80
|
+
if (!c) { issues.push(`${p}: no component "${n.type}" in Nova v3`); continue }
|
|
81
|
+
if (region && c.regions && !c.regions.includes(region) && !c.parents) issues.push(`${p}: not accepted in region ${region}`)
|
|
82
|
+
for (const [k, v] of Object.entries(n.settings ?? {})) {
|
|
83
|
+
stats.values++
|
|
84
|
+
const spec = c.attributes?.[k]
|
|
85
|
+
if (!spec) issues.push(`${p}.${k}: not a setting of "${n.type}" in Nova v3`)
|
|
86
|
+
else { const why = valueIssue(spec, v); if (why) issues.push(`${p}.${k}: ${why}`) }
|
|
87
|
+
}
|
|
88
|
+
nodes(n.blocks, `${p}.blocks`, null)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const [r, doc] of Object.entries(content.regions ?? {})) nodes(doc.sections, `regions.${r}`, r)
|
|
92
|
+
for (const [t, doc] of Object.entries(content.templates ?? {})) {
|
|
93
|
+
if (!manifest.templates[t]) { issues.push(`templates.${t}: not a template of Nova v3`); continue }
|
|
94
|
+
for (const [r, rd] of Object.entries(doc.regions ?? {})) nodes(rd.sections, `templates.${t}.${r}`, r)
|
|
95
|
+
}
|
|
96
|
+
console.log(`checked ${stats.settings} global settings, ${stats.nodes} nodes, ${stats.values} node values against Nova v3 (${manifest.name}@${manifest.version})`)
|
|
97
|
+
if (issues.length) { console.log(`✗ ${issues.length} value(s) Nova v3 would refuse:`); for (const i of issues) console.log(` - ${i}`); process.exit(1) }
|
|
98
|
+
console.log('✓ every key and value carries over unchanged')
|
|
99
|
+
void existsSync
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 8785 JSON Canonicalization Scheme for the values a manifest and its defaults can hold
|
|
3
|
+
* (null, boolean, string, finite number, array, plain object). theme-artifact-v1.md §1 requires
|
|
4
|
+
* manifest.json to be JCS + "\n"; the runner compares the `manifest` export the same way (S3).
|
|
5
|
+
*
|
|
6
|
+
* Keys are sorted by UTF-16 code units (JCS §3.2.3), which is what Array.prototype.sort does on
|
|
7
|
+
* strings. Numbers use ECMAScript Number::toString, which is what JCS §3.2.2.3 prescribes.
|
|
8
|
+
*/
|
|
9
|
+
export function canonicalize(value) {
|
|
10
|
+
if (value === null) return 'null'
|
|
11
|
+
switch (typeof value) {
|
|
12
|
+
case 'boolean':
|
|
13
|
+
return value ? 'true' : 'false'
|
|
14
|
+
case 'number':
|
|
15
|
+
if (!Number.isFinite(value)) throw new TypeError(`JCS: non-finite number ${value}`)
|
|
16
|
+
return JSON.stringify(value)
|
|
17
|
+
case 'string':
|
|
18
|
+
return JSON.stringify(value)
|
|
19
|
+
case 'object': {
|
|
20
|
+
if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`
|
|
21
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort()
|
|
22
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(',')}}`
|
|
23
|
+
}
|
|
24
|
+
default:
|
|
25
|
+
throw new TypeError(`JCS: unsupported ${typeof value}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A laptop host for the built ABI theme (dist/abi): renders pages through the real
|
|
3
|
+
* `server/entry.mjs` with a RenderContext built the way the platform builds it, serves the client
|
|
4
|
+
* bundle, answers the browser wire (`/_tq/data/*`, `/_tq/actions/*`) and sends the platform's CSP
|
|
5
|
+
* (`style-src-attr 'none'`, hashed `style-src-elem`). For looking at the theme in a browser and for
|
|
6
|
+
* the parity screenshots — not a conformance runner (scripts/abi/conformance.mjs is).
|
|
7
|
+
*
|
|
8
|
+
* node scripts/abi/local-host.mjs --data <dir> [--origin https://<store host>] [--content <file.json>] [--cart <lines.json>] [--port 4401] [--preview]
|
|
9
|
+
*
|
|
10
|
+
* --data a directory of recorded wire answers (`<op>-<base64url args>.json`, envelope form)
|
|
11
|
+
* --origin a live storefront to fill a missing answer from (its public /_tq/data wire, paced)
|
|
12
|
+
* --content a ContentRevision ({ settings: { values }, regions, templates }); default: the theme's own defaults
|
|
13
|
+
* --preview render with ctx.mode = 'preview' (the editor canvas)
|
|
14
|
+
* --cart a JSON file of ABI cart lines the in-memory cart starts with (`{ id, quantity, merchandise, attributes }`)
|
|
15
|
+
*/
|
|
16
|
+
import { createServer } from 'node:http'
|
|
17
|
+
import { createHash } from 'node:crypto'
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { extname, join, dirname, normalize } from 'node:path'
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const theme = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
23
|
+
const abi = join(theme, 'dist', 'abi')
|
|
24
|
+
const arg = (name, dflt = null) => {
|
|
25
|
+
const i = process.argv.indexOf(`--${name}`)
|
|
26
|
+
return i > 0 ? process.argv[i + 1] : dflt
|
|
27
|
+
}
|
|
28
|
+
const DATA = arg('data')
|
|
29
|
+
const ORIGIN = arg('origin')
|
|
30
|
+
const PORT = Number(arg('port', '4401'))
|
|
31
|
+
const PREVIEW = process.argv.includes('--preview')
|
|
32
|
+
if (!DATA) throw new Error('--data <dir> is required')
|
|
33
|
+
mkdirSync(DATA, { recursive: true })
|
|
34
|
+
|
|
35
|
+
const entry = await import(pathToFileURL(join(abi, 'server', 'entry.mjs')).href)
|
|
36
|
+
const manifest = JSON.parse(readFileSync(join(abi, 'manifest.json'), 'utf8'))
|
|
37
|
+
const clientManifest = JSON.parse(readFileSync(join(abi, 'client', 'client-manifest.json'), 'utf8'))
|
|
38
|
+
const readDefaults = (rel) => JSON.parse(readFileSync(join(abi, 'defaults', rel), 'utf8'))
|
|
39
|
+
const content = arg('content') ? JSON.parse(readFileSync(arg('content'), 'utf8')) : {}
|
|
40
|
+
|
|
41
|
+
// ── the wire: recorded answers, else (with --origin) the live store's, paced and recorded ─────────
|
|
42
|
+
const canon = (v) => (v === null || typeof v !== 'object' ? JSON.stringify(v) : Array.isArray(v) ? `[${v.map(canon).join(',')}]` : `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`)
|
|
43
|
+
const b64 = (s) => Buffer.from(s).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
44
|
+
const fileOf = (op, args) => join(DATA, `${op}${args.length ? '-' + b64(canon(args)) : ''}.json`.replace(/[^A-Za-z0-9._-]/g, '_'))
|
|
45
|
+
let live = null
|
|
46
|
+
async function liveSession() {
|
|
47
|
+
const r = await fetch(ORIGIN + '/')
|
|
48
|
+
live = { cookie: (r.headers.getSetCookie?.() ?? []).map((c) => c.split(';')[0]).join('; '), token: /name="tq-csrf" content="([^"]*)"/.exec(await r.text())?.[1] ?? '', last: 0 }
|
|
49
|
+
}
|
|
50
|
+
async function wire(op, args = []) {
|
|
51
|
+
const file = fileOf(op, args)
|
|
52
|
+
if (existsSync(file)) return JSON.parse(readFileSync(file, 'utf8'))
|
|
53
|
+
if (!ORIGIN) return { ok: true, data: null, dataVersion: 'local' }
|
|
54
|
+
if (!live) await liveSession()
|
|
55
|
+
for (let i = 0; i < 6; i++) {
|
|
56
|
+
const wait = Math.max(0, live.last + 3500 - Date.now())
|
|
57
|
+
if (wait) await new Promise((r) => setTimeout(r, wait))
|
|
58
|
+
live.last = Date.now()
|
|
59
|
+
const qs = args.length ? `?a=${b64(canon(args))}` : ''
|
|
60
|
+
const res = await fetch(`${ORIGIN}/_tq/data/${op}${qs}`, { headers: { cookie: live.cookie, 'x-tq-csrf': live.token, 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', origin: ORIGIN, referer: ORIGIN + '/', accept: 'application/json' } })
|
|
61
|
+
if (res.status === 429) { await new Promise((r) => setTimeout(r, Number(res.headers.get('retry-after') ?? 30) * 1000 + 500)); continue }
|
|
62
|
+
const env = await res.json()
|
|
63
|
+
if (!env.ok && env.error?.code === 'csrf_failed') { await liveSession(); continue }
|
|
64
|
+
writeFileSync(file, JSON.stringify(env))
|
|
65
|
+
return env
|
|
66
|
+
}
|
|
67
|
+
return { ok: false, error: { code: 'unavailable', requestId: 'local' } }
|
|
68
|
+
}
|
|
69
|
+
const get = async (op, args = []) => {
|
|
70
|
+
const env = await wire(op, args)
|
|
71
|
+
return env.ok ? env.data : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── routing (the platform's built-in kinds, theme-abi-v2 §2.1) ────────────────────────────────
|
|
75
|
+
function route(url) {
|
|
76
|
+
const p = decodeURIComponent(url.pathname).replace(/\/+$/, '') || '/'
|
|
77
|
+
const m = (re) => re.exec(p)
|
|
78
|
+
let r
|
|
79
|
+
if (p === '/') r = { kind: 'home' }
|
|
80
|
+
else if (m(/^\/products\/([^/]+)$/)) r = { kind: 'product', handle: m(/^\/products\/([^/]+)$/)[1] }
|
|
81
|
+
else if (p === '/collections') r = { kind: 'collections' }
|
|
82
|
+
else if (m(/^\/collections\/([^/]+)$/)) r = { kind: 'collection', handle: m(/^\/collections\/([^/]+)$/)[1] }
|
|
83
|
+
else if (m(/^\/pages\/([^/]+)$/)) r = { kind: 'page', handle: m(/^\/pages\/([^/]+)$/)[1] }
|
|
84
|
+
else if (m(/^\/blogs\/([^/]+)\/([^/]+)$/)) { const x = m(/^\/blogs\/([^/]+)\/([^/]+)$/); r = { kind: 'article', blogHandle: x[1], handle: x[2] } }
|
|
85
|
+
else if (m(/^\/blogs\/([^/]+)$/)) r = { kind: 'blog', handle: m(/^\/blogs\/([^/]+)$/)[1] }
|
|
86
|
+
else if (p === '/search') r = { kind: 'search' }
|
|
87
|
+
else if (p === '/cart') r = { kind: 'cart' }
|
|
88
|
+
else if (p === '/account' || p.startsWith('/account/')) r = { kind: 'customer' }
|
|
89
|
+
else if (m(/^\/policies\/([^/]+)$/)) r = { kind: 'policy', handle: m(/^\/policies\/([^/]+)$/)[1] }
|
|
90
|
+
else if (p === '/contact') r = { kind: 'custom:contact' }
|
|
91
|
+
else r = { kind: 'not_found' }
|
|
92
|
+
const params = {}
|
|
93
|
+
if (r.kind === 'search' && url.searchParams.has('q')) params.q = url.searchParams.get('q')
|
|
94
|
+
r.template = manifest.routes[r.kind] ?? '404'
|
|
95
|
+
if (r.kind === 'page' && manifest.templates[`page.${r.handle}`]) r.template = `page.${r.handle}`
|
|
96
|
+
return { ...r, params, canonical: `${url.origin}${p === '/' ? '/' : p}` }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── content: the store's revision over the theme's signed defaults; asset: → this host's URL ──────
|
|
100
|
+
const ASSET_BASE = (origin) => `${origin}/__assets`
|
|
101
|
+
function resolveAssets(v, origin) {
|
|
102
|
+
if (typeof v === 'string' && v.startsWith('asset:client/')) return `${ASSET_BASE(origin)}/${v.slice('asset:client/'.length)}`
|
|
103
|
+
if (Array.isArray(v)) return v.map((x) => resolveAssets(x, origin))
|
|
104
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, resolveAssets(x, origin)]))
|
|
105
|
+
return v
|
|
106
|
+
}
|
|
107
|
+
function pageDoc(template, origin) {
|
|
108
|
+
const t = manifest.templates[template]
|
|
109
|
+
const regions = []
|
|
110
|
+
for (const id of t.regions) {
|
|
111
|
+
const def = manifest.regions[id]
|
|
112
|
+
let sections
|
|
113
|
+
if (def.scope === 'shared') sections = content.regions?.[id]?.sections ?? readDefaults(`regions/${id}.json`).sections
|
|
114
|
+
else {
|
|
115
|
+
const own = content.templates?.[template]?.regions?.[id]?.sections
|
|
116
|
+
sections = own ?? readDefaults(`templates/${template}.json`).regions?.[id]?.sections
|
|
117
|
+
}
|
|
118
|
+
if (sections) regions.push({ region: id, sections: resolveAssets(sections, origin) })
|
|
119
|
+
}
|
|
120
|
+
return { contentVersion: manifest.contentVersion, template, regions }
|
|
121
|
+
}
|
|
122
|
+
const settingValues = () => ({ ...readDefaults('settings.json').values, ...(content.settings?.values ?? {}) })
|
|
123
|
+
|
|
124
|
+
// ── ctx.data: the snapshot the platform composes, from the wire ─────────────────────────────────
|
|
125
|
+
async function snapshot(r, page) {
|
|
126
|
+
const [shop, menuList] = await Promise.all([get('shop.get'), get('shop.menus')])
|
|
127
|
+
const menus = Object.fromEntries((menuList ?? []).map((m) => [m.handle, m]))
|
|
128
|
+
let resource = { kind: r.kind }
|
|
129
|
+
if (r.kind === 'product') resource = { kind: 'product', product: await get('catalog.product', [r.handle]) }
|
|
130
|
+
else if (r.kind === 'collection') resource = { kind: 'collection', collection: await get('catalog.collection', [r.handle, { productsFirst: 24 }]) }
|
|
131
|
+
else if (r.kind === 'collections') resource = { kind: 'collections', collections: await get('catalog.collections', [{ first: 50 }]) }
|
|
132
|
+
else if (r.kind === 'page') resource = { kind: 'page', page: await get('content.page', [r.handle]) }
|
|
133
|
+
else if (r.kind === 'blog') resource = { kind: 'blog', blog: await get('content.blog', [r.handle]) }
|
|
134
|
+
else if (r.kind === 'article') resource = { kind: 'article', article: await get('content.article', [r.blogHandle, r.handle]), blog: await get('content.blog', [r.blogHandle]) }
|
|
135
|
+
else if (r.kind === 'policy') resource = { kind: 'policy', policy: await get('content.policy', [r.handle]) }
|
|
136
|
+
else if (r.kind === 'search') resource = { kind: 'search', result: await get('catalog.search', [{ query: r.params.q ?? '', first: 48, types: ['product', 'page', 'article'] }]) }
|
|
137
|
+
else if (r.kind === 'cart') resource = { kind: 'cart', cart: cartView() }
|
|
138
|
+
const refs = {}
|
|
139
|
+
const walk = (nodes) => {
|
|
140
|
+
for (const n of nodes ?? []) {
|
|
141
|
+
const attrs = manifest.components[n.type]?.attributes ?? {}
|
|
142
|
+
for (const [k, spec] of Object.entries(attrs)) {
|
|
143
|
+
if (!spec.resolve) continue
|
|
144
|
+
const v = n.settings?.[k] ?? spec.default
|
|
145
|
+
if (typeof v === 'string' && v.trim()) refs[`${n.id}.${k}`] = { type: spec.type, handle: v.trim() }
|
|
146
|
+
}
|
|
147
|
+
walk(n.blocks)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const reg of page.regions) walk(reg.sections)
|
|
151
|
+
for (const [key, { type, handle }] of Object.entries(refs)) {
|
|
152
|
+
refs[key] =
|
|
153
|
+
type === 'collection' ? await get('catalog.collection', [handle, { productsFirst: 24 }])
|
|
154
|
+
: type === 'product' ? await get('catalog.product', [handle])
|
|
155
|
+
: type === 'menu' ? menus[handle] ?? null
|
|
156
|
+
: type === 'page' ? await get('content.page', [handle])
|
|
157
|
+
: type === 'blog' ? await get('content.blog', [handle])
|
|
158
|
+
: null
|
|
159
|
+
}
|
|
160
|
+
return { shop, menus, resource, refs, pixels: [], denied: [] }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── a cart the actions change (in memory, one shopper) ─────────────────────────────────────────
|
|
164
|
+
let cart = { id: 'cart-local', version: 1, lines: arg('cart') ? JSON.parse(readFileSync(arg('cart'), 'utf8')) : [], note: null, discountCodes: [] }
|
|
165
|
+
const money = (n, c) => ({ amount: n.toFixed(2), currencyCode: c })
|
|
166
|
+
function cartView() {
|
|
167
|
+
const cur = cart.lines[0]?.merchandise.price.currencyCode ?? 'USD'
|
|
168
|
+
const sub = cart.lines.reduce((s, l) => s + Number(l.merchandise.price.amount) * l.quantity, 0)
|
|
169
|
+
return {
|
|
170
|
+
...cart,
|
|
171
|
+
checkoutUrl: '/checkout',
|
|
172
|
+
totalQuantity: cart.lines.reduce((s, l) => s + l.quantity, 0),
|
|
173
|
+
attributes: {},
|
|
174
|
+
lines: cart.lines.map((l) => ({ ...l, cost: { subtotalAmount: money(Number(l.merchandise.price.amount) * l.quantity, cur), totalAmount: money(Number(l.merchandise.price.amount) * l.quantity, cur) } })),
|
|
175
|
+
cost: { subtotalAmount: money(sub, cur), totalAmount: money(sub, cur), totalTaxAmount: money(0, cur) },
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function variantById(id) {
|
|
179
|
+
for (const f of (await import('node:fs')).readdirSync(DATA).filter((x) => x.startsWith('catalog.product-'))) {
|
|
180
|
+
const p = JSON.parse(readFileSync(join(DATA, f), 'utf8')).data
|
|
181
|
+
const v = p?.variants?.nodes?.find((x) => x.id === id)
|
|
182
|
+
if (v) return { ...v, product: { id: p.id, handle: p.handle, title: p.title, featuredImage: p.featuredImage } }
|
|
183
|
+
}
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
async function cartAction(name, args) {
|
|
187
|
+
const addLines = async (lines) => {
|
|
188
|
+
for (const l of lines ?? []) {
|
|
189
|
+
const v = await variantById(l.merchandiseId)
|
|
190
|
+
if (!v) continue
|
|
191
|
+
const have = cart.lines.find((x) => x.merchandise.id === v.id)
|
|
192
|
+
if (have) have.quantity += l.quantity ?? 1
|
|
193
|
+
else cart.lines.push({ id: `line-${cart.lines.length + 1}`, quantity: l.quantity ?? 1, merchandise: { id: v.id, title: v.title, price: v.price, image: v.image ?? null, product: v.product, selectedOptions: v.selectedOptions ?? [] }, attributes: {} })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (name === 'cart.create') await addLines(args[0]?.lines)
|
|
197
|
+
else if (name === 'cart.addLines') await addLines(args[0])
|
|
198
|
+
else if (name === 'cart.updateLines') { for (const l of args[0] ?? []) { const x = cart.lines.find((y) => y.id === l.id); if (x) x.quantity = l.quantity } cart.lines = cart.lines.filter((l) => l.quantity > 0) }
|
|
199
|
+
else if (name === 'cart.removeLines') { const ids = new Set(args[0] ?? []); cart.lines = cart.lines.filter((l) => !ids.has(l.id)) }
|
|
200
|
+
else if (name === 'cart.updateNote') cart.note = args[0] ?? null
|
|
201
|
+
cart.version++
|
|
202
|
+
return { ok: true, data: { ok: true, cart: cartView() }, dataVersion: 'local' }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── the platform's response policy ─────────────────────────────────────────────────────────────
|
|
206
|
+
function csp(styleHashes) {
|
|
207
|
+
return [
|
|
208
|
+
"default-src 'self'", "script-src 'self'", `style-src-elem 'self' ${styleHashes.map((h) => `'${h}'`).join(' ')}`, "style-src-attr 'none'",
|
|
209
|
+
"connect-src 'self'", 'img-src https: data: blob: http://localhost:*', 'media-src https: data: blob:', "font-src 'self' https: data:",
|
|
210
|
+
"frame-src 'none'", "object-src 'none'", "base-uri 'self'", "form-action 'self'",
|
|
211
|
+
].join('; ')
|
|
212
|
+
}
|
|
213
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.png': 'image/png', '.json': 'application/json' }
|
|
214
|
+
|
|
215
|
+
createServer(async (req, res) => {
|
|
216
|
+
try {
|
|
217
|
+
const origin = `http://${req.headers.host}`
|
|
218
|
+
const url = new URL(req.url, origin)
|
|
219
|
+
if (url.pathname.startsWith('/__assets/')) {
|
|
220
|
+
const rel = normalize(url.pathname.slice('/__assets/'.length))
|
|
221
|
+
const file = join(abi, 'client', rel)
|
|
222
|
+
if (rel.startsWith('..') || !existsSync(file)) return res.writeHead(404).end()
|
|
223
|
+
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream', 'cache-control': 'no-cache' })
|
|
224
|
+
return res.end(readFileSync(file))
|
|
225
|
+
}
|
|
226
|
+
if (url.pathname.startsWith('/_tq/data/')) {
|
|
227
|
+
const op = url.pathname.slice('/_tq/data/'.length)
|
|
228
|
+
const a = url.searchParams.get('a')
|
|
229
|
+
const args = a ? JSON.parse(Buffer.from(a.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8')) : []
|
|
230
|
+
const env = op === 'cart.get' ? { ok: true, data: cartView(), dataVersion: 'local' } : op === 'consent.get' ? { ok: true, data: { known: false, analytics: false, marketing: false, functional: true, bannerRequired: false }, dataVersion: 'local' } : await wire(op, args)
|
|
231
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
232
|
+
return res.end(JSON.stringify(env))
|
|
233
|
+
}
|
|
234
|
+
if (url.pathname.startsWith('/_tq/actions/')) {
|
|
235
|
+
let body = ''
|
|
236
|
+
for await (const c of req) body += c
|
|
237
|
+
const name = url.pathname.slice('/_tq/actions/'.length)
|
|
238
|
+
const args = JSON.parse(body || '{}').args ?? []
|
|
239
|
+
const env = name.startsWith('cart.') ? await cartAction(name, args) : { ok: true, data: null, dataVersion: 'local' }
|
|
240
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
241
|
+
return res.end(JSON.stringify(env))
|
|
242
|
+
}
|
|
243
|
+
const r = route(url)
|
|
244
|
+
const page = pageDoc(r.template, origin)
|
|
245
|
+
const logs = []
|
|
246
|
+
const ctx = {
|
|
247
|
+
abi: '2.0',
|
|
248
|
+
mode: PREVIEW ? 'preview' : 'serve',
|
|
249
|
+
request: { method: req.method === 'POST' ? 'POST' : 'GET', url: url.href, headers: {}, hasCart: cart.lines.length > 0, isSignedIn: false, isPreview: PREVIEW },
|
|
250
|
+
route: { kind: r.kind, ...(r.handle ? { handle: r.handle } : {}), ...(r.blogHandle ? { blogHandle: r.blogHandle } : {}), template: r.template, params: r.params, canonical: r.canonical },
|
|
251
|
+
locale: { language: 'en', country: 'US', market: 'us', currency: 'USD', availableLanguages: ['en'], timeZone: 'UTC' },
|
|
252
|
+
consent: { known: false, analytics: false, marketing: false, functional: true },
|
|
253
|
+
content: { revision: 'sha256:local', contentVersion: manifest.contentVersion, page, settings: resolveAssets(settingValues(), origin) },
|
|
254
|
+
assets: { base: ASSET_BASE(origin), manifest: clientManifest },
|
|
255
|
+
signal: new AbortController().signal,
|
|
256
|
+
data: {
|
|
257
|
+
dataVersion: 'local',
|
|
258
|
+
snapshot: () => snapshot(r, page),
|
|
259
|
+
shop: { get: () => get('shop.get'), menus: async (h) => { const all = (await get('shop.menus')) ?? []; return h ? all.filter((m) => h.includes(m.handle)) : all }, policies: async () => [], pixels: async () => [] },
|
|
260
|
+
localization: { strings: async () => ({}) },
|
|
261
|
+
catalog: {
|
|
262
|
+
product: (h) => get('catalog.product', [h]),
|
|
263
|
+
products: () => get('catalog.products', [{}]),
|
|
264
|
+
collection: (h, o) => get('catalog.collection', [h, o ?? { productsFirst: 24 }]),
|
|
265
|
+
collections: (a) => get('catalog.collections', [a ?? { first: 50 }]),
|
|
266
|
+
search: (a) => get('catalog.search', [a]),
|
|
267
|
+
predictive: (q, l) => get('catalog.predictive', [q, l ?? 4]),
|
|
268
|
+
recommendations: async (h, i, l) => (await get('catalog.recommendations', [h, i ?? 'related', l ?? 8])) ?? [],
|
|
269
|
+
},
|
|
270
|
+
content: { page: (h) => get('content.page', [h]), blog: (h) => get('content.blog', [h]), article: (b, h) => get('content.article', [b, h]), policy: (h) => get('content.policy', [h]), template: async () => null, metaobjects: async () => ({ nodes: [], pageInfo: { hasNextPage: false } }) },
|
|
271
|
+
cart: { get: async () => cartView() },
|
|
272
|
+
customer: { current: async () => null, loginUrl: () => '/account/login', logoutUrl: () => '/account/logout', accountUrl: (p) => `/account${p ? '/' + p : ''}`, registerUrl: () => '/account/register', recoverUrl: () => '/account/recover' },
|
|
273
|
+
},
|
|
274
|
+
actions: undefined,
|
|
275
|
+
cache: { hint() {} },
|
|
276
|
+
log: { debug() {}, info: (m, f) => logs.push(['info', m, f]), warn: (m, f) => logs.push(['warn', m, f]), error: (m, f) => logs.push(['error', m, f]) },
|
|
277
|
+
now: () => Date.UTC(2026, 9, 13, 12),
|
|
278
|
+
release: { id: 'local', artifactDigest: 'local' },
|
|
279
|
+
capabilities: new Set(manifest.capabilities.server),
|
|
280
|
+
}
|
|
281
|
+
const out = await entry.render(new Request(url.href), ctx)
|
|
282
|
+
for (const l of logs) if (l[0] !== 'info') console.error(`[${l[0]}] ${url.pathname}: ${l[1]} ${JSON.stringify(l[2] ?? '')}`)
|
|
283
|
+
const hashes = out.headers.getAll ? out.headers.getAll('x-tq-csp-style-hash') : (out.headers.get('x-tq-csp-style-hash') ?? '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
284
|
+
let html = await out.text()
|
|
285
|
+
html = html.replace('<head>', '<head>\n <meta name="tq-csrf" content="local">')
|
|
286
|
+
res.writeHead(out.status, { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': csp(hashes), 'cache-control': 'no-store' })
|
|
287
|
+
res.end(html)
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.error(e)
|
|
290
|
+
res.writeHead(500, { 'content-type': 'text/plain' }).end(String(e?.stack ?? e))
|
|
291
|
+
}
|
|
292
|
+
}).listen(PORT, () => console.log(`nova-v3 local host on http://localhost:${PORT} (${PREVIEW ? 'preview' : 'serve'})`))
|
|
293
|
+
|
|
294
|
+
export const _sha = (s) => createHash('sha256').update(s).digest('base64')
|