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,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Layout sanity — the check that was missing every time a screenshot looked
|
|
4
|
+
* wrong while every gate passed.
|
|
5
|
+
*
|
|
6
|
+
* Nothing in this repo looked at PROPORTION. A footer cell stretched to 404px
|
|
7
|
+
* around 120px of payment marks, a newsletter squeezed into a 163px column with
|
|
8
|
+
* its email field clipped and its heading broken across two lines, a question
|
|
9
|
+
* that opened onto an empty panel — all of it passed typecheck, 209 unit tests,
|
|
10
|
+
* the design-rule checker and a full accessibility audit, because none of those
|
|
11
|
+
* ask whether a block is mostly empty or a line is four words long.
|
|
12
|
+
*
|
|
13
|
+
* Three measurements, at every breakpoint the design names:
|
|
14
|
+
* 1. a laid-out box far taller than what it contains
|
|
15
|
+
* 2. a text column so narrow the copy breaks into very short lines
|
|
16
|
+
* 3. a form field too narrow to show what a person types into it
|
|
17
|
+
*
|
|
18
|
+
* pnpm check:layout # needs the dev server running
|
|
19
|
+
*/
|
|
20
|
+
import { chromium } from 'playwright-core'
|
|
21
|
+
|
|
22
|
+
const BASE = process.env.THEME_URL ?? 'http://localhost:4321'
|
|
23
|
+
const WIDTHS = [1440, 1280, 768, 390]
|
|
24
|
+
const ROUTES = ['/', '/products/classic-double-breasted-blazer', '/collections/test-collection', '/cart']
|
|
25
|
+
|
|
26
|
+
/** Space a box holds beyond its content before it reads as a gap. */
|
|
27
|
+
const WASTE_PX = 120
|
|
28
|
+
/** Below this, a line of prose is too short to read comfortably. */
|
|
29
|
+
const MIN_CHARS_PER_LINE = 18
|
|
30
|
+
/** An email field narrower than this clips what has been typed. */
|
|
31
|
+
const MIN_FIELD_PX = 180
|
|
32
|
+
|
|
33
|
+
/** Runs in the page. A real function, not a string — passed as a string,
|
|
34
|
+
* Playwright read this arrow function with a destructured parameter as a
|
|
35
|
+
* statement and returned `undefined` for every page, so the gate reported
|
|
36
|
+
* success on a fixture built to fail three ways. */
|
|
37
|
+
function probe([wastePx, minChars, minField]) {
|
|
38
|
+
const out = []
|
|
39
|
+
/** Anything inside a closed overlay is not on screen; measuring it reports
|
|
40
|
+
* gaps nobody can see. */
|
|
41
|
+
const onScreen = (e) => {
|
|
42
|
+
for (let n = e; n; n = n.parentElement) {
|
|
43
|
+
if (n.hasAttribute?.('inert')) return false
|
|
44
|
+
const cs = getComputedStyle(n)
|
|
45
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
const boxes = [...document.querySelectorAll('main *, .site-footer *, .site-header *')]
|
|
50
|
+
for (const e of boxes) {
|
|
51
|
+
const r = e.getBoundingClientRect()
|
|
52
|
+
if (r.height < 80 || r.width < 40) continue
|
|
53
|
+
if (!onScreen(e)) continue
|
|
54
|
+
const cs = getComputedStyle(e)
|
|
55
|
+
if (cs.position === 'absolute' || cs.position === 'fixed') continue
|
|
56
|
+
// A box whose size the author SET is not a gap. A hero is 640px because
|
|
57
|
+
// the merchant chose "large" and its type sits at the bottom of the frame;
|
|
58
|
+
// a thumbnail rail is as tall as the photo beside it. This check is for
|
|
59
|
+
// space a layout leaves by accident, not space reserved on purpose.
|
|
60
|
+
// `getComputedStyle().height` is the USED value and is never 'auto' for a
|
|
61
|
+
// rendered element, so testing it disabled this check entirely — the
|
|
62
|
+
// fixture built to fail stopped failing. Only a declared minimum, a fixed
|
|
63
|
+
// ratio or a background image count as "the author asked for this space".
|
|
64
|
+
if (cs.aspectRatio !== 'auto') continue
|
|
65
|
+
if (cs.minHeight !== '0px' && cs.minHeight !== 'auto') continue
|
|
66
|
+
if (cs.backgroundImage !== 'none') continue
|
|
67
|
+
// Only overlays are excluded. Filtering on `position === 'static'` also
|
|
68
|
+
// dropped every relatively-positioned child — a product card's media is
|
|
69
|
+
// `position: relative`, so the card looked 80% empty.
|
|
70
|
+
const kids = [...e.children].filter((c) => {
|
|
71
|
+
const p = getComputedStyle(c).position
|
|
72
|
+
return p !== 'absolute' && p !== 'fixed'
|
|
73
|
+
})
|
|
74
|
+
if (!kids.length) continue
|
|
75
|
+
// The SPAN the children occupy, not the sum of their heights. Summing is
|
|
76
|
+
// only right for a vertical stack; in a grid or a row the children sit side
|
|
77
|
+
// by side, and adding their heights made a perfectly packed row of columns
|
|
78
|
+
// look like a mostly empty box.
|
|
79
|
+
const rects = kids.map((c) => c.getBoundingClientRect()).filter((b) => b.height > 0)
|
|
80
|
+
if (!rects.length) continue
|
|
81
|
+
const top = Math.min(...rects.map((b) => b.top))
|
|
82
|
+
const bottom = Math.max(...rects.map((b) => b.bottom))
|
|
83
|
+
// Padding is deliberate space, not a gap in the layout.
|
|
84
|
+
const chrome = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom)
|
|
85
|
+
const content = bottom - top + chrome
|
|
86
|
+
if (content > 0 && r.height - content > wastePx) {
|
|
87
|
+
out.push({ kind: 'empty-space', el: (e.className || e.tagName).toString().split(' ')[0],
|
|
88
|
+
detail: `${Math.round(r.height - content)}px of a ${Math.round(r.height)}px box holds nothing` })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const e of document.querySelectorAll('p, .lede, .newsletter__note')) {
|
|
92
|
+
if (!onScreen(e)) continue
|
|
93
|
+
const r = e.getBoundingClientRect()
|
|
94
|
+
const text = (e.textContent || '').trim()
|
|
95
|
+
if (r.height < 40 || text.length < 60) continue
|
|
96
|
+
const lh = parseFloat(getComputedStyle(e).lineHeight) || 20
|
|
97
|
+
const lines = Math.round(r.height / lh)
|
|
98
|
+
if (lines >= 3 && text.length / lines < minChars) {
|
|
99
|
+
out.push({ kind: 'cramped-text', el: (e.className || e.tagName).toString().split(' ')[0],
|
|
100
|
+
detail: `${Math.round(text.length / lines)} characters per line over ${lines} lines` })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const e of document.querySelectorAll('input[type=email], input[type=search], input[type=text]')) {
|
|
104
|
+
if (!onScreen(e)) continue
|
|
105
|
+
const r = e.getBoundingClientRect()
|
|
106
|
+
if (r.height > 0 && r.width < minField) {
|
|
107
|
+
out.push({ kind: 'clipped-field', el: (e.className || e.type).toString().split(' ')[0],
|
|
108
|
+
detail: `${Math.round(r.width)}px wide` })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const seen = new Set()
|
|
112
|
+
return out.filter((o) => {
|
|
113
|
+
const k = o.kind + o.el + o.detail
|
|
114
|
+
if (seen.has(k)) return false
|
|
115
|
+
seen.add(k)
|
|
116
|
+
return true
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// `playwright-core` ships no browser binaries, so it drives the Chrome already
|
|
122
|
+
// installed on the machine. Connecting over CDP to a shared Chrome was tried
|
|
123
|
+
// first and silently returned nothing on every page — a gate that reports
|
|
124
|
+
// success because it never looked is worse than no gate, which is why the
|
|
125
|
+
// sanity assertions below exist.
|
|
126
|
+
const browser = await chromium.launch({ channel: 'chrome' })
|
|
127
|
+
|
|
128
|
+
let total = 0
|
|
129
|
+
for (const width of WIDTHS) {
|
|
130
|
+
// A CDP connection hands back the browser's existing context; creating a new
|
|
131
|
+
// one is not supported, so the viewport is set on the page instead.
|
|
132
|
+
const context = await browser.newContext({ viewport: { width, height: 1000 } })
|
|
133
|
+
const page = await context.newPage()
|
|
134
|
+
for (const route of ROUTES) {
|
|
135
|
+
await page.goto(BASE + route, { waitUntil: 'networkidle' })
|
|
136
|
+
await page.waitForTimeout(4500)
|
|
137
|
+
try { await page.click('button:has-text("Accept")', { timeout: 1500 }) } catch { /* none */ }
|
|
138
|
+
// A silent empty result is how a gate becomes decoration: assert the page
|
|
139
|
+
// is actually loaded and sized before believing that it found nothing.
|
|
140
|
+
const sanity = await page.evaluate(() => ({
|
|
141
|
+
url: location.href,
|
|
142
|
+
width: document.documentElement.clientWidth,
|
|
143
|
+
boxes: document.querySelectorAll('main *, .site-footer *, .site-header *').length,
|
|
144
|
+
}))
|
|
145
|
+
if (sanity.boxes === 0) {
|
|
146
|
+
console.error(` ${width}px ${route} — page produced no elements to measure (${sanity.url})`)
|
|
147
|
+
process.exitCode = 1
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (Math.abs(sanity.width - width) > 40) {
|
|
151
|
+
console.error(` asked for ${width}px, the page reports ${sanity.width}px — viewport not applied`)
|
|
152
|
+
process.exitCode = 1
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
const found = await page.evaluate(probe, [WASTE_PX, MIN_CHARS_PER_LINE, MIN_FIELD_PX])
|
|
156
|
+
for (const f of found) {
|
|
157
|
+
total++
|
|
158
|
+
console.error(` ${width}px ${route} — [${f.kind}] ${f.el}: ${f.detail}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await context.close()
|
|
162
|
+
}
|
|
163
|
+
await browser.close()
|
|
164
|
+
|
|
165
|
+
if (total) {
|
|
166
|
+
console.error(`\n✗ ${total} layout problem(s)`)
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
console.log(`✓ no layout problems across ${ROUTES.length} routes x ${WIDTHS.length} widths`)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Exactly one top-level heading per template.
|
|
4
|
+
*
|
|
5
|
+
* A live sweep catches this on routable pages, but `templateSuffix` variants
|
|
6
|
+
* (product.bundle, collection.featured, blog.featured) need a product or
|
|
7
|
+
* collection assigned to them and so are never visited in a local run. Three
|
|
8
|
+
* of them shipped with two `h1`s because a decorative rich-text was set to the
|
|
9
|
+
* h1 size above a section that already owns the page heading.
|
|
10
|
+
*
|
|
11
|
+
* The list below is the source of truth for which sections own an `h1`, and is
|
|
12
|
+
* asserted against the code by tests/design-system.test.ts.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
15
|
+
import { join, dirname } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
// The theme: the directory the command runs in.
|
|
19
|
+
const ROOT = process.cwd()
|
|
20
|
+
|
|
21
|
+
/** Sections that always render the page's `h1`. */
|
|
22
|
+
const OWNS_H1 = new Set([
|
|
23
|
+
'product-details', 'blog-posts', 'page-body',
|
|
24
|
+
'article-body', 'cart-items', 'account', 'search-results',
|
|
25
|
+
'not-found', 'policy-page',
|
|
26
|
+
])
|
|
27
|
+
/** Sections that own it only in some configuration. */
|
|
28
|
+
const CONDITIONAL = {
|
|
29
|
+
// Only the first slide, and only when the slideshow is the page's opener.
|
|
30
|
+
slideshow: () => true,
|
|
31
|
+
hero: () => true,
|
|
32
|
+
// Only on the /collections index route, where it is the whole page.
|
|
33
|
+
'collection-list': (_s, template) => template === 'list-collections',
|
|
34
|
+
// Only when the merchant picks the h1 size.
|
|
35
|
+
'rich-text': (s) => (s.settings ?? {}).headingSize === 'h1',
|
|
36
|
+
// Direction B: the banner carries the title only when told to, and is then
|
|
37
|
+
// paired with `main-collection.showHeader: false` (checked below).
|
|
38
|
+
'collection-banner': (s) => (s.settings ?? {}).pageHeading === true,
|
|
39
|
+
// 07B: the contact page's first section carries "Contact us" when told to.
|
|
40
|
+
'contact-form': (s) => (s.settings ?? {}).pageHeading === true,
|
|
41
|
+
// The collection page's own header can be switched off for that pairing.
|
|
42
|
+
'main-collection': (s) => (s.settings ?? {}).showHeader !== false,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const problems = []
|
|
46
|
+
for (const file of readdirSync(join(ROOT, 'templates')).filter((f) => f.endsWith('.json'))) {
|
|
47
|
+
const doc = JSON.parse(readFileSync(join(ROOT, 'templates', file), 'utf8'))
|
|
48
|
+
const body = (doc.sections ?? []).filter(
|
|
49
|
+
(s) => s.area !== 'header' && !['header', 'footer', 'announcement-bar'].includes(s.type),
|
|
50
|
+
)
|
|
51
|
+
const owners = body.filter(
|
|
52
|
+
(s) => OWNS_H1.has(s.type) || (CONDITIONAL[s.type]?.(s, file.replace(/\.json$/, '')) ?? false),
|
|
53
|
+
)
|
|
54
|
+
if (owners.length > 1) {
|
|
55
|
+
problems.push(`${file}: ${owners.length} sections claim the page heading — ${owners.map((s) => s.type).join(', ')}`)
|
|
56
|
+
}
|
|
57
|
+
if (owners.length === 0) {
|
|
58
|
+
problems.push(`${file}: no section carries the page heading`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (problems.length) {
|
|
63
|
+
console.error('✗ page heading problems:')
|
|
64
|
+
for (const p of problems) console.error(` ${p}`)
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
console.log('✓ every template has exactly one section carrying the page heading')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `pnpm check:jsonld` — the BUILT theme must ship schema.org Product/Offer JSON-LD in the SERVER html of a product
|
|
4
|
+
* page (crawlers and AI answer engines do not run JS) and nothing on non-product pages.
|
|
5
|
+
*
|
|
6
|
+
* Runs the real SSR entry (`dist/ssr/entry.mjs`, so build first) against the theme's mock catalog and checks what
|
|
7
|
+
* Google's Product snippet requires: `name`, an absolute `url`, and an `offers` block with a numeric `price`, an
|
|
8
|
+
* ISO-4217 `priceCurrency` and a schema.org `availability` URL. Per-market currency and availability logic is
|
|
9
|
+
* unit-tested in tests/structured-data.test.ts; this guards the wiring (build -> render -> head).
|
|
10
|
+
*/
|
|
11
|
+
import { access } from 'node:fs/promises'
|
|
12
|
+
import { join, resolve } from 'node:path'
|
|
13
|
+
import { pathToFileURL } from 'node:url'
|
|
14
|
+
|
|
15
|
+
const root = resolve(process.argv[2] ?? '.')
|
|
16
|
+
const entry = join(root, 'dist', 'ssr', 'entry.mjs')
|
|
17
|
+
try {
|
|
18
|
+
await access(entry)
|
|
19
|
+
} catch {
|
|
20
|
+
console.error('error dist/ssr/entry.mjs is missing: run `pnpm build` first')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const mod = await import(pathToFileURL(entry).href)
|
|
25
|
+
const ctx = { mode: 'serve', assets: { base: '', manifest: {} }, content: { templates: {}, groups: {}, settings: {} } }
|
|
26
|
+
const errors = []
|
|
27
|
+
const CURRENCIES = new Set(Intl.supportedValuesOf('currency'))
|
|
28
|
+
const AVAILABILITY = new Set(['InStock', 'OutOfStock', 'PreOrder', 'BackOrder', 'SoldOut', 'LimitedAvailability', 'Discontinued', 'InStoreOnly', 'OnlineOnly'].map((a) => `https://schema.org/${a}`))
|
|
29
|
+
const page = async (path) => {
|
|
30
|
+
const res = await mod.render(new Request(`https://shop.example.com${path}`), ctx)
|
|
31
|
+
return { status: res.status, html: await res.text() }
|
|
32
|
+
}
|
|
33
|
+
const blocks = (html) => [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => m[1])
|
|
34
|
+
/** The Product blocks only: sections add their own types (BreadcrumbList, FAQPage), which are not duplicates. */
|
|
35
|
+
const productBlocks = (html) =>
|
|
36
|
+
blocks(html).filter((b) => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(b)['@type'] === 'Product'
|
|
39
|
+
} catch {
|
|
40
|
+
return true // unparseable counts, so the check below reports it
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const product = await page('/products/example-product-1')
|
|
45
|
+
const found = productBlocks(product.html)
|
|
46
|
+
if (product.status !== 200) errors.push(`product page answered ${product.status}`)
|
|
47
|
+
if (found.length !== 1) errors.push(`expected exactly one Product JSON-LD block on the product page, found ${found.length}`)
|
|
48
|
+
else {
|
|
49
|
+
let ld
|
|
50
|
+
try {
|
|
51
|
+
ld = JSON.parse(found[0])
|
|
52
|
+
} catch (e) {
|
|
53
|
+
errors.push(`JSON-LD is not valid JSON: ${e.message}`)
|
|
54
|
+
}
|
|
55
|
+
if (ld) {
|
|
56
|
+
if (ld['@context'] !== 'https://schema.org') errors.push('@context must be https://schema.org')
|
|
57
|
+
if (ld['@type'] !== 'Product') errors.push('@type must be Product')
|
|
58
|
+
if (!ld.name) errors.push('name is required')
|
|
59
|
+
if (!/^https?:\/\//.test(ld.url ?? '')) errors.push('url must be absolute')
|
|
60
|
+
const o = ld.offers
|
|
61
|
+
if (!o) errors.push('offers is required')
|
|
62
|
+
else {
|
|
63
|
+
const price = o['@type'] === 'AggregateOffer' ? o.lowPrice : o.price
|
|
64
|
+
if (!/^\d+(?:\.\d+)?$/.test(String(price ?? ''))) errors.push(`offers price is not numeric: ${price}`)
|
|
65
|
+
if (!CURRENCIES.has(o.priceCurrency)) errors.push(`offers priceCurrency is not ISO-4217: ${o.priceCurrency}`)
|
|
66
|
+
if (o.availability !== undefined && !AVAILABILITY.has(o.availability)) errors.push(`offers availability is not a schema.org URL: ${o.availability}`)
|
|
67
|
+
}
|
|
68
|
+
for (const img of ld.image ?? []) if (!/^https?:\/\//.test(img)) errors.push(`image is not absolute: ${img}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (/<\/script><script/.test(found[0] ?? '')) errors.push('JSON-LD must not contain a raw script terminator')
|
|
72
|
+
|
|
73
|
+
const home = await page('/')
|
|
74
|
+
if (productBlocks(home.html).length) errors.push('the home page must not carry a Product block')
|
|
75
|
+
|
|
76
|
+
if (errors.length) {
|
|
77
|
+
for (const e of errors) console.error(`error ${e}`)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
console.log('✓ product page ships one valid schema.org Product/Offer JSON-LD block in the server html; home has none')
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `pnpm check` — validate this BUILT theme against Tanqory theme spec v1.
|
|
4
|
+
*
|
|
5
|
+
* The theme owns all of its code, so it carries its own copy of the spec
|
|
6
|
+
* checker (the platform keeps the canonical one in `packages/theme-spec`);
|
|
7
|
+
* a clone of this repository alone can run every gate.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, stat, access } from 'node:fs/promises'
|
|
10
|
+
import { join, resolve } from 'node:path'
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const SPEC_VERSION = 1
|
|
14
|
+
|
|
15
|
+
const CANVAS_PROTOCOL = Object.freeze({
|
|
16
|
+
fromTheme: Object.freeze(['tq:ready', 'tq:select', 'tanqory-section-selected', 'tanqory-block-selected', 'tanqory-content-changed', 'tanqory-content', 'tanqory-collections', 'tanqory-menus', 'tanqory-products', 'tq-preview-height']),
|
|
17
|
+
toTheme: Object.freeze(['tq:set-content', 'tq:select', 'tq:theme-settings', 'tanqory-preview-update-section', 'tanqory-preview-select', 'tanqory-preview-reorder-sections', 'tanqory-preview-remove-section', 'tanqory-preview-insert-section', 'tanqory-get-content', 'tanqory-request-collections', 'tanqory-request-menus', 'tanqory-request-products']),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const LIMITS = Object.freeze({
|
|
21
|
+
ssrBundleBytes: 10 * 1024 * 1024, // Workers script limit
|
|
22
|
+
clientBundleBytes: 25 * 1024 * 1024,
|
|
23
|
+
renderMs: 50, // CPU budget per render (isolate)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const SETTING_TYPES = new Set(['text', 'textarea', 'richtext', 'html', 'url', 'number', 'range', 'boolean', 'select', 'radio', 'color', 'text_alignment', 'image', 'video', 'collection', 'product', 'page', 'blog', 'article', 'menu', 'header', 'paragraph', 'link_list', 'inline_richtext', 'font_picker', 'checkbox'])
|
|
27
|
+
|
|
28
|
+
/** Errors in a manifest object; [] when valid. */
|
|
29
|
+
function validateManifest(m) {
|
|
30
|
+
const errors = []
|
|
31
|
+
if (!m || typeof m !== 'object') return ['manifest is not an object']
|
|
32
|
+
if (m.spec !== SPEC_VERSION) errors.push(`manifest.spec must be ${SPEC_VERSION}`)
|
|
33
|
+
if (typeof m.name !== 'string' || !m.name) errors.push('manifest.name is required')
|
|
34
|
+
if (!Array.isArray(m.sections)) errors.push('manifest.sections must be an array')
|
|
35
|
+
else {
|
|
36
|
+
const seen = new Set()
|
|
37
|
+
m.sections.forEach((s, i) => {
|
|
38
|
+
if (!s || typeof s.name !== 'string' || !/^[a-z0-9-]+$/.test(s.name)) { errors.push(`sections[${i}].name must be kebab-case`); return }
|
|
39
|
+
if (seen.has(s.name)) errors.push(`duplicate section "${s.name}"`)
|
|
40
|
+
seen.add(s.name)
|
|
41
|
+
if (s.role && !['layout', 'section', 'block'].includes(s.role)) errors.push(`sections[${i}] "${s.name}": role must be layout|section|block`)
|
|
42
|
+
for (const [id, spec] of Object.entries(s.attributes ?? {})) {
|
|
43
|
+
if (!spec || typeof spec.type !== 'string') errors.push(`section "${s.name}" attribute "${id}" has no type`)
|
|
44
|
+
else if (!SETTING_TYPES.has(spec.type)) errors.push(`section "${s.name}" attribute "${id}": unknown type "${spec.type}"`)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(m.templates) || !m.templates.includes('index')) errors.push('manifest.templates must include "index"')
|
|
49
|
+
if (m.groups !== undefined && !Array.isArray(m.groups)) errors.push('manifest.groups must be an array')
|
|
50
|
+
return errors
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const FORBIDDEN_IMPORTS = /(?:^|[^\w])(?:from|import)\s*\(?\s*["'](node:[a-z_]+|fs|path|os|child_process|crypto|net|http|https|worker_threads|vm)["']/g
|
|
54
|
+
|
|
55
|
+
/** Validate a BUILT theme directory. Never throws for a theme error — reports it. */
|
|
56
|
+
async function checkTheme(themeDir) {
|
|
57
|
+
const errors = [], warnings = []
|
|
58
|
+
const entry = join(themeDir, 'dist', 'ssr', 'entry.mjs')
|
|
59
|
+
const clientDir = join(themeDir, 'dist', 'client')
|
|
60
|
+
const exists = async (p) => { try { await access(p); return true } catch { return false } }
|
|
61
|
+
if (!(await exists(entry))) errors.push('dist/ssr/entry.mjs is missing — the theme must build an SSR entry that exports render() and manifest')
|
|
62
|
+
if (!(await exists(join(clientDir, '.vite', 'manifest.json')))) errors.push('dist/client/.vite/manifest.json is missing — build the client with `build.manifest: true`')
|
|
63
|
+
let manifest
|
|
64
|
+
if (errors.length === 0) {
|
|
65
|
+
const size = (await stat(entry)).size
|
|
66
|
+
if (size > LIMITS.ssrBundleBytes) errors.push(`dist/ssr/entry.mjs is ${size} bytes, over the ${LIMITS.ssrBundleBytes}-byte isolate limit`)
|
|
67
|
+
const src = await readFile(entry, 'utf8')
|
|
68
|
+
const bad = [...src.matchAll(FORBIDDEN_IMPORTS)].map((m) => m[1])
|
|
69
|
+
if (bad.length) errors.push(`SSR bundle imports Node built-ins (${[...new Set(bad)].join(', ')}) — not available in the isolate; bundle everything (ssr.noExternal) and avoid Node APIs`)
|
|
70
|
+
if (/\brequire\s*\(/.test(src)) warnings.push('SSR bundle contains require() — it must be pure ESM for the isolate')
|
|
71
|
+
try {
|
|
72
|
+
const mod = await import(pathToFileURL(entry).href)
|
|
73
|
+
if (typeof mod.render !== 'function') errors.push('entry does not export render()')
|
|
74
|
+
if (!mod.manifest || typeof mod.manifest !== 'object') errors.push('entry does not export manifest')
|
|
75
|
+
else { manifest = mod.manifest; errors.push(...validateManifest(manifest)) }
|
|
76
|
+
} catch (e) {
|
|
77
|
+
errors.push(`entry failed to load: ${e?.message || e}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { ok: errors.length === 0, errors, warnings, manifest }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dir = resolve(process.argv[2] || '.')
|
|
84
|
+
const r = await checkTheme(dir)
|
|
85
|
+
for (const w of r.warnings) console.log('warn ', w)
|
|
86
|
+
for (const e of r.errors) console.log('error', e)
|
|
87
|
+
if (r.ok) console.log(`✓ theme "${r.manifest.name}" v${r.manifest.version ?? '?'} passes spec v1 (${r.manifest.sections.length} sections, ${r.manifest.templates.length} templates)`)
|
|
88
|
+
process.exit(r.ok ? 0 : 1)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `tanqory-theme contracts` — writes and checks the theme's contract locks,
|
|
4
|
+
* `.tanqory/contracts/`. Compiler-managed: theme authors never edit them.
|
|
5
|
+
*
|
|
6
|
+
* The approved design package (`*.dc.html`) carries its data in a
|
|
7
|
+
* `<script type="text/x-dc">` block whose `renderVals()` returns the tables the
|
|
8
|
+
* canvas renders. Executing that block is the only faithful way to read it.
|
|
9
|
+
* Each design family becomes one lock, `<theme>-<family>.json`: the persisted
|
|
10
|
+
* ids (sections, blocks, settings, options), labels, presets and shared
|
|
11
|
+
* settings. `LOCK.json` records, per lock, the design file it came from and the
|
|
12
|
+
* sha256 of both — so a hand edit, or a design change nobody re-locked, fails.
|
|
13
|
+
*
|
|
14
|
+
* tanqory-theme contracts --check locks match LOCK.json (no hand edits)
|
|
15
|
+
* tanqory-theme contracts --design <dir> --check …and still match the design package
|
|
16
|
+
* tanqory-theme contracts --design <dir> --write re-extract from the design package
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { join, resolve } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const THEME = process.cwd()
|
|
23
|
+
const OUT = join(THEME, '.tanqory', 'contracts')
|
|
24
|
+
const LOCK = join(OUT, 'LOCK.json')
|
|
25
|
+
const args = process.argv.slice(2)
|
|
26
|
+
const WRITE = args.includes('--write')
|
|
27
|
+
const designAt = args.indexOf('--design')
|
|
28
|
+
const DESIGN = designAt >= 0 ? resolve(args[designAt + 1] ?? '') : null
|
|
29
|
+
|
|
30
|
+
/** The families a theme locks, by default (Nova's): design file → family id. */
|
|
31
|
+
const DEFAULT_SOURCES = {
|
|
32
|
+
'01': 'Nova 01 Style System.dc.html',
|
|
33
|
+
'12A': 'Nova 12A - Chrome and Structure.dc.html',
|
|
34
|
+
'12B': 'Nova 12B - Heroes and Promotion.dc.html',
|
|
35
|
+
'12C': 'Nova 12C - Product and Merchandising.dc.html',
|
|
36
|
+
'12D': 'Nova 12D - Content and Storytelling.dc.html',
|
|
37
|
+
'12E': 'Nova 12E - Editorial and Long-form.dc.html',
|
|
38
|
+
'12F': 'Nova 12F - Trust and Advanced Commerce.dc.html',
|
|
39
|
+
'12G': 'Nova 12G - Utility and Customer Experience.dc.html',
|
|
40
|
+
'13': 'Nova 13 - Overlays and Feedback.dc.html',
|
|
41
|
+
'14': 'Nova 14 - Media and System States.dc.html',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sha256 = (text) => createHash('sha256').update(text).digest('hex')
|
|
45
|
+
const themeName = () => {
|
|
46
|
+
const name = JSON.parse(readFileSync(join(THEME, 'package.json'), 'utf8')).name ?? 'theme'
|
|
47
|
+
return name.split('/').pop()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── extraction (the design package → one JSON per family) ────────────────────
|
|
51
|
+
|
|
52
|
+
const DISPLAY_ONLY = new Set(['sbg', 'sfg', 'optDisp', 'legacyDisp', 'legacyLblDisp', 'defDisp', 'defRange', 'rowNames', 'counts'])
|
|
53
|
+
|
|
54
|
+
function renderVals(src, file) {
|
|
55
|
+
const m = src.match(/<script type="text\/x-dc"[^>]*>([\s\S]*?)<\/script>/)
|
|
56
|
+
if (!m) throw new Error(`${file}: no x-dc logic block`)
|
|
57
|
+
class DCLogic {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.props = {}
|
|
60
|
+
this.state = {}
|
|
61
|
+
}
|
|
62
|
+
setState() {}
|
|
63
|
+
}
|
|
64
|
+
return new Function('DCLogic', `${m[1]}; return new Component().renderVals()`)(DCLogic)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `a · b · c` → ['a', 'b', 'c']; '' → []. */
|
|
68
|
+
const ids = (s) => (typeof s === 'string' && s.trim() ? s.split('·').map((x) => x.trim()).filter(Boolean) : [])
|
|
69
|
+
|
|
70
|
+
/** Booleans and numbers arrive as strings in some tables ("false", "32"). */
|
|
71
|
+
function normaliseDefault(row) {
|
|
72
|
+
const d = row.def
|
|
73
|
+
if (row.control === 'boolean') return d === true || d === 'true'
|
|
74
|
+
if (row.control === 'range' || row.control === 'number') return d === '' || d == null ? null : Number(d)
|
|
75
|
+
return d == null ? '' : d
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function row(r) {
|
|
79
|
+
const out = {}
|
|
80
|
+
for (const [k, v] of Object.entries(r)) if (!DISPLAY_ONLY.has(k)) out[k] = v
|
|
81
|
+
out.options = ids(r.options)
|
|
82
|
+
out.optionLabels = ids(r.optionLabels)
|
|
83
|
+
out.legacyOptions = ids(r.legacyOptions)
|
|
84
|
+
out.legacyOptionLabels = ids(r.legacyOptionLabels)
|
|
85
|
+
out.def = normaliseDefault(r)
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A foundations table set (Nova 01) is kept whole, not as a section contract. */
|
|
90
|
+
const FOUNDATION_KEYS = ['tokens', 'colors', 'brandDerive', 'families', 'typeScale', 'typeRules', 'radii', 'radiusMap', 'gaps', 'sectionSpace', 'containers', 'ratios', 'motion', 'dark', 'principles']
|
|
91
|
+
|
|
92
|
+
function family(v) {
|
|
93
|
+
if (v.typeScale && v.radii) return Object.fromEntries(FOUNDATION_KEYS.map((k) => [k, v[k] ?? []]))
|
|
94
|
+
const out = {
|
|
95
|
+
sections: (v.sections ?? []).map((s) => ({ name: s.name, label: s.label, status: s.status, rows: s.rows.map(row) })),
|
|
96
|
+
blocks: (v.blocks ?? []).map((b) => ({
|
|
97
|
+
name: b.name,
|
|
98
|
+
label: b.label,
|
|
99
|
+
parents: [...new Set(b.parents ?? [])],
|
|
100
|
+
min: b.min,
|
|
101
|
+
recommended: b.recommended,
|
|
102
|
+
max: b.max,
|
|
103
|
+
status: b.status,
|
|
104
|
+
resp: b.resp,
|
|
105
|
+
empty: b.empty,
|
|
106
|
+
summary: b.summary ?? '',
|
|
107
|
+
rows: (b.rows ?? []).map(row),
|
|
108
|
+
})),
|
|
109
|
+
presets: v.presets ?? [],
|
|
110
|
+
shared: (v.shared ?? []).map(row),
|
|
111
|
+
sharedApplicability: v.sharedApplicability ?? [],
|
|
112
|
+
globals: (v.globals ?? []).map(row),
|
|
113
|
+
ai: v.ai ?? [],
|
|
114
|
+
conflicts: v.conflicts ?? [],
|
|
115
|
+
}
|
|
116
|
+
if (v.placeholders) out.placeholders = v.placeholders
|
|
117
|
+
if (v.stateFamilies) out.stateFamilies = v.stateFamilies
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── the lock ─────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
const readLock = () => (existsSync(LOCK) ? JSON.parse(readFileSync(LOCK, 'utf8')) : null)
|
|
124
|
+
const sourcesOf = (lock) => (lock ? Object.fromEntries(Object.values(lock.files).map((f) => [f.family, f.design])) : DEFAULT_SOURCES)
|
|
125
|
+
|
|
126
|
+
function extract(sources) {
|
|
127
|
+
const present = new Set(readdirSync(DESIGN))
|
|
128
|
+
const out = {}
|
|
129
|
+
for (const [id, file] of Object.entries(sources)) {
|
|
130
|
+
if (!present.has(file)) throw new Error(`${file} is not in ${DESIGN}`)
|
|
131
|
+
const design = readFileSync(join(DESIGN, file), 'utf8')
|
|
132
|
+
out[`${themeName()}-${id}.json`] = { family: id, design: file, designSha256: sha256(design), json: JSON.stringify(family(renderVals(design, file)), null, 2) + '\n' }
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function lockDoc(files) {
|
|
138
|
+
return {
|
|
139
|
+
$comment: 'Generated by `tanqory-theme contracts`. Do not edit — neither this file nor the locks beside it. Re-lock from the approved design package with `tanqory-theme contracts --design <dir> --write`.',
|
|
140
|
+
generator: '@tanqory/theme-compiler contracts',
|
|
141
|
+
files: Object.fromEntries(
|
|
142
|
+
Object.entries(files)
|
|
143
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
144
|
+
.map(([name, f]) => [name, { family: f.family, design: f.design, designSha256: f.designSha256, sha256: sha256(f.json) }]),
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let failures = 0
|
|
150
|
+
const fail = (msg) => {
|
|
151
|
+
console.error(`✗ ${msg}`)
|
|
152
|
+
failures++
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (WRITE) {
|
|
156
|
+
if (!DESIGN) {
|
|
157
|
+
console.error('usage: tanqory-theme contracts --design <dir> --write')
|
|
158
|
+
process.exit(2)
|
|
159
|
+
}
|
|
160
|
+
const files = extract(sourcesOf(readLock()))
|
|
161
|
+
mkdirSync(OUT, { recursive: true })
|
|
162
|
+
for (const [name, f] of Object.entries(files)) writeFileSync(join(OUT, name), f.json)
|
|
163
|
+
writeFileSync(LOCK, JSON.stringify(lockDoc(files), null, 2) + '\n')
|
|
164
|
+
console.log(`✓ .tanqory/contracts — ${Object.keys(files).length} locks written from ${DESIGN}`)
|
|
165
|
+
} else {
|
|
166
|
+
const lock = readLock()
|
|
167
|
+
if (!lock) {
|
|
168
|
+
console.error('✗ .tanqory/contracts/LOCK.json is missing — run `tanqory-theme contracts --design <dir> --write`')
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
// 1. No hand edits: every lock matches the hash the compiler recorded.
|
|
172
|
+
const onDisk = new Set(readdirSync(OUT).filter((f) => f.endsWith('.json') && f !== 'LOCK.json'))
|
|
173
|
+
for (const [name, f] of Object.entries(lock.files)) {
|
|
174
|
+
if (!onDisk.has(name)) fail(`${name} is missing`)
|
|
175
|
+
else if (sha256(readFileSync(join(OUT, name), 'utf8')) !== f.sha256) fail(`${name} was edited by hand — locks are compiler-managed`)
|
|
176
|
+
onDisk.delete(name)
|
|
177
|
+
}
|
|
178
|
+
for (const name of onDisk) fail(`${name} is not in LOCK.json — locks are compiler-managed`)
|
|
179
|
+
// 2. With the design package at hand: the locks still say what it says.
|
|
180
|
+
if (DESIGN) {
|
|
181
|
+
const files = extract(sourcesOf(lock))
|
|
182
|
+
for (const [name, f] of Object.entries(files)) {
|
|
183
|
+
if (lock.files[name]?.sha256 !== sha256(f.json)) fail(`${name} no longer matches ${f.design} — re-lock with --write`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (failures) process.exit(1)
|
|
187
|
+
console.log(`✓ .tanqory/contracts — ${Object.keys(lock.files).length} locks intact${DESIGN ? ' and match the design package' : ''}`)
|
|
188
|
+
}
|