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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A theme's approved contracts — the frozen persisted ids (sections, blocks,
|
|
3
|
+
* settings, options) with their editor labels and presets. Kept by the theme
|
|
4
|
+
* as locks in .tanqory/contracts/, written by `tanqory-theme contracts`
|
|
5
|
+
* (./extract.mjs) with their hashes in LOCK.json; never hand-edited.
|
|
6
|
+
*/
|
|
7
|
+
// The theme's frozen contracts (.tanqory/contracts/<theme>-<family>.json), read
|
|
8
|
+
// from the theme being built — Vite resolves a leading '/' against its root.
|
|
9
|
+
// LOCK.json sits beside them; it is not a family and the filter below skips it.
|
|
10
|
+
const LOCKS = import.meta.glob('/.tanqory/contracts/*.json', { eager: true, import: 'default' }) as Record<string, unknown>
|
|
11
|
+
|
|
12
|
+
export type ContractStatus =
|
|
13
|
+
| 'EXISTING'
|
|
14
|
+
| 'CHANGED'
|
|
15
|
+
| 'NEW'
|
|
16
|
+
| 'LEGACY-HIDDEN'
|
|
17
|
+
| 'NEEDS NEW FIELD TYPE'
|
|
18
|
+
| 'NEEDS NEW DATA SOURCE'
|
|
19
|
+
| 'DATA REQUIREMENT'
|
|
20
|
+
| 'PROPOSED'
|
|
21
|
+
| (string & {})
|
|
22
|
+
|
|
23
|
+
export interface ContractRow {
|
|
24
|
+
name: string
|
|
25
|
+
label: string
|
|
26
|
+
control: string
|
|
27
|
+
options: string[]
|
|
28
|
+
optionLabels: string[]
|
|
29
|
+
legacyOptions: string[]
|
|
30
|
+
legacyOptionLabels: string[]
|
|
31
|
+
def: string | number | boolean | null
|
|
32
|
+
min: number | null
|
|
33
|
+
max: number | null
|
|
34
|
+
step: number | null
|
|
35
|
+
resp: string
|
|
36
|
+
dep: string
|
|
37
|
+
desc: string
|
|
38
|
+
status: ContractStatus
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ContractSection {
|
|
42
|
+
name: string
|
|
43
|
+
label: string
|
|
44
|
+
status: ContractStatus
|
|
45
|
+
rows: ContractRow[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ContractBlock {
|
|
49
|
+
name: string
|
|
50
|
+
label: string
|
|
51
|
+
parents: string[]
|
|
52
|
+
min: number
|
|
53
|
+
recommended: number
|
|
54
|
+
max: number
|
|
55
|
+
status: ContractStatus
|
|
56
|
+
resp: string
|
|
57
|
+
empty: string
|
|
58
|
+
summary: string
|
|
59
|
+
rows: ContractRow[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ContractPreset {
|
|
63
|
+
name: string
|
|
64
|
+
label: string
|
|
65
|
+
section: string
|
|
66
|
+
purpose: string
|
|
67
|
+
settings: Record<string, unknown>
|
|
68
|
+
blocks: { type: string; settings: Record<string, unknown> }[]
|
|
69
|
+
content: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type Applicability = 'APPLIES' | 'NOT APPLICABLE' | 'OVERRIDDEN BY SECTION-SPECIFIC BEHAVIOR' | (string & {})
|
|
73
|
+
|
|
74
|
+
export interface ContractFamily {
|
|
75
|
+
sections: ContractSection[]
|
|
76
|
+
blocks: ContractBlock[]
|
|
77
|
+
presets: ContractPreset[]
|
|
78
|
+
shared: ContractRow[]
|
|
79
|
+
sharedApplicability: Record<string, string>[]
|
|
80
|
+
globals: ContractRow[]
|
|
81
|
+
ai: Record<string, unknown>[]
|
|
82
|
+
conflicts: string[]
|
|
83
|
+
placeholders?: { id: string; use: string; viewBox: string; aspect: string; tokens: string[]; density: string; a11y: string }[]
|
|
84
|
+
stateFamilies?: string[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Every section contract family the theme ships a lock for, by family id (the
|
|
89
|
+
* `<family>` in the file name). Other locks (design tokens, page specs) share
|
|
90
|
+
* the folder and are skipped: a family is one that declares sections.
|
|
91
|
+
*/
|
|
92
|
+
export const FAMILIES: Readonly<Record<string, ContractFamily>> = Object.fromEntries(
|
|
93
|
+
Object.entries(LOCKS)
|
|
94
|
+
.filter(([, v]) => Array.isArray((v as Partial<ContractFamily>)?.sections) && Array.isArray((v as Partial<ContractFamily>)?.blocks))
|
|
95
|
+
.map(([k, v]) => [k.match(/-([^-/]+)\.json$/)?.[1] ?? k, v as ContractFamily]),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
export type FamilyId = keyof typeof FAMILIES
|
|
99
|
+
|
|
100
|
+
export function contractSection(family: FamilyId, name: string): ContractSection | undefined {
|
|
101
|
+
return FAMILIES[family].sections.find((s) => s.name === name)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Blocks are defined once in the family that owns them, but a block may list
|
|
106
|
+
* parents in other families (heading is a 12D block that group, a 12A
|
|
107
|
+
* section, accepts). Search every family.
|
|
108
|
+
*/
|
|
109
|
+
export function contractBlock(name: string, parent?: string): { family: FamilyId; block: ContractBlock } | undefined {
|
|
110
|
+
for (const [family, f] of Object.entries(FAMILIES) as [FamilyId, ContractFamily][]) {
|
|
111
|
+
const block = f.blocks.find((b) => b.name === name && (!parent || b.parents.includes(parent)))
|
|
112
|
+
if (block) return { family, block }
|
|
113
|
+
}
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function presetsFor(section: string): ContractPreset[] {
|
|
118
|
+
return Object.values(FAMILIES).flatMap((f) => f.presets.filter((p) => p.section === section))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The contract row for a setting, falling back to the family's shared row. */
|
|
122
|
+
export function contractRow(family: FamilyId, owner: { kind: 'section' | 'block'; name: string; parent?: string }, key: string): ContractRow | undefined {
|
|
123
|
+
if (owner.kind === 'block') return contractBlock(owner.name, owner.parent)?.block.rows.find((r) => r.name === key)
|
|
124
|
+
return contractSection(family, owner.name)?.rows.find((r) => r.name === key) ?? FAMILIES[family].shared.find((r) => r.name === key)
|
|
125
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design → Code contract validation.
|
|
3
|
+
*
|
|
4
|
+
* Compares the code's schemas (what components, the adapter and Storybook use)
|
|
5
|
+
* with the approved contracts (generated/*.json). Every approved setting must
|
|
6
|
+
* be represented by the component API or intentionally handled by the adapter.
|
|
7
|
+
*
|
|
8
|
+
* Issue codes — each is a way the two have drifted apart:
|
|
9
|
+
*
|
|
10
|
+
* missing-section an in-scope contract section has no schema
|
|
11
|
+
* missing-block a contract block an in-scope section accepts has no schema
|
|
12
|
+
* missing-prop a contract row has no field
|
|
13
|
+
* extra-prop a field has no contract row and no stated `extension` reason
|
|
14
|
+
* control-mismatch the field's control differs from the contract's
|
|
15
|
+
* unknown-option the field offers an option the contract does not have
|
|
16
|
+
* missing-option the contract offers an option the field does not have
|
|
17
|
+
* invalid-default the field default differs from the contract default
|
|
18
|
+
* invalid-range range min/max/step differ from the contract
|
|
19
|
+
* invalid-preset a preset names an unknown setting, or a value that is not an option
|
|
20
|
+
* invalid-block a preset / parent names a block the section does not accept
|
|
21
|
+
* duplicated-setting two visible fields control the same semantic
|
|
22
|
+
* unsupported-legacy a LEGACY-HIDDEN row / legacy option is not handled by the field
|
|
23
|
+
* shared-applicability a shared setting's APPLIES / NOT APPLICABLE status is not honoured
|
|
24
|
+
*/
|
|
25
|
+
import { FAMILIES, contractBlock, presetsFor, type ContractRow, type FamilyId } from './index'
|
|
26
|
+
import type { Field, Schema } from '@tanqory/theme-kit'
|
|
27
|
+
|
|
28
|
+
export type IssueCode =
|
|
29
|
+
| 'missing-section'
|
|
30
|
+
| 'missing-block'
|
|
31
|
+
| 'missing-prop'
|
|
32
|
+
| 'extra-prop'
|
|
33
|
+
| 'control-mismatch'
|
|
34
|
+
| 'unknown-option'
|
|
35
|
+
| 'missing-option'
|
|
36
|
+
| 'invalid-default'
|
|
37
|
+
| 'invalid-range'
|
|
38
|
+
| 'invalid-preset'
|
|
39
|
+
| 'invalid-block'
|
|
40
|
+
| 'duplicated-setting'
|
|
41
|
+
| 'unsupported-legacy'
|
|
42
|
+
| 'shared-applicability'
|
|
43
|
+
|
|
44
|
+
export interface ContractIssue {
|
|
45
|
+
readonly code: IssueCode
|
|
46
|
+
/** `section:header` · `block:heading@group` · `preset:footer-full` */
|
|
47
|
+
readonly where: string
|
|
48
|
+
readonly key?: string
|
|
49
|
+
readonly message: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A difference the design package itself flags as open, recorded with the
|
|
54
|
+
* reason it is accepted. The validator reports these as `accepted`, not as
|
|
55
|
+
* failures, so every exception is visible and reviewed.
|
|
56
|
+
*/
|
|
57
|
+
export interface AcceptedDeviation {
|
|
58
|
+
readonly code: IssueCode
|
|
59
|
+
readonly where: string
|
|
60
|
+
readonly key?: string
|
|
61
|
+
readonly reason: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ValidationReport {
|
|
65
|
+
readonly issues: readonly ContractIssue[]
|
|
66
|
+
readonly accepted: readonly (ContractIssue & { reason: string })[]
|
|
67
|
+
/** Rows checked, for the report header. */
|
|
68
|
+
readonly checked: { sections: number; blocks: number; rows: number; presets: number }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Contract controls that the code carries with a different (but equivalent)
|
|
73
|
+
* control. Only the vocabulary gap is mapped here — never a semantic change.
|
|
74
|
+
*/
|
|
75
|
+
const CONTROL_EQUIVALENTS: Record<string, readonly string[]> = {
|
|
76
|
+
text_alignment: ['select'],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sameControl(contract: string, code: string): boolean {
|
|
80
|
+
return contract === code || (CONTROL_EQUIVALENTS[contract]?.includes(code) ?? false)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function eqDefault(row: ContractRow, f: Field): boolean {
|
|
84
|
+
const d = row.def
|
|
85
|
+
if (f.control === 'boolean') return (d === true || d === 'true') === f.default
|
|
86
|
+
if (f.control === 'range' || f.control === 'number') return d == null ? true : Number(d) === f.default
|
|
87
|
+
if (f.control === 'select') return String(d ?? '') === f.default
|
|
88
|
+
return String(d ?? '') === String(f.default ?? '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compareRow(where: string, row: ContractRow, f: Field | undefined, out: ContractIssue[]): void {
|
|
92
|
+
const key = row.name
|
|
93
|
+
if (!f) {
|
|
94
|
+
out.push({ code: 'missing-prop', where, key, message: `contract row "${key}" (${row.control}, ${row.status}) has no field` })
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (!sameControl(row.control, f.control)) {
|
|
98
|
+
out.push({ code: 'control-mismatch', where, key, message: `control is "${f.control}", contract says "${row.control}"` })
|
|
99
|
+
}
|
|
100
|
+
if (row.status === 'LEGACY-HIDDEN' && !f.legacyHidden) {
|
|
101
|
+
out.push({ code: 'unsupported-legacy', where, key, message: 'contract marks this LEGACY-HIDDEN; the field is not legacyHidden' })
|
|
102
|
+
}
|
|
103
|
+
if (row.status !== 'LEGACY-HIDDEN' && f.legacyHidden) {
|
|
104
|
+
out.push({ code: 'unsupported-legacy', where, key, message: `field is legacyHidden but the contract status is ${row.status}` })
|
|
105
|
+
}
|
|
106
|
+
if (f.control === 'select') {
|
|
107
|
+
const opts = f.options as readonly string[]
|
|
108
|
+
for (const o of opts) if (!row.options.includes(o)) out.push({ code: 'unknown-option', where, key, message: `option "${o}" is not in the contract (${row.options.join(' · ')})` })
|
|
109
|
+
for (const o of row.options) if (!opts.includes(o)) out.push({ code: 'missing-option', where, key, message: `contract option "${o}" is missing` })
|
|
110
|
+
for (const o of row.legacyOptions) {
|
|
111
|
+
if (!f.legacy || !(o in f.legacy)) out.push({ code: 'unsupported-legacy', where, key, message: `legacy value "${o}" is not mapped by the field` })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (f.control === 'range' || f.control === 'number') {
|
|
115
|
+
if ((row.min != null && row.min !== f.min) || (row.max != null && row.max !== f.max) || (row.step != null && row.step !== f.step)) {
|
|
116
|
+
out.push({ code: 'invalid-range', where, key, message: `range ${f.min}–${f.max}/${f.step}, contract ${row.min}–${row.max}/${row.step}` })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!eqDefault(row, f)) {
|
|
120
|
+
out.push({ code: 'invalid-default', where, key, message: `default is ${JSON.stringify(f.default)}, contract says ${JSON.stringify(row.def)}` })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function compareFields(where: string, rows: readonly ContractRow[], schema: Schema, out: ContractIssue[], sharedKeys: ReadonlySet<string> = new Set()): number {
|
|
125
|
+
const seen = new Set<string>(sharedKeys)
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
// A row whose control is "—" is a NEEDS NEW DATA SOURCE / PROPOSED marker, not a setting.
|
|
128
|
+
if (row.control === '—' || row.control === '') continue
|
|
129
|
+
seen.add(row.name)
|
|
130
|
+
compareRow(where, row, schema.fields[row.name], out)
|
|
131
|
+
}
|
|
132
|
+
for (const [key, f] of Object.entries(schema.fields)) {
|
|
133
|
+
if (!seen.has(key) && !f.extension) out.push({ code: 'extra-prop', where, key, message: 'field has no contract row and no `extension` reason' })
|
|
134
|
+
}
|
|
135
|
+
// Two visible fields that control the same thing make a merchant choose twice.
|
|
136
|
+
const bySemantic = new Map<string, string[]>()
|
|
137
|
+
for (const [key, f] of Object.entries(schema.fields)) {
|
|
138
|
+
if (f.legacyHidden) continue
|
|
139
|
+
const s = f.semantic ?? key
|
|
140
|
+
bySemantic.set(s, [...(bySemantic.get(s) ?? []), key])
|
|
141
|
+
}
|
|
142
|
+
for (const [s, keys] of bySemantic) {
|
|
143
|
+
if (keys.length > 1) out.push({ code: 'duplicated-setting', where, key: keys.join(' + '), message: `visible fields ${keys.join(', ')} all control "${s}"` })
|
|
144
|
+
}
|
|
145
|
+
return seen.size - [...sharedKeys].filter((k) => !rows.some((r) => r.name === k)).length
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The shared Nova 12 vocabulary is defined ONCE, in 12A ("00 · Shared Nova 12
|
|
150
|
+
* vocabulary"); later families list only their applicability. A family with
|
|
151
|
+
* no shared rows of its own reads 12A's.
|
|
152
|
+
*/
|
|
153
|
+
function sharedRowsOf(fam: FamilyId): readonly ContractRow[] {
|
|
154
|
+
return FAMILIES[fam].shared.length ? FAMILIES[fam].shared : FAMILIES['12A'].shared
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The block type a section accepts for a contract block name: the name itself,
|
|
159
|
+
* or the accepted block whose schema lists it in `legacyNames` (a block renamed
|
|
160
|
+
* after the contract was locked — the contract keeps the old name).
|
|
161
|
+
*/
|
|
162
|
+
function acceptedBlockFor(contractName: string, schema: Schema, blocks: ReadonlyMap<string, Schema>): string | undefined {
|
|
163
|
+
if (schema.blocks?.includes(contractName)) return contractName
|
|
164
|
+
return schema.blocks?.find((b) => (blocks.get(`${b}@${schema.name}`) ?? blocks.get(b))?.legacyNames?.includes(contractName))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function checkPreset(preset: ReturnType<typeof presetsFor>[number], schema: Schema, blocks: ReadonlyMap<string, Schema>, out: ContractIssue[]): void {
|
|
168
|
+
const where = `preset:${preset.name}`
|
|
169
|
+
for (const [key, value] of Object.entries(preset.settings)) {
|
|
170
|
+
const f = schema.fields[key]
|
|
171
|
+
if (!f) {
|
|
172
|
+
out.push({ code: 'invalid-preset', where, key, message: `sets "${key}", which ${schema.name} does not have` })
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (f.control === 'select' && !(f.options as readonly string[]).includes(String(value))) {
|
|
176
|
+
out.push({ code: 'invalid-preset', where, key, message: `value "${String(value)}" is not an option of ${schema.name}.${key}` })
|
|
177
|
+
}
|
|
178
|
+
if (f.control === 'boolean' && typeof value !== 'boolean') {
|
|
179
|
+
out.push({ code: 'invalid-preset', where, key, message: `value ${JSON.stringify(value)} is not a boolean` })
|
|
180
|
+
}
|
|
181
|
+
if (f.control === 'range' && (typeof value !== 'number' || value < (f as { min: number }).min || value > (f as { max: number }).max)) {
|
|
182
|
+
out.push({ code: 'invalid-preset', where, key, message: `value ${JSON.stringify(value)} is outside the range` })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const b of preset.blocks) {
|
|
186
|
+
const type = acceptedBlockFor(b.type, schema, blocks)
|
|
187
|
+
if (!type) {
|
|
188
|
+
out.push({ code: 'invalid-block', where, key: b.type, message: `block "${b.type}" is not accepted by ${schema.name}` })
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
const bs = blocks.get(`${type}@${schema.name}`) ?? blocks.get(type)
|
|
192
|
+
if (!bs) {
|
|
193
|
+
out.push({ code: 'invalid-block', where, key: b.type, message: `block "${b.type}" has no schema` })
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
for (const [key, value] of Object.entries(b.settings)) {
|
|
197
|
+
const f = bs.fields[key]
|
|
198
|
+
if (!f) out.push({ code: 'invalid-preset', where, key: `${b.type}.${key}`, message: `block setting "${key}" does not exist` })
|
|
199
|
+
else if (f.control === 'select' && !(f.options as readonly string[]).includes(String(value))) {
|
|
200
|
+
out.push({ code: 'invalid-preset', where, key: `${b.type}.${key}`, message: `value "${String(value)}" is not an option` })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ValidateOptions {
|
|
207
|
+
/** Design families whose sections must all be implemented. */
|
|
208
|
+
readonly families: readonly FamilyId[]
|
|
209
|
+
readonly sections: readonly Schema[]
|
|
210
|
+
readonly blocks: readonly Schema[]
|
|
211
|
+
/** Theme-setting groups (kind 'global') — compared with a family's globals rows. */
|
|
212
|
+
readonly globals?: readonly Schema[]
|
|
213
|
+
readonly accepted?: readonly AcceptedDeviation[]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function validateContracts(opts: ValidateOptions): ValidationReport {
|
|
217
|
+
const out: ContractIssue[] = []
|
|
218
|
+
const sectionByName = new Map(opts.sections.map((s) => [s.name, s]))
|
|
219
|
+
// A block schema is keyed by name, and by name@parent when one name has two
|
|
220
|
+
// definitions (image in 12D group vs image in 12F ugc-gallery).
|
|
221
|
+
const blockByKey = new Map<string, Schema>()
|
|
222
|
+
for (const b of opts.blocks) {
|
|
223
|
+
blockByKey.set(b.name, blockByKey.get(b.name) ?? b)
|
|
224
|
+
for (const p of b.parents ?? []) blockByKey.set(`${b.name}@${p}`, b)
|
|
225
|
+
}
|
|
226
|
+
let rows = 0
|
|
227
|
+
let presets = 0
|
|
228
|
+
let blockCount = 0
|
|
229
|
+
|
|
230
|
+
for (const fam of opts.families) {
|
|
231
|
+
const family = FAMILIES[fam]
|
|
232
|
+
// Global settings (Nova 13 popup): one global schema per family carries every globals row.
|
|
233
|
+
if (family.globals.length) {
|
|
234
|
+
const where = `global:${fam}`
|
|
235
|
+
const g = opts.globals?.find((s) => s.family === fam)
|
|
236
|
+
if (!g) out.push({ code: 'missing-section', where, message: `${fam} globals have no global schema` })
|
|
237
|
+
else {
|
|
238
|
+
rows += compareFields(where, family.globals, g, out)
|
|
239
|
+
for (const p of family.presets.filter((x) => x.section.startsWith('global:'))) {
|
|
240
|
+
presets++
|
|
241
|
+
checkPreset(p, g, blockByKey, out)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const cs of family.sections) {
|
|
246
|
+
const where = `section:${cs.name}`
|
|
247
|
+
const schema = sectionByName.get(cs.name)
|
|
248
|
+
if (!schema) {
|
|
249
|
+
out.push({ code: 'missing-section', where, message: `${fam} section "${cs.name}" has no schema` })
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
const sharedRows = sharedRowsOf(fam)
|
|
253
|
+
// A shared setting that APPLIES is a legitimate field even without a
|
|
254
|
+
// section row (12B+ carry no per-section shared rows).
|
|
255
|
+
const applies = new Set(family.sharedApplicability.filter((a) => a[cs.name] === 'APPLIES').map((a) => a.prop))
|
|
256
|
+
rows += compareFields(where, cs.rows, schema, out, applies)
|
|
257
|
+
|
|
258
|
+
// Shared settings: APPLIES must be present with the shared vocabulary;
|
|
259
|
+
// NOT APPLICABLE must be absent unless the section's own row declares it.
|
|
260
|
+
for (const app of family.sharedApplicability) {
|
|
261
|
+
const prop = app.prop
|
|
262
|
+
const status = app[cs.name]
|
|
263
|
+
if (!status) continue
|
|
264
|
+
const own = cs.rows.find((r) => r.name === prop)
|
|
265
|
+
const f = schema.fields[prop]
|
|
266
|
+
if (status === 'APPLIES' && !f) {
|
|
267
|
+
out.push({ code: 'shared-applicability', where, key: prop, message: 'shared setting APPLIES but the section has no field for it' })
|
|
268
|
+
}
|
|
269
|
+
if (status === 'APPLIES' && f && !own) {
|
|
270
|
+
const shared = sharedRows.find((r) => r.name === prop)
|
|
271
|
+
if (shared && shared.control && !sameControl(shared.control, f.control)) {
|
|
272
|
+
out.push({ code: 'control-mismatch', where, key: prop, message: `shared "${prop}" is a ${shared.control}; the field is a ${f.control}` })
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (status === 'APPLIES' && f?.control === 'select') {
|
|
276
|
+
const shared = sharedRows.find((r) => r.name === prop)
|
|
277
|
+
const opts2 = f.options as readonly string[]
|
|
278
|
+
if (shared && shared.options.some((o) => !opts2.includes(o))) {
|
|
279
|
+
out.push({ code: 'shared-applicability', where, key: prop, message: `does not use the shared vocabulary (${shared.options.join(' · ')})` })
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (status === 'NOT APPLICABLE' && f && !own) {
|
|
283
|
+
out.push({ code: 'shared-applicability', where, key: prop, message: 'shared setting is NOT APPLICABLE here but the section exposes it' })
|
|
284
|
+
}
|
|
285
|
+
if (status.startsWith('OVERRIDDEN') && f && !own) {
|
|
286
|
+
out.push({ code: 'duplicated-setting', where, key: prop, message: `shared "${prop}" is overridden by ${app[`${cs.name}Note`] || 'a section setting'}; exposing both duplicates it` })
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Every contract block that names this section as a parent must be accepted and implemented.
|
|
291
|
+
for (const f2 of Object.values(FAMILIES)) {
|
|
292
|
+
for (const cb of f2.blocks) {
|
|
293
|
+
if (!cb.parents.includes(cs.name)) continue
|
|
294
|
+
if (!acceptedBlockFor(cb.name, schema, blockByKey) && !schema.legacyBlocks?.includes(cb.name)) {
|
|
295
|
+
out.push({ code: 'invalid-block', where, key: cb.name, message: `contract block "${cb.name}" lists ${cs.name} as a parent but the section does not accept it` })
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const b of schema.blocks ?? []) {
|
|
300
|
+
const own = blockByKey.get(`${b}@${cs.name}`) ?? blockByKey.get(b)
|
|
301
|
+
const cb = contractBlock(b, cs.name) ?? (own?.legacyNames ?? []).map((n) => contractBlock(n, cs.name)).find(Boolean)
|
|
302
|
+
const where2 = `block:${b}@${cs.name}`
|
|
303
|
+
if (!cb) {
|
|
304
|
+
out.push({ code: 'invalid-block', where: where2, message: `section accepts "${b}" but no contract block lists ${cs.name} as a parent` })
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
const bs = blockByKey.get(`${b}@${cs.name}`) ?? blockByKey.get(b)
|
|
308
|
+
if (!bs) {
|
|
309
|
+
out.push({ code: 'missing-block', where: where2, message: `block "${b}" has no schema` })
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
blockCount++
|
|
313
|
+
rows += compareFields(where2, cb.block.rows, bs, out)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const p of presetsFor(cs.name)) {
|
|
317
|
+
presets++
|
|
318
|
+
checkPreset(p, schema, blockByKey, out)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const accepted: (ContractIssue & { reason: string })[] = []
|
|
324
|
+
const issues: ContractIssue[] = []
|
|
325
|
+
for (const i of out) {
|
|
326
|
+
const a = opts.accepted?.find((x) => x.code === i.code && x.where === i.where && (x.key === undefined || x.key === i.key))
|
|
327
|
+
if (a) accepted.push({ ...i, reason: a.reason })
|
|
328
|
+
else issues.push(i)
|
|
329
|
+
}
|
|
330
|
+
// Dedupe (a block reached from two parents reports once per parent — keep that; exact dupes go).
|
|
331
|
+
const uniq = (xs: ContractIssue[]) => xs.filter((x, i) => xs.findIndex((y) => y.code === x.code && y.where === x.where && y.key === x.key && y.message === x.message) === i)
|
|
332
|
+
return {
|
|
333
|
+
issues: uniq(issues),
|
|
334
|
+
accepted: uniq(accepted) as (ContractIssue & { reason: string })[],
|
|
335
|
+
checked: { sections: opts.families.reduce((n, f) => n + FAMILIES[f].sections.length, 0), blocks: blockCount, rows, presets },
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function formatReport(r: ValidationReport): string {
|
|
340
|
+
const lines = [
|
|
341
|
+
`Contract validation — ${r.checked.sections} sections · ${r.checked.blocks} block bindings · ${r.checked.rows} rows · ${r.checked.presets} presets`,
|
|
342
|
+
]
|
|
343
|
+
for (const i of r.issues) lines.push(` ✗ [${i.code}] ${i.where}${i.key ? ` · ${i.key}` : ''} — ${i.message}`)
|
|
344
|
+
for (const a of r.accepted) lines.push(` ~ [${a.code}] ${a.where}${a.key ? ` · ${a.key}` : ''} — accepted: ${a.reason}`)
|
|
345
|
+
lines.push(r.issues.length ? `✗ ${r.issues.length} issue(s)` : `✓ no drift (${r.accepted.length} accepted deviation(s))`)
|
|
346
|
+
return lines.join('\n')
|
|
347
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit theme.manifest.json — the single machine-readable catalog of this theme,
|
|
3
|
+
* read by the editor inserter, the AI theme generator (ai-api), and the Phase 1
|
|
4
|
+
* dashboard↔surface conformance test.
|
|
5
|
+
*
|
|
6
|
+
* It loads `manifest-entry.ts` through a Vite SSR server rather than plain Node
|
|
7
|
+
* so `import.meta.glob` resolves against the exact `sections/*.tsx` +
|
|
8
|
+
* `templates/*.json` the app registers — the manifest is generated from the
|
|
9
|
+
* same source of truth it describes, so it cannot drift.
|
|
10
|
+
*
|
|
11
|
+
* node scripts/gen-manifest.mjs [--check]
|
|
12
|
+
*
|
|
13
|
+
* --check regenerates in memory and fails (exit 1) if the committed
|
|
14
|
+
* theme.manifest.json is stale — for CI, so a new/edited section without a
|
|
15
|
+
* regenerated manifest is caught in review.
|
|
16
|
+
*/
|
|
17
|
+
import { createServer } from 'vite'
|
|
18
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
// The theme being built: the directory the command runs in.
|
|
24
|
+
const root = process.cwd()
|
|
25
|
+
// theme.manifest.json stays at the theme root: studio-api and the editor read
|
|
26
|
+
// it from a theme's files, and the runtime bundle imports it. It is committed,
|
|
27
|
+
// so a clean checkout builds without regenerating it first.
|
|
28
|
+
const outFile = join(root, 'theme.manifest.json')
|
|
29
|
+
const readmeFile = join(root, 'README.md')
|
|
30
|
+
const check = process.argv.includes('--check')
|
|
31
|
+
|
|
32
|
+
const CATALOG_START = '<!-- BEGIN GENERATED CATALOG -->'
|
|
33
|
+
const CATALOG_END = '<!-- END GENERATED CATALOG -->'
|
|
34
|
+
|
|
35
|
+
/** Render the human-readable catalog block spliced into README.md — a view of
|
|
36
|
+
* the same manifest, so the docs can't drift from what the theme ships. */
|
|
37
|
+
function renderCatalog(m) {
|
|
38
|
+
const byCat = {}
|
|
39
|
+
for (const s of m.sections) if (s.role !== 'block') (byCat[s.category] ??= []).push(s.name)
|
|
40
|
+
const blocks = m.sections.filter((s) => s.role === 'block').map((s) => s.name).sort()
|
|
41
|
+
const lines = []
|
|
42
|
+
lines.push(
|
|
43
|
+
`**${m.stats.sections} sections (${m.stats.headerFooterSections} in the header / footer zones) · ${m.stats.blocks} block types · ${m.stats.templates} templates · ${m.stats.settings} settings**`,
|
|
44
|
+
)
|
|
45
|
+
lines.push('')
|
|
46
|
+
lines.push('### Sections by category')
|
|
47
|
+
for (const cat of Object.keys(byCat).sort()) {
|
|
48
|
+
lines.push(`- **${cat}** (${byCat[cat].length}): ${byCat[cat].sort().join(', ')}`)
|
|
49
|
+
}
|
|
50
|
+
lines.push('')
|
|
51
|
+
lines.push(`### Blocks (${blocks.length} types, nested inside sections)`)
|
|
52
|
+
lines.push(`- ${blocks.join(', ')}`)
|
|
53
|
+
lines.push('')
|
|
54
|
+
lines.push('### Shared groups')
|
|
55
|
+
for (const g of m.groups ?? []) {
|
|
56
|
+
lines.push(`- **${g.name}** (${g.type}): ${g.sectionTypes.join(', ')} — used by ${g.usedBy.length} template(s)`)
|
|
57
|
+
}
|
|
58
|
+
lines.push('')
|
|
59
|
+
lines.push('### Templates')
|
|
60
|
+
lines.push('| template | header | footer | sections |')
|
|
61
|
+
lines.push('| --- | --- | --- | --- |')
|
|
62
|
+
const slot = (s) => (s.mode === 'ref' ? `↗ ${s.group}` : s.mode)
|
|
63
|
+
for (const t of m.templates)
|
|
64
|
+
lines.push(`| \`${t.name}\` | ${slot(t.groups.header)} | ${slot(t.groups.footer)} | ${t.sectionTypes.join(', ') || '—'} |`)
|
|
65
|
+
lines.push('')
|
|
66
|
+
lines.push('### Theme settings')
|
|
67
|
+
for (const g of m.settingsSchema) {
|
|
68
|
+
lines.push(`- **${g.group}**: ${g.items.map((i) => i.key).join(', ')}`)
|
|
69
|
+
}
|
|
70
|
+
return lines.join('\n')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Splice a freshly-rendered catalog between the markers. Returns the new
|
|
74
|
+
* README text, or throws if the markers are missing. */
|
|
75
|
+
function spliceReadme(current, catalog) {
|
|
76
|
+
const a = current.indexOf(CATALOG_START)
|
|
77
|
+
const b = current.indexOf(CATALOG_END)
|
|
78
|
+
if (a === -1 || b === -1 || b < a) {
|
|
79
|
+
throw new Error(`README.md is missing the ${CATALOG_START} / ${CATALOG_END} markers`)
|
|
80
|
+
}
|
|
81
|
+
return current.slice(0, a + CATALOG_START.length) + '\n' + catalog + '\n' + current.slice(b)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Print every integrity warning the manifest carries. */
|
|
85
|
+
function reportWarnings(w, log) {
|
|
86
|
+
for (const d of w.danglingTemplateRefs) log(`⚠ template ${d.template} → unknown section ${d.type}`)
|
|
87
|
+
for (const d of w.danglingGroupRefs ?? []) log(`⚠ template ${d.template} → unknown group ${d.group} (groups/${d.group}.json)`)
|
|
88
|
+
for (const u of w.unknownTemplateSettings ?? [])
|
|
89
|
+
log(`⚠ template ${u.template} → ${u.type} sets '${u.setting}', which that section does not declare`)
|
|
90
|
+
for (const b of w.disallowedTemplateBlocks ?? [])
|
|
91
|
+
log(`⚠ template ${b.template} → ${b.parent} nests '${b.type}', not in its allowedBlocks`)
|
|
92
|
+
for (const k of w.settingsUndeclared) log(`⚠ settings.json key '${k}' has no schema entry`)
|
|
93
|
+
for (const k of w.settingsMissingValue) log(`⚠ schema key '${k}' has no value in settings.json`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Drift that must fail CI (content that cannot work as written). */
|
|
97
|
+
function countDrift(w) {
|
|
98
|
+
return (
|
|
99
|
+
w.danglingTemplateRefs.length +
|
|
100
|
+
(w.danglingGroupRefs ?? []).length +
|
|
101
|
+
(w.unknownTemplateSettings ?? []).length +
|
|
102
|
+
(w.disallowedTemplateBlocks ?? []).length
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const server = await createServer({
|
|
107
|
+
root,
|
|
108
|
+
logLevel: 'error',
|
|
109
|
+
server: { middlewareMode: true },
|
|
110
|
+
// The section files import CSS/assets; Vite SSR stubs those, so importing a
|
|
111
|
+
// section to read its static definition is safe.
|
|
112
|
+
appType: 'custom',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const mod = await server.ssrLoadModule(join(here, 'manifest-entry.ts'))
|
|
117
|
+
const manifest = mod.buildManifest()
|
|
118
|
+
const json = JSON.stringify(manifest, null, 2) + '\n'
|
|
119
|
+
|
|
120
|
+
const newReadme = spliceReadme(readFileSync(readmeFile, 'utf8'), renderCatalog(manifest))
|
|
121
|
+
|
|
122
|
+
if (check) {
|
|
123
|
+
const curJson = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
|
|
124
|
+
const curReadme = readFileSync(readmeFile, 'utf8')
|
|
125
|
+
const staleJson = curJson !== json
|
|
126
|
+
const staleReadme = curReadme !== newReadme
|
|
127
|
+
// Template drift is a CI failure, not a warning: a template that sets a
|
|
128
|
+
// setting the section does not declare, nests a block the parent forbids,
|
|
129
|
+
// or points at a section that no longer exists is broken content shipped
|
|
130
|
+
// as if it worked.
|
|
131
|
+
const drift = countDrift(manifest.warnings)
|
|
132
|
+
if (drift > 0) {
|
|
133
|
+
reportWarnings(manifest.warnings, console.error)
|
|
134
|
+
console.error(`✗ ${drift} template/settings drift problem(s). Fix the template or declare the setting.`)
|
|
135
|
+
process.exitCode = 1
|
|
136
|
+
}
|
|
137
|
+
if (staleJson || staleReadme) {
|
|
138
|
+
const what = [staleJson && 'theme.manifest.json', staleReadme && 'README.md catalog']
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(' + ')
|
|
141
|
+
console.error(`✗ ${what} stale. Run \`tanqory-theme manifest\` and commit the result.`)
|
|
142
|
+
process.exitCode = 1
|
|
143
|
+
} else {
|
|
144
|
+
console.log('✓ theme.manifest.json + README catalog are up to date.')
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
mkdirSync(dirname(outFile), { recursive: true })
|
|
148
|
+
writeFileSync(outFile, json)
|
|
149
|
+
writeFileSync(readmeFile, newReadme)
|
|
150
|
+
const { sections, blocks, templates } = manifest.stats
|
|
151
|
+
console.log(`✓ theme.manifest.json + README catalog — ${sections} sections, ${blocks} block types, ${templates} templates`)
|
|
152
|
+
reportWarnings(manifest.warnings, console.warn)
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
await server.close()
|
|
156
|
+
}
|