@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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cobertura do vocabulário gráfico — a medida do degrau 1 da escada.
|
|
3
|
+
|
|
4
|
+
POR QUE ISTO EXISTE
|
|
5
|
+
"Cara de AI" não é falta de capricho: é variedade sem sistema. O olho lê repetição
|
|
6
|
+
como intenção e variedade como acidente. Em TRÊS marcas diferentes, a virada de "podia
|
|
7
|
+
ser de qualquer um" para "tem dono" veio do mesmo movimento — um punhado de marcas
|
|
8
|
+
gráficas próprias, repetidas em toda seção:
|
|
9
|
+
|
|
10
|
+
Bicafé .pilula .selo .micro .numeral .regua ("é barato e é o que faz o site
|
|
11
|
+
ter dono")
|
|
12
|
+
Zé Tona código de cor por tampa em TODO componente que lista produto
|
|
13
|
+
Oddie o estilo adesivo com contorno
|
|
14
|
+
|
|
15
|
+
E o defeito que este script pega não é a AUSÊNCIA do vocabulário — é ele existir e não
|
|
16
|
+
ter sido distribuído. Medido na Pipó em 04/09/2026:
|
|
17
|
+
|
|
18
|
+
pipo-rotulo 24 usos <- voz tipográfica: distribuída
|
|
19
|
+
pipo-display 16 usos
|
|
20
|
+
pipo-filete 1 uso <- filete dourado 44x2px: a marca da casa
|
|
21
|
+
pipo-roda 1 uso usada UMA vez cada
|
|
22
|
+
pipo-pop 1 uso
|
|
23
|
+
pipo-parallax 0 usos <- definida e nunca usada
|
|
24
|
+
|
|
25
|
+
Uma linha dourada numa seção é decoração. A mesma linha em nove seções é assinatura.
|
|
26
|
+
|
|
27
|
+
O QUE ELE MEDE
|
|
28
|
+
1. o vocabulário declarado (classes `.<prefixo>-*` no CSS da marca)
|
|
29
|
+
2. quantas vezes cada uma é de fato usada, e quais estão mortas
|
|
30
|
+
3. COBERTURA POR SEÇÃO: quantas seções da receita carregam pelo menos uma marca da casa
|
|
31
|
+
— uma seção sem nenhuma é uma seção que poderia estar em qualquer loja
|
|
32
|
+
4. cobertura por página (home / catálogo / produto / carrinho), que é onde a assinatura
|
|
33
|
+
costuma parar: o vocabulário chega na home e não atravessa para as internas
|
|
34
|
+
|
|
35
|
+
USO
|
|
36
|
+
vocabulario.py <dir-do-projeto> [--prefixo pipo] [--cobertura 0.7] [--minimo 3]
|
|
37
|
+
|
|
38
|
+
CÓDIGOS DE SAÍDA
|
|
39
|
+
0 o vocabulário está distribuído
|
|
40
|
+
1 existe vocabulário, mas ele não cobre a loja
|
|
41
|
+
2 O GATE NÃO RODOU (caminho errado, sem CSS, ou nenhuma classe de marca encontrada)
|
|
42
|
+
|
|
43
|
+
O 2 é o mesmo do escala.py: gate que varre o vazio e diz "limpo" aprova sem ter olhado.
|
|
44
|
+
"""
|
|
45
|
+
import re
|
|
46
|
+
import sys
|
|
47
|
+
import pathlib
|
|
48
|
+
from collections import Counter
|
|
49
|
+
|
|
50
|
+
# Ancorado na RAIZ do projeto de proposito: `marca/` (briefing) se ignora, mas
|
|
51
|
+
# `components/marca/` e onde vivem os componentes de marca — foi la que a primeira
|
|
52
|
+
# versao deste script perdeu a `pipo-roda` e a declarou morta.
|
|
53
|
+
IGNORAR_RAIZ = {"node_modules", ".next", ".git", "shots", "reviews", "public", "marca"}
|
|
54
|
+
|
|
55
|
+
# Classes utilitárias do próprio framework não são vocabulário de marca: elas resolvem
|
|
56
|
+
# layout, não identidade. Quem assina a página é o filete, a roda, o selo — não o
|
|
57
|
+
# container da seção.
|
|
58
|
+
SUFIXO_UTILITARIO = re.compile(r"-(secao|tela|trilho|container|grid|wrap|layout|sr)$")
|
|
59
|
+
|
|
60
|
+
RE_CLASSE_CSS = re.compile(r"^\.([a-z][a-z0-9]*)-([a-z0-9-]+)\s*(?:,|\{|::|:)", re.M)
|
|
61
|
+
|
|
62
|
+
# Nem toda marca da casa e uma classe CSS. Na Pipo, a chuva de pipoca e a roda
|
|
63
|
+
# tipografica sao COMPONENTES em components/marca/ — a primeira versao deste script
|
|
64
|
+
# so contava classes e reportou o vocabulario como muito menos distribuido do que
|
|
65
|
+
# ele esta: a chuva aparece em 8 lugares, nao em 1.
|
|
66
|
+
DIR_COMPONENTES_MARCA = "components/marca"
|
|
67
|
+
RE_COMPONENTE = re.compile(r"export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9]*)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def arquivos(raiz, exts):
|
|
71
|
+
for p in raiz.rglob("*"):
|
|
72
|
+
if p.suffix.lower() not in exts:
|
|
73
|
+
continue
|
|
74
|
+
rel = p.relative_to(raiz).parts
|
|
75
|
+
if rel and rel[0] in IGNORAR_RAIZ:
|
|
76
|
+
continue
|
|
77
|
+
yield p
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def descobrir_vocabulario(raiz, prefixo=None):
|
|
81
|
+
"""Lê o CSS e devolve (prefixo, {classe: descricao curta})."""
|
|
82
|
+
css = list(arquivos(raiz, {".css"}))
|
|
83
|
+
if not css:
|
|
84
|
+
return None, {}
|
|
85
|
+
|
|
86
|
+
achadas = Counter()
|
|
87
|
+
por_prefixo = {}
|
|
88
|
+
for p in css:
|
|
89
|
+
t = p.read_text(encoding="utf-8", errors="ignore")
|
|
90
|
+
for m in RE_CLASSE_CSS.finditer(t):
|
|
91
|
+
pre, resto = m.group(1), m.group(2)
|
|
92
|
+
achadas[pre] += 1
|
|
93
|
+
por_prefixo.setdefault(pre, set()).add(f"{pre}-{resto}")
|
|
94
|
+
|
|
95
|
+
if prefixo is None:
|
|
96
|
+
# O prefixo da casa é o que tem mais classes distintas — `pipo-`, `bic-`, `ze-`.
|
|
97
|
+
# Prefixos de framework (tw, sm, md) não chegam perto em variedade.
|
|
98
|
+
candidatos = sorted(por_prefixo.items(), key=lambda kv: -len(kv[1]))
|
|
99
|
+
if not candidatos or len(candidatos[0][1]) < 3:
|
|
100
|
+
return None, {}
|
|
101
|
+
prefixo = candidatos[0][0]
|
|
102
|
+
|
|
103
|
+
return prefixo, sorted(por_prefixo.get(prefixo, set()))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def componentes_de_marca(raiz):
|
|
107
|
+
"""Componentes exportados de components/marca/ — marcas da casa que nao sao classe."""
|
|
108
|
+
base = raiz / DIR_COMPONENTES_MARCA
|
|
109
|
+
nomes = set()
|
|
110
|
+
if base.is_dir():
|
|
111
|
+
for p in base.rglob("*.tsx"):
|
|
112
|
+
nomes |= set(RE_COMPONENTE.findall(p.read_text(encoding="utf-8", errors="ignore")))
|
|
113
|
+
return sorted(nomes)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def usos(raiz, classes, comps=()):
|
|
117
|
+
"""Conta uso de cada marca (classe OU componente) e mapeia arquivo -> marcas."""
|
|
118
|
+
contagem = Counter()
|
|
119
|
+
por_arquivo = {}
|
|
120
|
+
base_marca = (raiz / DIR_COMPONENTES_MARCA).resolve()
|
|
121
|
+
for p in arquivos(raiz, {".tsx", ".jsx"}):
|
|
122
|
+
# a definicao do componente nao conta como uso dele
|
|
123
|
+
dentro_da_definicao = str(p.resolve()).startswith(str(base_marca))
|
|
124
|
+
t = p.read_text(encoding="utf-8", errors="ignore")
|
|
125
|
+
presentes = set()
|
|
126
|
+
for c in classes:
|
|
127
|
+
n = len(re.findall(r"\b" + re.escape(c) + r"\b", t))
|
|
128
|
+
if n:
|
|
129
|
+
contagem[c] += n
|
|
130
|
+
presentes.add(c)
|
|
131
|
+
if not dentro_da_definicao:
|
|
132
|
+
for c in comps:
|
|
133
|
+
n = len(re.findall(r"<" + re.escape(c) + r"\b", t))
|
|
134
|
+
if n:
|
|
135
|
+
contagem[c] += n
|
|
136
|
+
presentes.add(c)
|
|
137
|
+
if presentes:
|
|
138
|
+
por_arquivo[p] = presentes
|
|
139
|
+
return contagem, por_arquivo
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def secoes_da_receita(raiz):
|
|
143
|
+
for nome in ("components/home/home-recipe.ts", "components/home/home-recipe.tsx"):
|
|
144
|
+
p = raiz / nome
|
|
145
|
+
if p.exists():
|
|
146
|
+
t = p.read_text(encoding="utf-8", errors="ignore")
|
|
147
|
+
return re.findall(r"\bsection:\s*[\"']([^\"']+)[\"']", t)
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def arquivo_da_secao(raiz, nome):
|
|
152
|
+
for cand in (
|
|
153
|
+
raiz / "components/home/sections" / f"{nome}.tsx",
|
|
154
|
+
raiz / "components/home" / f"{nome}.tsx",
|
|
155
|
+
):
|
|
156
|
+
if cand.exists():
|
|
157
|
+
return cand
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
PAGINAS = {
|
|
162
|
+
"home": ["components/home"],
|
|
163
|
+
"catálogo (PLP)": ["components/catalog", "app/(loja)/produtos"],
|
|
164
|
+
"produto (PDP)": ["components/product", "app/(loja)/produto"],
|
|
165
|
+
"carrinho": ["components/cart", "app/(loja)/carrinho"],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main():
|
|
170
|
+
args = sys.argv[1:]
|
|
171
|
+
if not args:
|
|
172
|
+
sys.exit(__doc__)
|
|
173
|
+
raiz = pathlib.Path(args[0]).resolve()
|
|
174
|
+
prefixo = args[args.index("--prefixo") + 1] if "--prefixo" in args else None
|
|
175
|
+
alvo_cob = float(args[args.index("--cobertura") + 1]) if "--cobertura" in args else 0.7
|
|
176
|
+
minimo = int(args[args.index("--minimo") + 1]) if "--minimo" in args else 3
|
|
177
|
+
|
|
178
|
+
if not raiz.is_dir():
|
|
179
|
+
print(f"ERRO: {raiz} nao e um diretorio — o gate NAO RODOU", file=sys.stderr)
|
|
180
|
+
sys.exit(2)
|
|
181
|
+
|
|
182
|
+
prefixo, classes = descobrir_vocabulario(raiz, prefixo)
|
|
183
|
+
if not classes:
|
|
184
|
+
print(
|
|
185
|
+
"ERRO: nenhum vocabulario de marca encontrado no CSS — o gate NAO RODOU.\n"
|
|
186
|
+
" Ou a loja nao tem vocabulario proprio (que ja e o achado do degrau 1),\n"
|
|
187
|
+
" ou o prefixo precisa vir em --prefixo.",
|
|
188
|
+
file=sys.stderr,
|
|
189
|
+
)
|
|
190
|
+
sys.exit(2)
|
|
191
|
+
|
|
192
|
+
comps = componentes_de_marca(raiz)
|
|
193
|
+
contagem, por_arquivo = usos(raiz, classes, comps)
|
|
194
|
+
|
|
195
|
+
# marcas gráficas = o vocabulário menos os utilitários de layout, mais os componentes
|
|
196
|
+
marcas = [c for c in classes if not SUFIXO_UTILITARIO.search(c)] + list(comps)
|
|
197
|
+
mortas = [c for c in marcas if contagem[c] == 0]
|
|
198
|
+
unicas = [c for c in marcas if contagem[c] == 1]
|
|
199
|
+
|
|
200
|
+
print(f"Vocabulario grafico: {len(classes)} classes `{prefixo}-*` + {len(comps)} componentes de marca\n")
|
|
201
|
+
for c in sorted(marcas, key=lambda x: -contagem[x]):
|
|
202
|
+
n = contagem[c]
|
|
203
|
+
estado = "MORTA " if n == 0 else ("uso unico" if n == 1 else "")
|
|
204
|
+
print(f" {c:<28}{n:>4} usos {estado}")
|
|
205
|
+
|
|
206
|
+
ok = True
|
|
207
|
+
|
|
208
|
+
# ── cobertura por seção da home ───────────────────────────────────────────
|
|
209
|
+
secoes = secoes_da_receita(raiz)
|
|
210
|
+
if secoes:
|
|
211
|
+
sem_marca = []
|
|
212
|
+
for s in secoes:
|
|
213
|
+
f = arquivo_da_secao(raiz, s)
|
|
214
|
+
if f is None:
|
|
215
|
+
continue
|
|
216
|
+
if not (por_arquivo.get(f, set()) & set(marcas)):
|
|
217
|
+
sem_marca.append(s)
|
|
218
|
+
cobertas = len(secoes) - len(sem_marca)
|
|
219
|
+
frac = cobertas / len(secoes) if secoes else 0
|
|
220
|
+
print(f"\nCobertura da home: {cobertas}/{len(secoes)} secoes carregam ao menos uma marca da casa")
|
|
221
|
+
if sem_marca:
|
|
222
|
+
print(" sem nenhuma marca (poderiam estar em qualquer loja):")
|
|
223
|
+
for s in sem_marca:
|
|
224
|
+
print(f" - {s}")
|
|
225
|
+
if frac < alvo_cob:
|
|
226
|
+
print(f" ABAIXO DO ALVO ({frac:.0%} < {alvo_cob:.0%})")
|
|
227
|
+
ok = False
|
|
228
|
+
else:
|
|
229
|
+
print("\n(sem receita de home encontrada — cobertura por secao nao medida)")
|
|
230
|
+
|
|
231
|
+
# ── cobertura por página ──────────────────────────────────────────────────
|
|
232
|
+
print("\nCobertura por pagina:")
|
|
233
|
+
for pagina, dirs in PAGINAS.items():
|
|
234
|
+
marcas_ali = set()
|
|
235
|
+
for d in dirs:
|
|
236
|
+
base = raiz / d
|
|
237
|
+
if not base.exists():
|
|
238
|
+
continue
|
|
239
|
+
for f, cs in por_arquivo.items():
|
|
240
|
+
try:
|
|
241
|
+
f.relative_to(base)
|
|
242
|
+
except ValueError:
|
|
243
|
+
continue
|
|
244
|
+
marcas_ali |= cs & set(marcas)
|
|
245
|
+
estado = "" if len(marcas_ali) >= minimo else f" <- abaixo de {minimo}"
|
|
246
|
+
print(f" {pagina:<18}{len(marcas_ali):>3} marcas distintas{estado}")
|
|
247
|
+
if len(marcas_ali) < minimo:
|
|
248
|
+
ok = False
|
|
249
|
+
|
|
250
|
+
if mortas or unicas:
|
|
251
|
+
print()
|
|
252
|
+
if mortas:
|
|
253
|
+
print(f" {len(mortas)} marca(s) MORTA(S): " + ", ".join(mortas))
|
|
254
|
+
ok = False
|
|
255
|
+
if unicas:
|
|
256
|
+
print(f" {len(unicas)} marca(s) de USO UNICO: " + ", ".join(unicas))
|
|
257
|
+
print(" uma marca usada uma vez e decoracao; usada em toda secao e assinatura.")
|
|
258
|
+
ok = False
|
|
259
|
+
|
|
260
|
+
sys.exit(0 if ok else 1)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
package/tools/LEIA-ME.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# tools/ — utilitários de desenvolvimento do CLI
|
|
2
|
+
|
|
3
|
+
Nada aqui é publicado: o `package.json` só empacota `bin`, `src` e `template`.
|
|
4
|
+
|
|
5
|
+
- `tokenize-neutrals.mjs` / `tokenize-radius.mjs` — scripts de migração usados uma vez na
|
|
6
|
+
tokenização da foundation (v0.6).
|
|
7
|
+
- `workflow-storefront.legado.js` — orquestrador multiagente da era "construir um storefront
|
|
8
|
+
do zero, sem o CLI". **Saiu do `template/` na v0.15.0** por três motivos: o próprio
|
|
9
|
+
`agents/MANAGER.md` já dizia que ele não se aplica a quem usa o CLI; ele custava ~11k tokens
|
|
10
|
+
de contexto se o agente o abrisse por engano, com "Regras de Ouro" divergentes das do
|
|
11
|
+
MANAGER; e as linhas 13-15 tinham caminhos absolutos da máquina do desenvolvedor, que
|
|
12
|
+
viajavam para dentro do projeto de todo cliente.
|
|
13
|
+
|
|
14
|
+
Fica aqui como referência: o padrão dele (agentes em paralelo que NÃO escrevem em disco,
|
|
15
|
+
devolvem `{path, content}` por schema, e um passo serial grava) é o que a fase multiagente
|
|
16
|
+
do briefing deve adotar.
|
|
17
|
+
|
|
18
|
+
## `notion-doc.mjs` — a página do Notion
|
|
19
|
+
|
|
20
|
+
Gera o conteúdo da página **create-unbox-store — CLI de geração de loja**, em
|
|
21
|
+
`Documentações → Lojas Store` do Notion de tecnologia:
|
|
22
|
+
|
|
23
|
+
`https://app.notion.com/p/3cbf44e324da8125a0afd7fbe6916247`
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node tools/notion-doc.mjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A versão sai do `package.json` e o changelog é extraído do `README.md` — as duas únicas
|
|
30
|
+
fontes da verdade, então a página não pode divergir do pacote. A prosa de apresentação é
|
|
31
|
+
curada dentro do script (uma página para o time não é um despejo do README); as entradas
|
|
32
|
+
`### vX.Y.Z` viram toggles, e a era "Beta" fica condensada em uma linha por versão dentro de
|
|
33
|
+
um toggle único, com o texto integral seguindo no README.
|
|
34
|
+
|
|
35
|
+
**Depois de todo release:** rode o comando e substitua o conteúdo da página com a saída. O
|
|
36
|
+
próprio topo da página diz isso, para quem chegar por lá não editar o changelog à mão.
|
|
37
|
+
|
|
38
|
+
O link fica aqui e não no `README.md` de propósito: aquele README viaja dentro do zip
|
|
39
|
+
entregue ao cliente, e não deve apontar para o Notion interno da Unbox.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// GATE DE NEUTRALIDADE DO TEMPLATE — roda no `prepack`, e o pack FALHA se reprovar.
|
|
3
|
+
//
|
|
4
|
+
// Por que existe: a foundation nasceu de uma loja de temperos e, por duas varreduras, ainda
|
|
5
|
+
// entregava loja de outro ramo com "polvilhe sobre o alimento", "escolha o molho", aba
|
|
6
|
+
// nutricional com traços e selo "Sem glúten" inventado. As varreduras anteriores olharam COR
|
|
7
|
+
// e NOME DE CLIENTE; o que vazava era VOCABULÁRIO DE DOMÍNIO. Este gate olha isso.
|
|
8
|
+
//
|
|
9
|
+
// Por que aqui e NÃO no projeto gerado: uma loja de alimento pode e deve dizer "polvilhe"
|
|
10
|
+
// depois do briefing. O que não pode é a FOUNDATION dizer. Então a checagem é no repositório
|
|
11
|
+
// do CLI, antes de empacotar — e nunca viaja para o cliente.
|
|
12
|
+
//
|
|
13
|
+
// Escopo: código e docs que o agente lê ou o cliente vê. Comentário conta: comentário
|
|
14
|
+
// ensina o agente a escrever igual.
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const RAIZ = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
const TEMPLATE = path.join(RAIZ, "template");
|
|
22
|
+
const EXTS = new Set([".ts", ".tsx", ".md", ".json", ".css", ".svg", ".txt", ".mjs"]);
|
|
23
|
+
const IGNORAR_DIRS = new Set(["node_modules", ".next", "qa"]);
|
|
24
|
+
|
|
25
|
+
// [regex, por quê]. Só termos SEM uso legítimo numa foundation neutra. "alimento",
|
|
26
|
+
// "nutricional", "glúten" ficam de fora de propósito: aparecem em código CONDICIONAL correto
|
|
27
|
+
// ("tabela nutricional só se for alimento") e o gate não pode punir isso.
|
|
28
|
+
const PROIBIDOS = [
|
|
29
|
+
[/\bbadia\b|\bblessy\b|\bzetona\b|\bzétona\b|\boddie\b/i, "nome de cliente"],
|
|
30
|
+
[/polvilh/i, "instrução de uso de tempero"],
|
|
31
|
+
[/\btemperos?\b|\btempere\b|\btemperad[oa]s?\b/i, "vocabulário de tempero"],
|
|
32
|
+
[/\bespeciarias?\b|\bcondimentos?\b/i, "vocabulário de tempero"],
|
|
33
|
+
[/p[aá]prica|or[eé]gano|cominho|a[cç]afr[aã]o|chimichurri|lemon pepper|sriracha|garlic|alho em p[oó]/i, "produto de tempero"],
|
|
34
|
+
[/\bmolhos?\b(?! de contato)/i, "produto de alimentação"],
|
|
35
|
+
[/marinad[ao]s?|churrasco|festa junina/i, "contexto de alimentação"],
|
|
36
|
+
[/\bsabor(es)?\b/i, "copy de alimentação ('mais sabor')"],
|
|
37
|
+
[/porção 1 ?g\b/i, "porção de tempero cravada"],
|
|
38
|
+
[/Cliente [A-C]\. .*(tempero|prato|receita de)/i, "depoimento de alimentação"],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Trechos permitidos: a explicação do próprio bug, em comentário, precisa poder citar a
|
|
42
|
+
// palavra uma vez. Mantém a lista CURTA — todo item aqui é uma exceção que alguém revisou.
|
|
43
|
+
const PERMITIDOS = [
|
|
44
|
+
"tools/", // este diretório
|
|
45
|
+
"README.md::### v0.15", // changelog descreve o bug
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const achados = [];
|
|
49
|
+
function varrer(dir) {
|
|
50
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
const p = path.join(dir, ent.name);
|
|
52
|
+
if (ent.isDirectory()) { if (!IGNORAR_DIRS.has(ent.name)) varrer(p); continue; }
|
|
53
|
+
if (!EXTS.has(path.extname(ent.name))) continue;
|
|
54
|
+
const rel = path.relative(RAIZ, p);
|
|
55
|
+
const linhas = fs.readFileSync(p, "utf8").split("\n");
|
|
56
|
+
linhas.forEach((linha, i) => {
|
|
57
|
+
for (const [re, motivo] of PROIBIDOS) {
|
|
58
|
+
if (!re.test(linha)) continue;
|
|
59
|
+
const chave = `${rel}::${linha.trim().slice(0, 30)}`;
|
|
60
|
+
if (PERMITIDOS.some((ok) => rel.startsWith(ok) || chave.startsWith(ok))) continue;
|
|
61
|
+
achados.push({ rel, n: i + 1, motivo, texto: linha.trim().slice(0, 110) });
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
varrer(TEMPLATE);
|
|
68
|
+
|
|
69
|
+
// ── Travessão no texto que o CLI IMPRIME ─────────────────────────────────────
|
|
70
|
+
// O template tem gate próprio (scripts/check-unbox-brand.mjs). Aqui pega o outro lado: as
|
|
71
|
+
// perguntas, avisos e mensagens do wizard, e os cabeçalhos que o CLI escreve dentro do projeto.
|
|
72
|
+
// Comentário de código fica de fora: é nota de engenharia, não texto para quem lê a tela.
|
|
73
|
+
function semComentario(linha, dentroDeBloco) {
|
|
74
|
+
let out = "", aspas = null, bloco = dentroDeBloco;
|
|
75
|
+
for (let i = 0; i < linha.length; i++) {
|
|
76
|
+
const c = linha[i], prox = linha[i + 1];
|
|
77
|
+
if (bloco) { if (c === "*" && prox === "/") { bloco = false; i++; } continue; }
|
|
78
|
+
if (aspas) {
|
|
79
|
+
if (c === "\\") { out += c + (prox ?? ""); i++; continue; }
|
|
80
|
+
if (c === aspas) aspas = null;
|
|
81
|
+
out += c; continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === '"' || c === "'" || c === "`") { aspas = c; out += c; continue; }
|
|
84
|
+
if (c === "/" && prox === "/") break;
|
|
85
|
+
if (c === "/" && prox === "*") { bloco = true; i++; continue; }
|
|
86
|
+
out += c;
|
|
87
|
+
}
|
|
88
|
+
return { texto: out, bloco };
|
|
89
|
+
}
|
|
90
|
+
for (const arquivo of ["bin/cli.js", "src/theme.js", "src/presets.js", "src/colors.js"]) {
|
|
91
|
+
const caminho = path.join(RAIZ, arquivo);
|
|
92
|
+
if (!fs.existsSync(caminho)) continue;
|
|
93
|
+
let bloco = false;
|
|
94
|
+
fs.readFileSync(caminho, "utf8").split("\n").forEach((linha, i) => {
|
|
95
|
+
const r = semComentario(linha, bloco);
|
|
96
|
+
bloco = r.bloco;
|
|
97
|
+
if (r.texto.includes("\u2014")) {
|
|
98
|
+
achados.push({ rel: arquivo, n: i + 1, motivo: "travessão no texto impresso pelo CLI", texto: linha.trim().slice(0, 110) });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (achados.length) {
|
|
104
|
+
console.error(`\n✗ PACK BLOQUEADO — a foundation tem ${achados.length} resíduo(s) de domínio:\n`);
|
|
105
|
+
for (const a of achados) console.error(` ${a.rel}:${a.n} [${a.motivo}]\n ${a.texto}`);
|
|
106
|
+
console.error("\n A foundation é neutra: nada de vocabulário de um ramo específico, nem em comentário.");
|
|
107
|
+
console.error(" E o texto que o CLI imprime não usa travessão: vírgula, dois-pontos, ponto ou \"·\".");
|
|
108
|
+
console.error(" Loja de alimento escreve isso DEPOIS, no briefing. Corrija e rode de novo.\n");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
console.log("✓ Foundation neutra (sem vocabulário de domínio nem nome de cliente)");
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// GERADOR DA PÁGINA DO NOTION — node tools/notion-doc.mjs
|
|
3
|
+
//
|
|
4
|
+
// Emite o conteúdo da página "create-unbox-store — CLI de geração de loja" em Notion-flavored
|
|
5
|
+
// Markdown, pronto pra ser passado ao update-page do MCP do Notion.
|
|
6
|
+
//
|
|
7
|
+
// Existe pra que "manter a página atualizada" seja um comando, não um trabalho manual que
|
|
8
|
+
// ninguém lembra de fazer no dia do release. A versão vem do package.json e o changelog é
|
|
9
|
+
// extraído do README.md — as duas únicas fontes da verdade. Rode depois de todo release e
|
|
10
|
+
// atualize a página com a saída.
|
|
11
|
+
//
|
|
12
|
+
// A prosa de apresentação é curada aqui (uma página de Notion para o time não é um despejo
|
|
13
|
+
// do README). O changelog é mecânico: toda entrada "### vX.Y.Z" do README vira um toggle.
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const RAIZ = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(RAIZ, "package.json"), "utf8"));
|
|
21
|
+
const readme = fs.readFileSync(path.join(RAIZ, "README.md"), "utf8");
|
|
22
|
+
const VERSAO = pkg.version;
|
|
23
|
+
|
|
24
|
+
// ── Changelog: extrai as entradas "### vX" / "### Beta vX" do README ────────────────────
|
|
25
|
+
function extrairChangelog() {
|
|
26
|
+
const linhas = readme.split("\n");
|
|
27
|
+
const ini = linhas.findIndex((l) => l.trim() === "## Changelog");
|
|
28
|
+
if (ini === -1) throw new Error("seção '## Changelog' não encontrada no README.md");
|
|
29
|
+
|
|
30
|
+
const entradas = [];
|
|
31
|
+
let atual = null;
|
|
32
|
+
for (const linha of linhas.slice(ini + 1)) {
|
|
33
|
+
const m = linha.match(/^### (.+)$/);
|
|
34
|
+
if (m) {
|
|
35
|
+
if (atual) entradas.push(atual);
|
|
36
|
+
atual = { titulo: m[1].trim(), corpo: [] };
|
|
37
|
+
} else if (atual) {
|
|
38
|
+
atual.corpo.push(linha);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (atual) entradas.push(atual);
|
|
42
|
+
return entradas;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Markdown do README → Notion-flavored. As diferenças que importam:
|
|
46
|
+
// • parágrafo quebrado em várias linhas vira UMA linha (no Notion, \n encerra o bloco);
|
|
47
|
+
// • item de lista com continuação indentada é juntado no mesmo item;
|
|
48
|
+
// • o resto (negrito, código inline, links) é compatível.
|
|
49
|
+
function paraNotion(corpo) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const bruta of corpo) {
|
|
52
|
+
const linha = bruta.replace(/\s+$/, "");
|
|
53
|
+
if (!linha.trim()) { out.push(""); continue; }
|
|
54
|
+
const ehItem = /^\s*[-*]\s+/.test(linha);
|
|
55
|
+
const ehTitulo = /^#{1,6}\s/.test(linha);
|
|
56
|
+
const ehCodigo = /^\s*```/.test(linha);
|
|
57
|
+
const continuacao = /^\s+\S/.test(linha) && !ehItem && !ehCodigo;
|
|
58
|
+
if ((continuacao || (!ehItem && !ehTitulo && !ehCodigo)) && out.length && out[out.length - 1]) {
|
|
59
|
+
// continua o bloco anterior em vez de abrir um novo
|
|
60
|
+
const ant = out[out.length - 1];
|
|
61
|
+
if (!/^\s*```/.test(ant)) { out[out.length - 1] = ant + " " + linha.trim(); continue; }
|
|
62
|
+
}
|
|
63
|
+
out.push(ehItem ? "- " + linha.replace(/^\s*[-*]\s+/, "") : linha.trim());
|
|
64
|
+
}
|
|
65
|
+
return escaparAngulos(out.join("\n").replace(/\n{3,}/g, "\n\n").trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// O Notion lê "<algo>" fora de crase como bloco/tag XML. Escapa só o que está solto —
|
|
69
|
+
// dentro de crase (código inline e blocos) o conteúdo é literal e não pode ser tocado.
|
|
70
|
+
function escaparAngulos(texto) {
|
|
71
|
+
const partes = texto.split(/(```[\s\S]*?```|`[^`\n]*`)/g);
|
|
72
|
+
return partes.map((p, i) => (i % 2 ? p : p.replace(/</g, "\\<").replace(/>/g, "\\>"))).join("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entradas = extrairChangelog();
|
|
76
|
+
// Detalhe integral só na era da arquitetura atual (v0.12.13+, quando o versionamento foi
|
|
77
|
+
// unificado). O histórico anterior vira uma linha por versão dentro de um toggle: ninguém lê
|
|
78
|
+
// 35 changelogs completos, e o texto integral continua no README do pacote.
|
|
79
|
+
const DETALHADAS = entradas.findIndex((e) => /^Beta v/.test(e.titulo));
|
|
80
|
+
const corte = DETALHADAS === -1 ? entradas.length : DETALHADAS;
|
|
81
|
+
|
|
82
|
+
const detalhadas = entradas.slice(0, corte).map((e, i) => {
|
|
83
|
+
const corpo = paraNotion(e.corpo);
|
|
84
|
+
const marca = i === 0 ? " · atual" : "";
|
|
85
|
+
return `### ${e.titulo}${marca} {toggle="true"}\n` +
|
|
86
|
+
corpo.split("\n").map((l) => (l ? "\t" + l : "")).join("\n");
|
|
87
|
+
}).join("\n");
|
|
88
|
+
|
|
89
|
+
// Uma linha por versão antiga: título + primeira frase do corpo.
|
|
90
|
+
const antigas = entradas.slice(corte).map((e) => {
|
|
91
|
+
const texto = paraNotion(e.corpo)
|
|
92
|
+
.split("\n").map((l) => l.replace(/^-\s*/, "").trim()).filter(Boolean)[0] ?? "";
|
|
93
|
+
let resumo = texto.split(/(?<=\.)\s/)[0].slice(0, 170).replace(/[\s:—-]+$/, "");
|
|
94
|
+
if (resumo.length >= 170) resumo += "…";
|
|
95
|
+
return `- **${e.titulo}** — ${resumo}`;
|
|
96
|
+
}).join("\n");
|
|
97
|
+
|
|
98
|
+
const changelog = detalhadas + `
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary>Histórico anterior (${entradas.length - corte} versões, era "Beta")</summary>
|
|
102
|
+
` + antigas.split("\n").map((l) => "\t" + l).join("\n") + `
|
|
103
|
+
\t
|
|
104
|
+
\tO texto integral de cada uma está no \`README.md\` dentro do pacote.
|
|
105
|
+
</details>`;
|
|
106
|
+
|
|
107
|
+
// ── A página ────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
const pagina = `<callout icon="📦" color="blue_bg">
|
|
109
|
+
**Versão atual: v${VERSAO}** — entregue como \`CLI - Unbox v${VERSAO}.zip\`, que contém \`create-unbox-store.tgz\` e o README completo.
|
|
110
|
+
O nome do tarball é sempre \`create-unbox-store.tgz\`, **sem versão** — a versão fica dentro do pacote. Zips antigos traziam a versão no nome do arquivo e isso gerava erro de \`npx\` quando o comando não batia com o nome real.
|
|
111
|
+
</callout>
|
|
112
|
+
|
|
113
|
+
<callout icon="🔄" color="gray_bg">
|
|
114
|
+
Esta página é gerada a partir do \`README.md\` e do \`package.json\` do CLI. Para atualizá-la depois de um release, rode \`node tools/notion-doc.mjs\` no repositório do CLI e substitua o conteúdo com a saída. Não edite o changelog aqui à mão: ele será sobrescrito.
|
|
115
|
+
</callout>
|
|
116
|
+
|
|
117
|
+
## O que é
|
|
118
|
+
|
|
119
|
+
CLI de dependência zero (só \`prompts\`) que gera um **storefront Next.js 15 completo**, integrado com a API headless da Unbox, já com a marca do cliente aplicada: layout, fontes, neutros, cores e nome da loja.
|
|
120
|
+
|
|
121
|
+
O que sai do CLI é **fundação**, não loja pronta. A personalização de verdade acontece logo depois, no briefing de marca conduzido pelo Claude Code — e é de propósito: o cliente é entrevistado **antes** de ver qualquer coisa, para não ancorar a expectativa dele no padrão.
|
|
122
|
+
|
|
123
|
+
## Como usar
|
|
124
|
+
|
|
125
|
+
Único pré-requisito da máquina: **Node.js** ([nodejs.org](https://nodejs.org), botão LTS). Coloque o \`create-unbox-store.tgz\` na pasta onde quer criar o projeto e rode:
|
|
126
|
+
|
|
127
|
+
\`\`\`bash
|
|
128
|
+
npx --package=./create-unbox-store.tgz create-unbox-store
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
<callout icon="⚠️" color="yellow_bg">
|
|
132
|
+
Use sempre a forma com \`--package=\`. O \`npx ./create-unbox-store.tgz\` sem ela dá erro de permissão em algumas versões do npm.
|
|
133
|
+
</callout>
|
|
134
|
+
|
|
135
|
+
O CLI pergunta 12 coisas: nome do projeto e da loja, cor primária e de CTA, site e Instagram da marca, objetivo em uma frase, **estilo visual** (Essencial, Promocional, Editorial ou Boutique), credenciais da Unbox, tipo de checkout, o segredo de captcha (só com key de parceiro) e se roda \`npm install\`. Tudo menos as credenciais fica em \`marca/briefing.json\`, para o agente de marca não reperguntar.
|
|
136
|
+
|
|
137
|
+
Sem credenciais em mãos, responda "não": o projeto sobe em **modo mockup** com o layout completo e sem dados reais. Preenche o \`.env.local\` depois.
|
|
138
|
+
|
|
139
|
+
### O passo seguinte não é \`npm run dev\`
|
|
140
|
+
|
|
141
|
+
\`\`\`bash
|
|
142
|
+
cd <nome-do-projeto>
|
|
143
|
+
claude
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
O Claude Code abre direto no briefing de marca e conduz a personalização. Encerrado o briefing, o próprio agente remove a chave que força esse modo e as sessões seguintes voltam ao normal.
|
|
147
|
+
|
|
148
|
+
### Sem interação (CI ou testes)
|
|
149
|
+
|
|
150
|
+
\`\`\`bash
|
|
151
|
+
npx --package=./create-unbox-store.tgz create-unbox-store minha-loja --yes --no-install
|
|
152
|
+
\`\`\`
|
|
153
|
+
|
|
154
|
+
Use \`--estilo <essencial|promocional|editorial|boutique>\` para escolher o preset sem interação.
|
|
155
|
+
|
|
156
|
+
## O que o projeto gerado traz
|
|
157
|
+
|
|
158
|
+
Next.js 15 (App Router) + Tailwind v4 + shadcn/ui sobre Base UI, com catálogo, carrinho, checkout, área do cliente (login por OTP, pedidos, assinaturas, endereços), páginas legais, PWA e SEO básico já implementados. \`DEPLOY.md\` e \`QA.md\` dentro do projeto trazem o checklist de publicação na Vercel.
|
|
159
|
+
|
|
160
|
+
O projeto também vem com um \`CLAUDE.md\` que mapeia "quero mudar X, mexo em Y", e com \`agents/MANAGER.md\`, que descreve a ordem dos passos: **15 (Branding) primeiro**, depois os opcionais 13 (CRO) e 14 (SEO), e por último 12 (Deploy).
|
|
161
|
+
|
|
162
|
+
### Portões de qualidade
|
|
163
|
+
|
|
164
|
+
| Comando | O que faz |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| \`npm run typecheck\` | o gate mais barato, rode primeiro |
|
|
167
|
+
| \`npm run build\` | dispara o prebuild de marca (bloqueia sem o selo Powered by Unbox ou sem o GTM) |
|
|
168
|
+
| \`npm run unbox:honestidade\` | promessa comercial ou prova social sem lastro |
|
|
169
|
+
| \`npm run unbox:receita\` | variedade da composição da home (mede, não bloqueia) |
|
|
170
|
+
| \`npm run unbox:qa\` | screenshots do QA com emulação de dispositivo |
|
|
171
|
+
| \`npm run unbox:dump\` | imprime o catálogo e as promoções **reais** da loja |
|
|
172
|
+
|
|
173
|
+
## Antes de publicar — decisões que não são do CLI
|
|
174
|
+
|
|
175
|
+
A fundação traz placeholders que funcionam mas **precisam de uma decisão consciente**. O CLI não resolve isso sozinho de propósito: seria tomar a decisão no lugar de quem publica.
|
|
176
|
+
|
|
177
|
+
1. **Prova social: a foundation não traz nenhuma.** Sem avaliação real em \`lib/enrichment/products.json\`, a loja não mostra nota, estrelas, contagem nem depoimento (o bloco some, não cai em placeholder). Decida antes de publicar: popular com dados reais ou publicar sem prova social. O agente de Branding pede esse material no briefing; o \`unbox:honestidade\` lista qualquer número ou promessa que entrar sem lastro.
|
|
178
|
+
2. **CRO: 1 de 10 módulos na fundação.** Só a barra de frete grátis vem pronta. Os outros nove são do agente 13, que o briefing oferece ao terminar (o wizard não pergunta mais sobre isso).
|
|
179
|
+
3. **SEO avançado: o básico está, os 7 módulos não.** JSON-LD de produto, Organization + WebSite/SearchAction, imagem Open Graph gerada e canonical por página já vêm. Breadcrumbs, FAQ schema, OG por produto e templates de título/description são do agente 14.
|
|
180
|
+
4. **Kits e combos vêm vazios.** A seção só aparece depois que alguém popular os kits reais.
|
|
181
|
+
|
|
182
|
+
<callout icon="🧭" color="gray_bg">
|
|
183
|
+
A regra que orienta tudo isso: **um campo vazio declarado é melhor que um campo plausível inventado.** Quando falta material real, o certo é registrar a pendência, não preencher com algo verossímil. É no briefing e na fundação que um valor plausível vira lei sem ninguém questionar.
|
|
184
|
+
</callout>
|
|
185
|
+
|
|
186
|
+
## Changelog
|
|
187
|
+
|
|
188
|
+
${changelog}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
process.stdout.write(pagina);
|
|
Binary file
|