@unbox-plus/cli 0.20.4
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.md +46 -0
- package/README.md +175 -0
- package/bin/cli.js +439 -0
- package/package.json +43 -0
- package/src/colors.js +100 -0
- package/src/presets.js +241 -0
- package/src/theme.js +253 -0
- package/template/.claude/agents/assets-marca.md +40 -0
- package/template/.claude/agents/avaliador-visual.md +48 -0
- package/template/.claude/agents/branding-briefing.md +401 -0
- package/template/.claude/agents/conteudo-secao.md +46 -0
- package/template/.claude/agents/paginas-legais.md +44 -0
- package/template/.claude/settings.json +3 -0
- package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +55 -0
- package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
- package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
- package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
- package/template/.env.example +158 -0
- package/template/.mcp.json.example +14 -0
- package/template/.nvmrc +1 -0
- package/template/CLAUDE.md +333 -0
- package/template/COVERAGE.md +80 -0
- package/template/DEPLOY.md +179 -0
- package/template/QA.md +108 -0
- package/template/README.md +152 -0
- package/template/agents/CONSTRUCAO.md +207 -0
- package/template/agents/MANAGER.md +192 -0
- package/template/agents/PADROES.md +255 -0
- package/template/agents/definitions/00-scaffold.md +56 -0
- package/template/agents/definitions/01-layout.md +47 -0
- package/template/agents/definitions/02-homepage.md +70 -0
- package/template/agents/definitions/03-catalog.md +86 -0
- package/template/agents/definitions/04-pdp.md +97 -0
- package/template/agents/definitions/05-cart.md +116 -0
- package/template/agents/definitions/06-checkout.md +146 -0
- package/template/agents/definitions/07-auth.md +130 -0
- package/template/agents/definitions/08-customer.md +55 -0
- package/template/agents/definitions/09-promotions.md +62 -0
- package/template/agents/definitions/10-feedback.md +128 -0
- package/template/agents/definitions/11-seo-infra.md +159 -0
- package/template/agents/definitions/12-deploy.md +173 -0
- package/template/agents/definitions/13-cro.md +275 -0
- package/template/agents/definitions/14-seo.md +388 -0
- package/template/agents/definitions/15-branding.md +26 -0
- package/template/agents/definitions/16-qa-visual.md +138 -0
- package/template/agents/definitions/17-aeo.md +257 -0
- package/template/app/(loja)/busca/page.tsx +72 -0
- package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
- package/template/app/(loja)/carrinho/page.tsx +160 -0
- package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
- package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
- package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
- package/template/app/(loja)/checkout/page.tsx +80 -0
- package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
- package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
- package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
- package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
- package/template/app/(loja)/conta/entrar/page.tsx +119 -0
- package/template/app/(loja)/conta/page.tsx +45 -0
- package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
- package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
- package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
- package/template/app/(loja)/devolucoes/page.tsx +36 -0
- package/template/app/(loja)/layout.tsx +22 -0
- package/template/app/(loja)/oferta/page.tsx +57 -0
- package/template/app/(loja)/page.tsx +86 -0
- package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
- package/template/app/(loja)/privacidade/page.tsx +113 -0
- package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
- package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
- package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
- package/template/app/(loja)/produtos/loading.tsx +16 -0
- package/template/app/(loja)/produtos/page.tsx +33 -0
- package/template/app/(loja)/termos/page.tsx +104 -0
- package/template/app/acesso/page.tsx +129 -0
- package/template/app/api/account/addresses/route.ts +39 -0
- package/template/app/api/account/exists/route.ts +21 -0
- package/template/app/api/account/me/route.ts +25 -0
- package/template/app/api/account/otp/route.ts +26 -0
- package/template/app/api/account/preferences/route.ts +19 -0
- package/template/app/api/account/signin/route.ts +27 -0
- package/template/app/api/account/signout/route.ts +9 -0
- package/template/app/api/acesso/route.ts +171 -0
- package/template/app/api/capi/route.ts +34 -0
- package/template/app/api/cart/coupon/route.ts +49 -0
- package/template/app/api/cart/items/route.ts +45 -0
- package/template/app/api/cart/link/route.ts +69 -0
- package/template/app/api/cart/route.ts +113 -0
- package/template/app/api/cart/share/route.ts +21 -0
- package/template/app/api/cep/[code]/route.ts +18 -0
- package/template/app/api/checkout/address/route.ts +24 -0
- package/template/app/api/checkout/email/route.ts +41 -0
- package/template/app/api/checkout/installments/route.ts +16 -0
- package/template/app/api/checkout/route.ts +110 -0
- package/template/app/api/checkout/shipping/route.ts +65 -0
- package/template/app/api/checkout-destination/route.ts +31 -0
- package/template/app/api/order/[ref]/route.ts +17 -0
- package/template/app/api/payment-link/route.ts +28 -0
- package/template/app/api/revalidate/route.ts +52 -0
- package/template/app/api/shipping/quote/route.ts +29 -0
- package/template/app/api/subscriptions/[id]/route.ts +52 -0
- package/template/app/api/track/route.ts +28 -0
- package/template/app/api/unbox/catalogo/route.ts +136 -0
- package/template/app/api/unbox/paginas/route.ts +167 -0
- package/template/app/api/unbox/vitrine/route.ts +84 -0
- package/template/app/api/webhooks/unbox/route.ts +97 -0
- package/template/app/apple-icon.svg +5 -0
- package/template/app/error.tsx +15 -0
- package/template/app/globals.css +457 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +90 -0
- package/template/app/llms.txt/route.ts +43 -0
- package/template/app/manifest.ts +21 -0
- package/template/app/not-found.tsx +18 -0
- package/template/app/opengraph-image.tsx +35 -0
- package/template/app/robots.ts +31 -0
- package/template/app/sitemap.ts +42 -0
- package/template/bootstrap.sh +87 -0
- package/template/components/account/account-shell.tsx +47 -0
- package/template/components/account/address-book.tsx +147 -0
- package/template/components/account/preferences-form.tsx +77 -0
- package/template/components/account/reorder-button.tsx +47 -0
- package/template/components/account/signout-button.tsx +20 -0
- package/template/components/account/subscription-actions.tsx +204 -0
- package/template/components/account-nav.tsx +41 -0
- package/template/components/address-fields.tsx +102 -0
- package/template/components/analytics/data-layer-ready.tsx +16 -0
- package/template/components/analytics/purchase-tracker.tsx +32 -0
- package/template/components/brand-search.tsx +34 -0
- package/template/components/cart/cart-provider.tsx +340 -0
- package/template/components/cart/mini-cart.tsx +228 -0
- package/template/components/cart-button.tsx +35 -0
- package/template/components/catalog/catalog-client.tsx +523 -0
- package/template/components/catalog/grid-skeleton.tsx +19 -0
- package/template/components/catalog/pager.tsx +66 -0
- package/template/components/catalog/product-grid-card.tsx +52 -0
- package/template/components/catalog/product-grid.tsx +27 -0
- package/template/components/checkout/checkout-client.tsx +1333 -0
- package/template/components/chrome/announce-bar.tsx +57 -0
- package/template/components/chrome/chrome-recipe.ts +13 -0
- package/template/components/chrome/footers/captura-botao.tsx +28 -0
- package/template/components/chrome/footers/colunas.tsx +110 -0
- package/template/components/chrome/footers/conversao.tsx +90 -0
- package/template/components/chrome/footers/editorial.tsx +98 -0
- package/template/components/chrome/footers/minimal.tsx +55 -0
- package/template/components/chrome/header-bar-mobile.tsx +64 -0
- package/template/components/chrome/headers/centralizado.tsx +71 -0
- package/template/components/chrome/headers/classico.tsx +64 -0
- package/template/components/chrome/headers/compacto.tsx +66 -0
- package/template/components/chrome/headers/equilibrado.tsx +73 -0
- package/template/components/chrome/headers/imersivo.tsx +62 -0
- package/template/components/chrome/registry.ts +70 -0
- package/template/components/chrome/solid-on-scroll.tsx +39 -0
- package/template/components/empty-state.tsx +22 -0
- package/template/components/home/combo-card-compact.tsx +67 -0
- package/template/components/home/combos-home.tsx +64 -0
- package/template/components/home/combos-section.tsx +177 -0
- package/template/components/home/home-recipe.ts +41 -0
- package/template/components/home/sections/attributes-marquee.tsx +178 -0
- package/template/components/home/sections/benefits.tsx +80 -0
- package/template/components/home/sections/bloco-html.tsx +47 -0
- package/template/components/home/sections/catalogo.ts +165 -0
- package/template/components/home/sections/category-pills.tsx +32 -0
- package/template/components/home/sections/combos-carousel.tsx +141 -0
- package/template/components/home/sections/comparison.tsx +73 -0
- package/template/components/home/sections/founder-story.tsx +40 -0
- package/template/components/home/sections/hero.tsx +187 -0
- package/template/components/home/sections/kits.tsx +11 -0
- package/template/components/home/sections/media-cards.tsx +70 -0
- package/template/components/home/sections/newsletter.tsx +62 -0
- package/template/components/home/sections/product-showcase.tsx +175 -0
- package/template/components/home/sections/purchase-hero.tsx +187 -0
- package/template/components/home/sections/quote-banner.tsx +50 -0
- package/template/components/home/sections/registry.ts +186 -0
- package/template/components/home/sections/reviews-carousel.tsx +66 -0
- package/template/components/home/sections/reviews.tsx +68 -0
- package/template/components/home/sections/ritual.tsx +96 -0
- package/template/components/home/sections/savings.tsx +47 -0
- package/template/components/home/sections/social-row.tsx +65 -0
- package/template/components/home/sections/spec-table.tsx +117 -0
- package/template/components/home/sections/stars.tsx +12 -0
- package/template/components/home/sections/stats-grid.tsx +54 -0
- package/template/components/home/sections/trust-bar.tsx +56 -0
- package/template/components/home/sections/trust-strip.tsx +40 -0
- package/template/components/home/sections/video-wall.tsx +59 -0
- package/template/components/landing/landing-recipe.ts +20 -0
- package/template/components/landing/oferta-sections.tsx +40 -0
- package/template/components/landing/product-picker.tsx +257 -0
- package/template/components/mobile-nav.tsx +71 -0
- package/template/components/order-status.tsx +163 -0
- package/template/components/powered-by-unbox.tsx +27 -0
- package/template/components/product/pdp/buy-box.tsx +604 -0
- package/template/components/product/pdp/catalog-grid.tsx +113 -0
- package/template/components/product/pdp/faq-modelo.ts +86 -0
- package/template/components/product/pdp/gallery.tsx +87 -0
- package/template/components/product/pdp/interactive.tsx +200 -0
- package/template/components/product/pdp/newsletter.tsx +64 -0
- package/template/components/product/pdp/payment-chips.tsx +22 -0
- package/template/components/product/pdp/pdp-view.tsx +227 -0
- package/template/components/product/pdp/recommendations.tsx +144 -0
- package/template/components/product/pdp/sections.tsx +173 -0
- package/template/components/quantity-stepper.tsx +36 -0
- package/template/components/search-box.tsx +37 -0
- package/template/components/site-footer.tsx +59 -0
- package/template/components/site-header.tsx +64 -0
- package/template/components/ui/accordion.tsx +72 -0
- package/template/components/ui/alert-dialog.tsx +187 -0
- package/template/components/ui/alert.tsx +76 -0
- package/template/components/ui/aspect-ratio.tsx +22 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +52 -0
- package/template/components/ui/breadcrumb.tsx +125 -0
- package/template/components/ui/button.tsx +58 -0
- package/template/components/ui/card.tsx +103 -0
- package/template/components/ui/carousel.tsx +242 -0
- package/template/components/ui/command.tsx +196 -0
- package/template/components/ui/dialog.tsx +160 -0
- package/template/components/ui/dropdown-menu.tsx +268 -0
- package/template/components/ui/foto.tsx +54 -0
- package/template/components/ui/input-group.tsx +158 -0
- package/template/components/ui/input-otp.tsx +87 -0
- package/template/components/ui/input.tsx +20 -0
- package/template/components/ui/label.tsx +20 -0
- package/template/components/ui/navigation-menu.tsx +168 -0
- package/template/components/ui/pagination.tsx +132 -0
- package/template/components/ui/popover.tsx +90 -0
- package/template/components/ui/progress.tsx +83 -0
- package/template/components/ui/radio-group.tsx +38 -0
- package/template/components/ui/scroll-area.tsx +55 -0
- package/template/components/ui/select.tsx +201 -0
- package/template/components/ui/separator.tsx +25 -0
- package/template/components/ui/sheet.tsx +138 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/sonner.tsx +44 -0
- package/template/components/ui/switch.tsx +32 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +82 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +89 -0
- package/template/components/ui/toggle.tsx +45 -0
- package/template/components/ui/tooltip.tsx +66 -0
- package/template/components.json +25 -0
- package/template/eslint.config.mjs +36 -0
- package/template/gitignore +21 -0
- package/template/lib/analytics.ts +369 -0
- package/template/lib/api.ts +60 -0
- package/template/lib/capi.ts +87 -0
- package/template/lib/cart-link.ts +51 -0
- package/template/lib/cart-normalize.ts +142 -0
- package/template/lib/cart-recovery.ts +23 -0
- package/template/lib/cart-response.ts +48 -0
- package/template/lib/catalog-map.ts +54 -0
- package/template/lib/catalog.ts +3 -0
- package/template/lib/checkout-lock.ts +21 -0
- package/template/lib/checkout-nav.ts +22 -0
- package/template/lib/config.ts +51 -0
- package/template/lib/crm.ts +40 -0
- package/template/lib/customer-session.ts +21 -0
- package/template/lib/dataloader.ts +23 -0
- package/template/lib/editable/config.ts +11 -0
- package/template/lib/editable/document.ts +1762 -0
- package/template/lib/editable/index.ts +3 -0
- package/template/lib/editable/primitives.tsx +821 -0
- package/template/lib/editable/provider.tsx +861 -0
- package/template/lib/editable/rastreio-navegacao.tsx +65 -0
- package/template/lib/editable/rastreio.tsx +168 -0
- package/template/lib/editable/server.ts +189 -0
- package/template/lib/editable/tokens.ts +22 -0
- package/template/lib/editable/verify.ts +24 -0
- package/template/lib/enrichment/combos.ts +184 -0
- package/template/lib/enrichment/index.ts +188 -0
- package/template/lib/enrichment/products.json +1 -0
- package/template/lib/env-check.ts +47 -0
- package/template/lib/format.ts +130 -0
- package/template/lib/icons.ts +67 -0
- package/template/lib/json-ld.ts +9 -0
- package/template/lib/llms-txt.ts +75 -0
- package/template/lib/mockup.ts +24 -0
- package/template/lib/newsletter.ts +12 -0
- package/template/lib/orders.ts +160 -0
- package/template/lib/queries.ts +66 -0
- package/template/lib/ratelimit.ts +47 -0
- package/template/lib/rotas-editaveis.ts +192 -0
- package/template/lib/sanitize.ts +36 -0
- package/template/lib/schemas.ts +78 -0
- package/template/lib/session.ts +98 -0
- package/template/lib/store-config.ts +67 -0
- package/template/lib/unbox/client.ts +829 -0
- package/template/lib/unbox/customer.ts +224 -0
- package/template/lib/unbox/errors.ts +97 -0
- package/template/lib/unbox/index.ts +10 -0
- package/template/lib/unbox/store.ts +113 -0
- package/template/lib/unbox/types.ts +195 -0
- package/template/lib/unbox/webhooks.ts +66 -0
- package/template/lib/utils.ts +6 -0
- package/template/lib/vitrine.ts +242 -0
- package/template/lib/webhook-store.ts +22 -0
- package/template/middleware.ts +178 -0
- package/template/next.config.ts +63 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +5 -0
- package/template/public/brand/coll/default.png +0 -0
- package/template/public/brand/hero-desktop.svg +12 -0
- package/template/public/brand/hero-mobile.svg +12 -0
- package/template/public/brand/heros/boutique-desktop.svg +11 -0
- package/template/public/brand/heros/boutique-mobile.svg +11 -0
- package/template/public/brand/heros/editorial-desktop.svg +11 -0
- package/template/public/brand/heros/editorial-mobile.svg +11 -0
- package/template/public/brand/heros/essencial-desktop.svg +12 -0
- package/template/public/brand/heros/essencial-mobile.svg +12 -0
- package/template/public/brand/heros/promocional-desktop.svg +12 -0
- package/template/public/brand/heros/promocional-mobile.svg +12 -0
- package/template/public/brand/logo-chrome.svg +6 -0
- package/template/public/brand/logo-white.svg +6 -0
- package/template/public/brand/logo.svg +7 -0
- package/template/public/brand/pay-amex.webp +0 -0
- package/template/public/brand/pay-elo.webp +0 -0
- package/template/public/brand/pay-mastercard.webp +0 -0
- package/template/public/brand/pay-pix.webp +0 -0
- package/template/public/brand/pay-visa.webp +0 -0
- package/template/public/brand/ph/avatar-a.svg +5 -0
- package/template/public/brand/ph/avatar-b.svg +5 -0
- package/template/public/brand/ph/avatar-c.svg +5 -0
- package/template/public/brand/ph/avatar-d.svg +5 -0
- package/template/public/brand/ph/photo-a.svg +7 -0
- package/template/public/brand/ph/photo-b.svg +6 -0
- package/template/public/brand/ph/photo-c.svg +6 -0
- package/template/public/brand/ph/poster-a.svg +6 -0
- package/template/public/brand/ph/poster-b.svg +6 -0
- package/template/public/brand/ph/poster-c.svg +6 -0
- package/template/public/unbox/powered-by-fundo-preto.png +0 -0
- package/template/public/unbox/powered-by-transparente.png +0 -0
- package/template/public/unbox/powered-by.png +0 -0
- package/template/scripts/abandoned-cart.ts +82 -0
- package/template/scripts/check-editable.mjs +467 -0
- package/template/scripts/check-honestidade.mjs +111 -0
- package/template/scripts/check-placeholder.mjs +200 -0
- package/template/scripts/check-recipe.mjs +76 -0
- package/template/scripts/check-unbox-brand.mjs +298 -0
- package/template/scripts/contraste.js +177 -0
- package/template/scripts/dump-catalog.ts +95 -0
- package/template/scripts/load-env.ts +13 -0
- package/template/scripts/medir-sistema.mjs +56 -0
- package/template/scripts/place-order-pix.ts +61 -0
- package/template/scripts/qa-screenshots.mjs +150 -0
- package/template/scripts/sistema.py +231 -0
- package/template/scripts/subscribe-webhook.ts +36 -0
- package/template/scripts/test-live.ts +186 -0
- package/template/scripts/vocabulario.py +264 -0
- package/template/tsconfig.json +27 -0
- package/tools/LEIA-ME.md +39 -0
- package/tools/check-template-neutro.mjs +111 -0
- package/tools/notion-doc.mjs +191 -0
- package/tools/tokenize-neutrals.mjs +0 -0
- package/tools/tokenize-radius.mjs +52 -0
- package/tools/workflow-storefront.legado.js +1207 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unbox-plus/cli",
|
|
3
|
+
"version": "0.20.4",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Unbox Development Team",
|
|
6
|
+
"email": "devs@unbox.com.br",
|
|
7
|
+
"url": "https://unbox.com.br"
|
|
8
|
+
},
|
|
9
|
+
"description": "Scaffold a new Unbox storefront (Next.js) from the official template",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"unbox",
|
|
12
|
+
"storefront",
|
|
13
|
+
"ecommerce"
|
|
14
|
+
],
|
|
15
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"create-unbox-store": "./bin/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src",
|
|
23
|
+
"template",
|
|
24
|
+
"tools"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.11"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"prompts": "^2.4.2"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/unbox-plus/unbox-cli.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/unbox-plus/unbox-cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"check:neutro": "node tools/check-template-neutro.mjs",
|
|
41
|
+
"prepack": "node tools/check-template-neutro.mjs"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/colors.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Matemática de cor mínima pra derivar a paleta da loja a partir de 2-3 cores escolhidas pelo
|
|
2
|
+
// usuário (primária + CTA, opcionalmente chrome). Sem dependências externas.
|
|
3
|
+
|
|
4
|
+
const HEX_RE = /^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/;
|
|
5
|
+
|
|
6
|
+
export function normalizeHex(input) {
|
|
7
|
+
const m = HEX_RE.exec(input.trim());
|
|
8
|
+
if (!m) return null;
|
|
9
|
+
let hex = m[1];
|
|
10
|
+
if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
|
|
11
|
+
return `#${hex.toUpperCase()}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hexToRgb(hex) {
|
|
15
|
+
const n = parseInt(hex.slice(1), 16);
|
|
16
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rgbToHex({ r, g, b }) {
|
|
20
|
+
const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
|
|
21
|
+
return `#${c(r)}${c(g)}${c(b)}`.toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Mistura `hex` com `target` (preto/branco) por `amount` (0-1). amount=0.2 → 20% em direção ao target. */
|
|
25
|
+
function mix(hex, target, amount) {
|
|
26
|
+
const a = hexToRgb(hex);
|
|
27
|
+
const b = hexToRgb(target);
|
|
28
|
+
return rgbToHex({
|
|
29
|
+
r: a.r + (b.r - a.r) * amount,
|
|
30
|
+
g: a.g + (b.g - a.g) * amount,
|
|
31
|
+
b: a.b + (b.b - a.b) * amount,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const darken = (hex, amount = 0.15) => mix(hex, "#000000", amount);
|
|
36
|
+
export const lighten = (hex, amount = 0.15) => mix(hex, "#ffffff", amount);
|
|
37
|
+
|
|
38
|
+
/** Luminância relativa (WCAG simplificado) — decide se o texto sobre `hex` deve ser claro ou escuro. */
|
|
39
|
+
function relativeLuminance(hex) {
|
|
40
|
+
const { r, g, b } = hexToRgb(hex);
|
|
41
|
+
const lin = (v) => {
|
|
42
|
+
const c = v / 255;
|
|
43
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
44
|
+
};
|
|
45
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Texto de contraste (preto ou branco) pra ficar legível sobre `bgHex`. */
|
|
49
|
+
export function contrastText(bgHex) {
|
|
50
|
+
return relativeLuminance(bgHex) > 0.45 ? "#1A1B18" : "#FFFFFF";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deriva os tokens de marca a partir de `primary` + `cta` (e opcionalmente `chromeBg`),
|
|
55
|
+
* mais o MODO de chrome do preset:
|
|
56
|
+
* - "brand-dark" (default): chrome = primária escurecida (comportamento histórico)
|
|
57
|
+
* - "ink-dark": chrome = tinta do estilo escurecida (neutro, não "cor da marca")
|
|
58
|
+
* - "light": chrome claro (superfície do estilo, texto na tinta)
|
|
59
|
+
* Mesmo conjunto de variáveis do bloco "CONFIGURE" em app/globals.css.
|
|
60
|
+
*/
|
|
61
|
+
export function deriveBrandTokens({ primary, cta, chromeBg }, preset) {
|
|
62
|
+
const mode = preset?.chrome ?? "brand-dark";
|
|
63
|
+
const neutrals = preset?.neutrals ?? {};
|
|
64
|
+
const ink = neutrals["--store-ink"] ?? "#20211E";
|
|
65
|
+
|
|
66
|
+
let resolvedChromeBg, chromeText, chromeMuted;
|
|
67
|
+
if (chromeBg) {
|
|
68
|
+
resolvedChromeBg = chromeBg;
|
|
69
|
+
chromeText = contrastText(resolvedChromeBg);
|
|
70
|
+
chromeMuted = mix(resolvedChromeBg, chromeText, 0.55);
|
|
71
|
+
} else if (mode === "light") {
|
|
72
|
+
resolvedChromeBg = neutrals["--store-surface"] ?? "#FAF8F3";
|
|
73
|
+
chromeText = ink;
|
|
74
|
+
chromeMuted = neutrals["--store-muted"] ?? "#85816F";
|
|
75
|
+
} else if (mode === "ink-dark") {
|
|
76
|
+
resolvedChromeBg = darken(ink, 0.35);
|
|
77
|
+
chromeText = contrastText(resolvedChromeBg);
|
|
78
|
+
chromeMuted = mix(resolvedChromeBg, "#FFFFFF", 0.55);
|
|
79
|
+
} else {
|
|
80
|
+
resolvedChromeBg = darken(primary, 0.55);
|
|
81
|
+
chromeText = contrastText(resolvedChromeBg);
|
|
82
|
+
chromeMuted = mix(resolvedChromeBg, "#FFFFFF", 0.55);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"--store-primary": primary,
|
|
87
|
+
"--store-primary-dark": darken(primary, 0.15),
|
|
88
|
+
"--store-primary-soft": lighten(primary, 0.88),
|
|
89
|
+
"--store-cta": cta,
|
|
90
|
+
"--store-cta-fg": contrastText(cta),
|
|
91
|
+
"--store-cta-dark": darken(cta, 0.15),
|
|
92
|
+
"--store-cta-soft": lighten(cta, 0.8),
|
|
93
|
+
"--store-chrome-bg": resolvedChromeBg,
|
|
94
|
+
"--store-chrome-text": chromeText,
|
|
95
|
+
"--store-chrome-muted": chromeMuted,
|
|
96
|
+
// divisores DENTRO do chrome (rodapé). Derivado do texto pra funcionar também em
|
|
97
|
+
// chrome claro — um branco fixo a 10% sumiria por completo lá.
|
|
98
|
+
"--store-chrome-line": mix(resolvedChromeBg, chromeText, 0.14),
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/presets.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Presets de ESTILO visual da loja: receita da home + tipografia + neutros + chrome +
|
|
2
|
+
// radius + hero. O CLI sugere um (suggestPreset), o usuário confirma no select, e
|
|
3
|
+
// applyPreset (theme.js) escreve tudo no projeto gerado. O agente de branding refina
|
|
4
|
+
// depois no briefing (agents/PADROES.md documenta o repertório).
|
|
5
|
+
//
|
|
6
|
+
// Regras dos presets:
|
|
7
|
+
// - "essencial" é o default equilibrado (não mais "paridade com o visual histórico":
|
|
8
|
+
// header sem barra de busca e hero full-bleed, que é o que tira a cara de template).
|
|
9
|
+
// - Fontes: só famílias do Google Fonts com pesos 500-800 (o design usa font-extrabold).
|
|
10
|
+
// - Neutros: hex literais por preset (o CLI escreve nos tokens --store-* de globals.css).
|
|
11
|
+
|
|
12
|
+
export const PRESETS = {
|
|
13
|
+
essencial: {
|
|
14
|
+
// dials do briefing (1-10) — variância de composição, intensidade de motion,
|
|
15
|
+
// densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
|
|
16
|
+
dials: { variancia: 5, motion: 3, densidade: 6 },
|
|
17
|
+
label: "Essencial",
|
|
18
|
+
tagline: "layout completo e equilibrado, funciona pra qualquer loja",
|
|
19
|
+
fonts: {
|
|
20
|
+
sans: { fn: "Plus_Jakarta_Sans", weights: ["400", "500", "600", "700"] },
|
|
21
|
+
display: { fn: "Poppins", weights: ["500", "600", "700", "800"] },
|
|
22
|
+
},
|
|
23
|
+
neutrals: {
|
|
24
|
+
"--store-ink": "#18181B",
|
|
25
|
+
"--store-ink-2": "#3F3F46",
|
|
26
|
+
"--store-muted": "#71717A",
|
|
27
|
+
"--store-faint": "#A1A1AA",
|
|
28
|
+
"--store-bg": "#FAFAFA",
|
|
29
|
+
"--store-surface": "#FFFFFF",
|
|
30
|
+
"--store-surface-2": "#F4F4F5",
|
|
31
|
+
"--store-line": "#E4E4E7",
|
|
32
|
+
"--store-line-2": "#D4D4D8",
|
|
33
|
+
"--store-sale": "#DC2626",
|
|
34
|
+
"--store-sale-soft": "#FEE9E9",
|
|
35
|
+
},
|
|
36
|
+
chrome: "brand-dark",
|
|
37
|
+
chromeRecipe: { header: "equilibrado", footer: "colunas" },
|
|
38
|
+
radius: "0.875rem",
|
|
39
|
+
axes: {
|
|
40
|
+
"--section-gap": "52px", "--container-max": "1240px",
|
|
41
|
+
"--card-bg": "#ffffff", "--card-border": "1px solid var(--store-line)", "--card-shadow": "none",
|
|
42
|
+
},
|
|
43
|
+
// hero full-bleed: o banner em card arredondado é o maior "cara de template" que existe.
|
|
44
|
+
hero: "imagem-imersiva",
|
|
45
|
+
home: [
|
|
46
|
+
{ section: "hero", variant: "imagem-imersiva", props: { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", title: "Bem-vindo à nossa loja", subtitle: "Produtos escolhidos com cuidado, do nosso catálogo pra sua casa.", ctaLabel: "Ver destaques" } },
|
|
47
|
+
{ section: "trust-bar" },
|
|
48
|
+
{ section: "attributes-marquee" },
|
|
49
|
+
{ section: "category-pills" },
|
|
50
|
+
{ section: "combos-carousel" },
|
|
51
|
+
{ section: "savings" },
|
|
52
|
+
{ section: "benefits" },
|
|
53
|
+
{ section: "media-cards" },
|
|
54
|
+
{ section: "reviews", variant: "grid" },
|
|
55
|
+
{ section: "ritual" },
|
|
56
|
+
{ section: "founder-story" },
|
|
57
|
+
{ section: "kits" },
|
|
58
|
+
{ section: "spec-table" },
|
|
59
|
+
{ section: "video-wall" },
|
|
60
|
+
{ section: "trust-strip" },
|
|
61
|
+
{ section: "newsletter", variant: "bloco" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
promocional: {
|
|
66
|
+
// dials do briefing (1-10) — variância de composição, intensidade de motion,
|
|
67
|
+
// densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
|
|
68
|
+
dials: { variancia: 6, motion: 4, densidade: 9 },
|
|
69
|
+
label: "Promocional",
|
|
70
|
+
tagline: "oferta em primeiro plano: kits, economia e urgência pra subir o ticket",
|
|
71
|
+
fonts: {
|
|
72
|
+
sans: { fn: "Barlow", weights: ["400", "500", "600", "700"] },
|
|
73
|
+
display: { fn: "Barlow_Condensed", weights: ["500", "600", "700", "800"] },
|
|
74
|
+
},
|
|
75
|
+
neutrals: {
|
|
76
|
+
"--store-ink": "#09090B",
|
|
77
|
+
"--store-ink-2": "#27272A",
|
|
78
|
+
"--store-muted": "#71717A",
|
|
79
|
+
"--store-faint": "#A1A1AA",
|
|
80
|
+
"--store-bg": "#FFFFFF",
|
|
81
|
+
"--store-surface": "#FAFAFA",
|
|
82
|
+
"--store-surface-2": "#F4F4F5",
|
|
83
|
+
"--store-line": "#E4E4E7",
|
|
84
|
+
"--store-line-2": "#D4D4D8",
|
|
85
|
+
"--store-sale": "#E11D48",
|
|
86
|
+
"--store-sale-soft": "#FFE4E9",
|
|
87
|
+
},
|
|
88
|
+
chrome: "brand-dark",
|
|
89
|
+
chromeRecipe: { header: "compacto", footer: "conversao" },
|
|
90
|
+
radius: "1.25rem",
|
|
91
|
+
axes: {
|
|
92
|
+
"--section-gap": "44px", "--container-max": "1240px",
|
|
93
|
+
"--card-bg": "#ffffff", "--card-border": "1px solid transparent", "--card-shadow": "0 8px 28px rgba(0,0,0,0.07)",
|
|
94
|
+
},
|
|
95
|
+
hero: "carousel",
|
|
96
|
+
home: [
|
|
97
|
+
{ section: "hero", variant: "carousel", props: { banners: [
|
|
98
|
+
{ imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", href: "#comprar" },
|
|
99
|
+
{ imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", href: "#comprar" },
|
|
100
|
+
{ imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg" },
|
|
101
|
+
] } },
|
|
102
|
+
{ section: "attributes-marquee" },
|
|
103
|
+
{ section: "purchase-hero" },
|
|
104
|
+
{ section: "product-showcase", props: { ctaHref: "#comprar" } },
|
|
105
|
+
{ section: "savings" },
|
|
106
|
+
{ section: "combos-carousel" },
|
|
107
|
+
{ section: "benefits" },
|
|
108
|
+
{ section: "stats-grid" },
|
|
109
|
+
{ section: "reviews-carousel" },
|
|
110
|
+
{ section: "media-cards" },
|
|
111
|
+
{ section: "comparison" },
|
|
112
|
+
{ section: "kits" },
|
|
113
|
+
{ section: "ritual", props: { ctaLabel: "Montar meu pedido" } },
|
|
114
|
+
{ section: "social-row" },
|
|
115
|
+
{ section: "trust-strip" },
|
|
116
|
+
{ section: "newsletter", variant: "bloco" },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
editorial: {
|
|
121
|
+
// dials do briefing (1-10) — variância de composição, intensidade de motion,
|
|
122
|
+
// densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
|
|
123
|
+
dials: { variancia: 7, motion: 5, densidade: 4 },
|
|
124
|
+
label: "Editorial",
|
|
125
|
+
tagline: "clima de clube e assinatura, com respiro e tipografia de revista",
|
|
126
|
+
fonts: {
|
|
127
|
+
sans: { fn: "Inter", weights: ["400", "500", "600", "700"] },
|
|
128
|
+
display: { fn: "Fraunces", weights: ["500", "600", "700", "800"] },
|
|
129
|
+
},
|
|
130
|
+
neutrals: {
|
|
131
|
+
"--store-ink": "#1C1917",
|
|
132
|
+
"--store-ink-2": "#44403C",
|
|
133
|
+
"--store-muted": "#78716C",
|
|
134
|
+
"--store-faint": "#A8A29E",
|
|
135
|
+
"--store-bg": "#FAFAF9",
|
|
136
|
+
"--store-surface": "#FFFFFF",
|
|
137
|
+
"--store-surface-2": "#F5F5F4",
|
|
138
|
+
"--store-line": "#E7E5E4",
|
|
139
|
+
"--store-line-2": "#D6D3D1",
|
|
140
|
+
"--store-sale": "#B91C1C",
|
|
141
|
+
"--store-sale-soft": "#FBE8E8",
|
|
142
|
+
},
|
|
143
|
+
chrome: "ink-dark",
|
|
144
|
+
chromeRecipe: { header: "centralizado", footer: "editorial" },
|
|
145
|
+
radius: "0.5rem",
|
|
146
|
+
axes: {
|
|
147
|
+
"--section-gap": "72px", "--container-max": "1080px",
|
|
148
|
+
"--card-bg": "var(--store-surface)", "--card-border": "1px solid transparent", "--card-shadow": "none",
|
|
149
|
+
},
|
|
150
|
+
hero: "split-editorial",
|
|
151
|
+
home: [
|
|
152
|
+
{ section: "hero", variant: "split-editorial", props: { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", title: "Feito com calma, entregue todo mês", subtitle: "Curadoria da nossa loja pra sua rotina. Conheça os destaques e, se fizer sentido, assine.", ctaLabel: "Ver destaques" } },
|
|
153
|
+
{ section: "attributes-marquee" },
|
|
154
|
+
{ section: "benefits" },
|
|
155
|
+
{ section: "combos-carousel" },
|
|
156
|
+
{ section: "founder-story" },
|
|
157
|
+
{ section: "ritual" },
|
|
158
|
+
{ section: "media-cards" },
|
|
159
|
+
{ section: "reviews", variant: "faixa" },
|
|
160
|
+
{ section: "quote-banner" },
|
|
161
|
+
{ section: "kits" },
|
|
162
|
+
{ section: "spec-table" },
|
|
163
|
+
{ section: "social-row" },
|
|
164
|
+
{ section: "newsletter", variant: "inline" },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
boutique: {
|
|
169
|
+
// dials do briefing (1-10) — variância de composição, intensidade de motion,
|
|
170
|
+
// densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
|
|
171
|
+
dials: { variancia: 8, motion: 6, densidade: 2 },
|
|
172
|
+
label: "Boutique",
|
|
173
|
+
tagline: "minimal e premium: poucas seções, muito respiro, detalhes finos",
|
|
174
|
+
fonts: {
|
|
175
|
+
sans: { fn: "DM_Sans", weights: ["400", "500", "600", "700"] },
|
|
176
|
+
display: { fn: "Playfair_Display", weights: ["500", "600", "700", "800"] },
|
|
177
|
+
},
|
|
178
|
+
neutrals: {
|
|
179
|
+
"--store-ink": "#1A1A1F",
|
|
180
|
+
"--store-ink-2": "#3F3F49",
|
|
181
|
+
"--store-muted": "#71717D",
|
|
182
|
+
"--store-faint": "#A5A5B0",
|
|
183
|
+
"--store-bg": "#FBFBFC",
|
|
184
|
+
"--store-surface": "#FFFFFF",
|
|
185
|
+
"--store-surface-2": "#F4F4F6",
|
|
186
|
+
"--store-line": "#E8E8EC",
|
|
187
|
+
"--store-line-2": "#DCDCE2",
|
|
188
|
+
"--store-sale": "#A63D2F",
|
|
189
|
+
"--store-sale-soft": "#F6E7E3",
|
|
190
|
+
},
|
|
191
|
+
chrome: "ink-dark",
|
|
192
|
+
chromeRecipe: { header: "imersivo", footer: "minimal" },
|
|
193
|
+
radius: "0.25rem",
|
|
194
|
+
axes: {
|
|
195
|
+
"--section-gap": "88px", "--container-max": "1000px",
|
|
196
|
+
"--card-bg": "#ffffff", "--card-border": "1px solid var(--store-line-2)", "--card-shadow": "none",
|
|
197
|
+
},
|
|
198
|
+
// hero imersivo: é o par do header "imersivo" (a foto sobe por baixo dele e o header
|
|
199
|
+
// fica transparente). Sem foto boa da marca, troque na receita por "minimal-texto" —
|
|
200
|
+
// o header degrada pra sólido sozinho.
|
|
201
|
+
hero: "imagem-imersiva",
|
|
202
|
+
home: [
|
|
203
|
+
{ section: "hero", variant: "imagem-imersiva", props: { title: "Peças escolhidas a dedo", subtitle: "Um catálogo enxuto, pensado pra durar. Conheça os destaques da casa.", ctaLabel: "Ver destaques" } },
|
|
204
|
+
{ section: "combos-carousel" },
|
|
205
|
+
{ section: "benefits" },
|
|
206
|
+
{ section: "media-cards" },
|
|
207
|
+
{ section: "quote-banner" },
|
|
208
|
+
{ section: "reviews", variant: "faixa" },
|
|
209
|
+
{ section: "founder-story" },
|
|
210
|
+
{ section: "kits" },
|
|
211
|
+
{ section: "spec-table" },
|
|
212
|
+
{ section: "newsletter", variant: "inline" },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const PRESET_NAMES = Object.keys(PRESETS);
|
|
218
|
+
|
|
219
|
+
// Heurística de sugestão: soma pontos por palavra-chave (sem acento, lowercase) no
|
|
220
|
+
// objetivo/site/instagram. Empate: promocional > editorial > boutique. Zero: essencial.
|
|
221
|
+
const KEYWORDS = {
|
|
222
|
+
promocional: ["ticket", "upsell", "kit", "combo", "oferta", "promoc", "desconto", "atacado", "black", "volume", "venda", "conversao", "barato"],
|
|
223
|
+
editorial: ["assinatura", "recorren", "clube", "curadoria", "conteudo", "comunidade", "blog", "receita", "ritual", "mensal"],
|
|
224
|
+
boutique: ["premium", "minimal", "luxo", "sofistic", "estetic", "design", "autoral", "exclusiv", "boutique", "atelie", "artesanal", "fino"],
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function normalize(s) {
|
|
228
|
+
return (s ?? "").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Sugere um preset a partir do que o usuário contou no formulário. Nunca lança. */
|
|
232
|
+
export function suggestPreset({ objetivo, site, instagram } = {}) {
|
|
233
|
+
const haystack = [objetivo, site, instagram].map(normalize).join(" ");
|
|
234
|
+
const scores = {};
|
|
235
|
+
for (const [preset, words] of Object.entries(KEYWORDS)) {
|
|
236
|
+
scores[preset] = words.reduce((acc, w) => acc + (haystack.includes(w) ? 1 : 0), 0);
|
|
237
|
+
}
|
|
238
|
+
const order = ["promocional", "editorial", "boutique"]; // desempate
|
|
239
|
+
const best = order.reduce((a, b) => (scores[b] > scores[a] ? b : a), "promocional");
|
|
240
|
+
return scores[best] > 0 ? best : "essencial";
|
|
241
|
+
}
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PRESETS } from "./presets.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Substitui os valores dos tokens `--store-*` (e `--radius`) no bloco "CONFIGURE" de
|
|
7
|
+
* app/globals.css. Só troca o valor depois de `:`, nunca a chave nem o resto do arquivo.
|
|
8
|
+
* Aceita hex e valores rem (radius).
|
|
9
|
+
*
|
|
10
|
+
* A regex é ANCORADA no início da linha (`^\s*`) e proibida de atravessar quebra de linha
|
|
11
|
+
* (`[^;\n]`) por um motivo concreto: sem isso, uma MENÇÃO ao token dentro de um comentário
|
|
12
|
+
* (ex.: "o token --motion: 0 desliga tudo") casava primeiro, e o `[^;]+` engolia tudo até o
|
|
13
|
+
* `;` seguinte — várias linhas adiante, levando junto o fechamento do comentário e o
|
|
14
|
+
* `@import` do Tailwind. O CSS saía quebrado e só o build acusava, com erro que não apontava
|
|
15
|
+
* pra causa. Declaração de token vive sempre sozinha na linha; menção em prosa, não.
|
|
16
|
+
*/
|
|
17
|
+
export function applyBrandTokens(targetDir, tokens) {
|
|
18
|
+
const cssPath = path.join(targetDir, "app", "globals.css");
|
|
19
|
+
let css = fs.readFileSync(cssPath, "utf8");
|
|
20
|
+
|
|
21
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
22
|
+
const re = new RegExp(`^(\\s*${name}:\\s*)[^;\\n]+;`, "m");
|
|
23
|
+
if (!re.test(css)) {
|
|
24
|
+
throw new Error(`[create-unbox-store] token ${name} não encontrado em app/globals.css: a foundation pode ter mudado.`);
|
|
25
|
+
}
|
|
26
|
+
css = css.replace(re, `$1${value};`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fs.writeFileSync(cssPath, css);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reescreve o bloco UNBOX-FONTS de app/layout.tsx com o par tipográfico do preset.
|
|
34
|
+
* next/font exige declaração estática top-level — por isso replace de bloco marcado.
|
|
35
|
+
*/
|
|
36
|
+
export function applyFonts(targetDir, preset) {
|
|
37
|
+
const layoutPath = path.join(targetDir, "app", "layout.tsx");
|
|
38
|
+
let src = fs.readFileSync(layoutPath, "utf8");
|
|
39
|
+
const re = /\/\/ UNBOX-FONTS-BEGIN[\s\S]*?\/\/ UNBOX-FONTS-END/;
|
|
40
|
+
if (!re.test(src)) {
|
|
41
|
+
throw new Error("[create-unbox-store] markers UNBOX-FONTS não encontrados em app/layout.tsx: a foundation pode ter mudado.");
|
|
42
|
+
}
|
|
43
|
+
const { sans, display } = preset.fonts;
|
|
44
|
+
const families = [...new Set(["Geist_Mono", sans.fn, display.fn])].sort();
|
|
45
|
+
const block = [
|
|
46
|
+
"// UNBOX-FONTS-BEGIN (bloco reescrito pelo create-unbox-store conforme o estilo escolhido; não renomear os markers)",
|
|
47
|
+
`import { ${families.join(", ")} } from "next/font/google";`,
|
|
48
|
+
`const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });`,
|
|
49
|
+
`const sans = ${sans.fn}({ variable: "--font-sans", subsets: ["latin"], weight: [${sans.weights.map((w) => `"${w}"`).join(", ")}], display: "swap" });`,
|
|
50
|
+
`const displayFont = ${display.fn}({ variable: "--font-display", subsets: ["latin"], weight: [${display.weights.map((w) => `"${w}"`).join(", ")}], display: "swap" });`,
|
|
51
|
+
"// UNBOX-FONTS-END",
|
|
52
|
+
].join("\n");
|
|
53
|
+
fs.writeFileSync(layoutPath, src.replace(re, block));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Escreve a receita da home (components/home/home-recipe.ts) conforme o preset. */
|
|
57
|
+
/** Escreve components/chrome/chrome-recipe.ts — qual header e qual rodapé a loja usa.
|
|
58
|
+
* Mesma mecânica do applyHomeRecipe: reescreve o arquivo inteiro e falha alto se ele sumiu. */
|
|
59
|
+
export function applyChromeRecipe(targetDir, preset) {
|
|
60
|
+
const recipePath = path.join(targetDir, "components", "chrome", "chrome-recipe.ts");
|
|
61
|
+
if (!fs.existsSync(recipePath)) {
|
|
62
|
+
throw new Error("[create-unbox-store] components/chrome/chrome-recipe.ts não encontrado: a foundation pode ter mudado.");
|
|
63
|
+
}
|
|
64
|
+
const recipe = preset.chromeRecipe ?? { header: "classico", footer: "colunas" };
|
|
65
|
+
const content = `// ═══════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// RECEITA DO CHROME: qual header e qual rodapé esta loja usa.
|
|
67
|
+
// Escrita pelo create-unbox-store conforme o ESTILO escolhido no setup; editada
|
|
68
|
+
// pelo agente de branding no briefing (é AQUI que o chrome muda, não reescreva
|
|
69
|
+
// o JSX de uma variante; catálogo em agents/PADROES.md).
|
|
70
|
+
// \`npm run typecheck\` acusa header/rodapé inexistente.
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
import type { ChromeRecipe } from "@/components/chrome/registry";
|
|
73
|
+
|
|
74
|
+
export const chromeRecipe: ChromeRecipe = ${JSON.stringify(recipe, null, 2)};
|
|
75
|
+
`;
|
|
76
|
+
fs.writeFileSync(recipePath, content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function applyHomeRecipe(targetDir, preset) {
|
|
80
|
+
const recipePath = path.join(targetDir, "components", "home", "home-recipe.ts");
|
|
81
|
+
if (!fs.existsSync(recipePath)) {
|
|
82
|
+
throw new Error("[create-unbox-store] components/home/home-recipe.ts não encontrado: a foundation pode ter mudado.");
|
|
83
|
+
}
|
|
84
|
+
const body = JSON.stringify(preset.home, null, 2);
|
|
85
|
+
const content = `// ═══════════════════════════════════════════════════════════════════════════
|
|
86
|
+
// RECEITA DA HOME: a ordem, as variantes e as props das seções.
|
|
87
|
+
// Escrita pelo create-unbox-store conforme o ESTILO escolhido no setup; editada
|
|
88
|
+
// pelo agente de branding no briefing (é AQUI que o layout muda, não reescreva
|
|
89
|
+
// o JSX das seções; catálogo de seções/variantes em agents/PADROES.md).
|
|
90
|
+
// \`npm run typecheck\` acusa seção/variante inexistente.
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
92
|
+
import type { HomeRecipe } from "@/components/home/sections/registry";
|
|
93
|
+
|
|
94
|
+
export const homeRecipe: HomeRecipe = ${body};
|
|
95
|
+
`;
|
|
96
|
+
fs.writeFileSync(recipePath, content);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Cores DEFAULT usadas nos SVGs de hero placeholder — trocadas pelas da marca no scaffold.
|
|
100
|
+
// Sentinelas NEUTRAS (= os valores default da paleta), únicas entre si: o template cru já
|
|
101
|
+
// renderiza limpo e o recolorizador segue mapeando hex→token no scaffold.
|
|
102
|
+
const HERO_SVG_COLOR_SLOTS = {
|
|
103
|
+
"#18181B": "--store-chrome-bg",
|
|
104
|
+
"#3F3F46": "--store-ink-2",
|
|
105
|
+
"#27272A": "--store-primary",
|
|
106
|
+
"#D97706": "--store-cta",
|
|
107
|
+
"#F1F1F3": "--store-primary-soft",
|
|
108
|
+
"#FCFCFD": "--store-surface",
|
|
109
|
+
"#F4F4F5": "--store-surface-2",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Recolore um SVG trocando as cores default pelos valores da marca. */
|
|
113
|
+
function recolorSvg(svg, palette) {
|
|
114
|
+
for (const [hex, token] of Object.entries(HERO_SVG_COLOR_SLOTS)) {
|
|
115
|
+
const value = palette[token];
|
|
116
|
+
if (value) svg = svg.replaceAll(hex, value);
|
|
117
|
+
}
|
|
118
|
+
return svg;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Escolhe o par de heros placeholder do preset, recolore com a marca e grava como
|
|
123
|
+
* public/brand/hero-{desktop,mobile}.svg. Remove a pasta heros/ do projeto gerado.
|
|
124
|
+
* Também recolore TODOS os placeholders de mídia em public/brand/ph/ (fotos, posters,
|
|
125
|
+
* avatares usados pelas seções da biblioteca) — a loja nasce com a paleta da marca.
|
|
126
|
+
*/
|
|
127
|
+
export function applyHeroAssets(targetDir, presetName, tokens, neutrals) {
|
|
128
|
+
const herosDir = path.join(targetDir, "public", "brand", "heros");
|
|
129
|
+
const palette = { ...neutrals, ...tokens };
|
|
130
|
+
for (const kind of ["desktop", "mobile"]) {
|
|
131
|
+
const src = path.join(herosDir, `${presetName}-${kind}.svg`);
|
|
132
|
+
if (!fs.existsSync(src)) {
|
|
133
|
+
throw new Error(`[create-unbox-store] hero placeholder ${presetName}-${kind}.svg não encontrado: a foundation pode ter mudado.`);
|
|
134
|
+
}
|
|
135
|
+
const svg = recolorSvg(fs.readFileSync(src, "utf8"), palette);
|
|
136
|
+
fs.writeFileSync(path.join(targetDir, "public", "brand", `hero-${kind}.svg`), svg);
|
|
137
|
+
}
|
|
138
|
+
fs.rmSync(herosDir, { recursive: true, force: true });
|
|
139
|
+
|
|
140
|
+
const phDir = path.join(targetDir, "public", "brand", "ph");
|
|
141
|
+
if (fs.existsSync(phDir)) {
|
|
142
|
+
for (const f of fs.readdirSync(phDir)) {
|
|
143
|
+
if (!f.endsWith(".svg")) continue;
|
|
144
|
+
const p = path.join(phDir, f);
|
|
145
|
+
fs.writeFileSync(p, recolorSvg(fs.readFileSync(p, "utf8"), palette));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Aplica o preset inteiro: tokens (cores+neutros+radius), fontes, receita e heros. */
|
|
151
|
+
export function applyPreset(targetDir, presetName, brandTokens) {
|
|
152
|
+
const preset = PRESETS[presetName];
|
|
153
|
+
if (!preset) throw new Error(`[create-unbox-store] estilo desconhecido: ${presetName}`);
|
|
154
|
+
// --motion vem do dial: 0 desliga as animações, 1 é o padrão, acima disso amplifica.
|
|
155
|
+
const motion = preset.dials?.motion != null ? (preset.dials.motion / 5).toFixed(2) : "1";
|
|
156
|
+
applyBrandTokens(targetDir, {
|
|
157
|
+
...brandTokens, ...preset.neutrals, ...(preset.axes ?? {}),
|
|
158
|
+
"--radius": preset.radius, "--motion": motion,
|
|
159
|
+
});
|
|
160
|
+
applyFonts(targetDir, preset);
|
|
161
|
+
applyHomeRecipe(targetDir, preset);
|
|
162
|
+
applyChromeRecipe(targetDir, preset);
|
|
163
|
+
applyHeroAssets(targetDir, presetName, brandTokens, preset.neutrals);
|
|
164
|
+
// manifest.ts e opengraph-image.tsx: os DOIS lugares onde a cor precisa ser hex literal, porque
|
|
165
|
+
// nem o manifest do PWA nem o satori (ImageResponse) leem variavel de CSS. Sem reescrever aqui,
|
|
166
|
+
// a loja aplica a paleta da marca em tudo e mesmo assim instala no celular e compartilha no
|
|
167
|
+
// WhatsApp com a cor default da foundation.
|
|
168
|
+
const tokens = { ...preset.neutrals, ...brandTokens };
|
|
169
|
+
const chromeBg = tokens["--store-chrome-bg"];
|
|
170
|
+
const manifestPath = path.join(targetDir, "app", "manifest.ts");
|
|
171
|
+
if (chromeBg && fs.existsSync(manifestPath)) {
|
|
172
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
173
|
+
fs.writeFileSync(manifestPath, content.replace(/theme_color:\s*"[^"]*"/, `theme_color: "${chromeBg}"`));
|
|
174
|
+
}
|
|
175
|
+
const ogPath = path.join(targetDir, "app", "opengraph-image.tsx");
|
|
176
|
+
const ogBg = tokens["--store-primary"];
|
|
177
|
+
const ogFg = tokens["--store-chrome-text"] || "#FFFFFF";
|
|
178
|
+
if (ogBg && fs.existsSync(ogPath)) {
|
|
179
|
+
let og = fs.readFileSync(ogPath, "utf8");
|
|
180
|
+
og = og.replace(/const PRIMARY = "[^"]*";/, `const PRIMARY = "${ogBg}";`);
|
|
181
|
+
og = og.replace(/const PRIMARY_FG = "[^"]*";/, `const PRIMARY_FG = "${ogFg}";`);
|
|
182
|
+
fs.writeFileSync(ogPath, og);
|
|
183
|
+
}
|
|
184
|
+
if (preset.chrome === "light") {
|
|
185
|
+
fs.copyFileSync(
|
|
186
|
+
path.join(targetDir, "public", "brand", "logo.svg"),
|
|
187
|
+
path.join(targetDir, "public", "brand", "logo-chrome.svg"),
|
|
188
|
+
);
|
|
189
|
+
// Selo "Powered by Unbox": em rodapé CLARO a variante ativa é a com fundo preto
|
|
190
|
+
// (regra da Unbox). O default do template (transparente neon) serve os chromes escuros.
|
|
191
|
+
fs.copyFileSync(
|
|
192
|
+
path.join(targetDir, "public", "unbox", "powered-by-fundo-preto.png"),
|
|
193
|
+
path.join(targetDir, "public", "unbox", "powered-by.png"),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Troca o fallback "Minha Loja" pelo nome de exibição escolhido (header, footer e metadata). */
|
|
199
|
+
export function applyStoreName(targetDir, displayName) {
|
|
200
|
+
if (!displayName) return;
|
|
201
|
+
// Literal de string TS: escapa barra e aspas. E TODA substituição usa função (não string)
|
|
202
|
+
// — com string, um nome com `$&` ou `$'` vira padrão de replace e corrompe o TSX.
|
|
203
|
+
const safe = displayName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
204
|
+
const lit = () => `?? "${safe}"`;
|
|
205
|
+
const nome = () => safe;
|
|
206
|
+
const files = [
|
|
207
|
+
path.join(targetDir, "components", "site-header.tsx"),
|
|
208
|
+
path.join(targetDir, "components", "site-footer.tsx"),
|
|
209
|
+
];
|
|
210
|
+
for (const f of files) {
|
|
211
|
+
if (!fs.existsSync(f)) continue;
|
|
212
|
+
const content = fs.readFileSync(f, "utf8");
|
|
213
|
+
fs.writeFileSync(f, content.replaceAll('?? "Minha Loja"', lit));
|
|
214
|
+
}
|
|
215
|
+
// metadata do app/layout.tsx (título da aba, template e OpenGraph siteName) e da PDP,
|
|
216
|
+
// que repete siteName no seu próprio openGraph — o App Router substitui o objeto
|
|
217
|
+
// openGraph inteiro do layout pai, não faz merge, então o campo precisa existir lá também.
|
|
218
|
+
for (const rel of [["app", "layout.tsx"], ["app", "(loja)", "produto", "[productSlug]", "page.tsx"], ["app", "(loja)", "page.tsx"], ["app", "opengraph-image.tsx"]]) {
|
|
219
|
+
const p = path.join(targetDir, ...rel);
|
|
220
|
+
if (!fs.existsSync(p)) continue;
|
|
221
|
+
fs.writeFileSync(p, fs.readFileSync(p, "utf8").replaceAll("Minha Loja", nome));
|
|
222
|
+
}
|
|
223
|
+
// app/manifest.ts (nome do PWA na tela inicial — some com o "Minha Loja"/"Loja" placeholder)
|
|
224
|
+
const manifestPath = path.join(targetDir, "app", "manifest.ts");
|
|
225
|
+
if (fs.existsSync(manifestPath)) {
|
|
226
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
227
|
+
fs.writeFileSync(manifestPath, content.replaceAll('?? "Minha Loja"', lit).replaceAll('?? "Loja"', lit));
|
|
228
|
+
}
|
|
229
|
+
// Porta de preview: o slug vira nome do cookie e título do negócio no Pipedrive
|
|
230
|
+
// ("<slug> - Nome do Lead"). Sem acento/espaço pra viver em nome de cookie.
|
|
231
|
+
const slug = displayName
|
|
232
|
+
.normalize("NFD").replace(/[̀-ͯ]/g, "")
|
|
233
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 30) || "loja";
|
|
234
|
+
for (const rel of [["middleware.ts"], ["app", "api", "acesso", "route.ts"]]) {
|
|
235
|
+
const p = path.join(targetDir, ...rel);
|
|
236
|
+
if (!fs.existsSync(p)) continue;
|
|
237
|
+
const content = fs.readFileSync(p, "utf8");
|
|
238
|
+
fs.writeFileSync(p, content.replaceAll('"minhaloja"', `"${slug}"`));
|
|
239
|
+
}
|
|
240
|
+
// Editor da Unbox: o MESMO slug vira o STORE_SLUG de lib/editable/config.ts (a foundation traz o
|
|
241
|
+
// marcador __STORE_SLUG__; é por ele que a loja pede o conteúdo publicado ao editor). Tem de ser
|
|
242
|
+
// IGUAL ao `slug` da entrada da loja no shops.json do editor, que segue esta mesma derivação
|
|
243
|
+
// (minúsculas, sem acento, só letras e números). Marcador ausente é erro, não silêncio: uma loja
|
|
244
|
+
// com "__STORE_SLUG__" no ar pede o publicado de uma loja que não existe e nunca muda.
|
|
245
|
+
const configEditavel = path.join(targetDir, "lib", "editable", "config.ts");
|
|
246
|
+
if (fs.existsSync(configEditavel)) {
|
|
247
|
+
const content = fs.readFileSync(configEditavel, "utf8");
|
|
248
|
+
if (!content.includes("__STORE_SLUG__")) {
|
|
249
|
+
throw new Error("[create-unbox-store] marcador __STORE_SLUG__ não encontrado em lib/editable/config.ts: a foundation do editor pode ter mudado.");
|
|
250
|
+
}
|
|
251
|
+
fs.writeFileSync(configEditavel, content.replaceAll("__STORE_SLUG__", () => slug));
|
|
252
|
+
}
|
|
253
|
+
}
|