@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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contraste.js — mede contraste REAL no navegador, e classifica a paleta da marca.
|
|
3
|
+
*
|
|
4
|
+
* Cole no console do DevTools com a loja aberta, ou rode via CDP. Duas funções:
|
|
5
|
+
*
|
|
6
|
+
* contraste() varre o texto visível da página e lista o que reprova
|
|
7
|
+
* classificar("F20A64") diz se a cor é SUPERFÍCIE (pode ter texto em cima) ou ACENTO
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* OS TRÊS ERROS QUE ESTA VERSÃO NÃO COMETE
|
|
11
|
+
* A mesma ferramenta foi escrita errada três vezes numa loja, sempre respondendo com
|
|
12
|
+
* confiança. Os três estão tratados aqui, e cada um está comentado no ponto onde importa:
|
|
13
|
+
*
|
|
14
|
+
* 1. `getComputedStyle().color` NÃO é sempre rgb(). Com qualquer opacidade (e o Tailwind v4
|
|
15
|
+
* gera isso em todo `text-cor/60`) o navegador devolve `oklab(...)`. Parser de rgb()
|
|
16
|
+
* silenciosamente lê 0,0,0 e reprova texto claro sem motivo.
|
|
17
|
+
* 2. Fundo não é a camada mais próxima. Véu semitransparente empilhado muda a cor efetiva:
|
|
18
|
+
* é preciso COMPOR todas as camadas até achar uma opaca.
|
|
19
|
+
* 3. Alpha entra UMA vez só, na composição sobre o fundo. Medido neste navegador: o
|
|
20
|
+
* ImageData do canvas 2D é NÃO pré-multiplicado (`rgba(255,255,255,.5)` volta
|
|
21
|
+
* [255,255,255,128]), então "desfazer a pré-multiplicação" dividindo por alpha é o erro,
|
|
22
|
+
* não a correção — numa cor fora do sRGB isso estoura a faixa. Aplicar o alpha duas vezes,
|
|
23
|
+
* de qualquer um dos dois jeitos, reprova texto claro sobre escuro e APROVA texto escuro
|
|
24
|
+
* sobre claro que na verdade falha.
|
|
25
|
+
*
|
|
26
|
+
* E duas coisas que a ferramenta NÃO resolve, ditas aqui para ninguém confiar demais:
|
|
27
|
+
* • texto sobre foto: é preciso amostrar os pixels da imagem sob o texto e compor o véu por
|
|
28
|
+
* cima. Aqui isso é sinalizado como "sobre imagem: medir à mão", nunca aprovado no escuro.
|
|
29
|
+
* • captura de tela: esqueleto de carregamento e imagem não pintada já enganaram a leitura.
|
|
30
|
+
* Conclusão tirada de screenshot precisa ser confirmada no DOM.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Converte QUALQUER cor que o navegador devolva em [r,g,b,a] 0-255. */
|
|
34
|
+
function corParaRgba(valor) {
|
|
35
|
+
if (!valor || valor === "transparent") return [0, 0, 0, 0];
|
|
36
|
+
// Hex direto, sem canvas: é o formato que a paleta da marca usa, e assim `classificar()`
|
|
37
|
+
// também roda fora do navegador (Node, script de build) sem depender de DOM.
|
|
38
|
+
const h = String(valor).trim();
|
|
39
|
+
if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(h)) {
|
|
40
|
+
const p = h.slice(1);
|
|
41
|
+
const par = p.length <= 4 ? p.split("").map((c) => c + c) : p.match(/../g);
|
|
42
|
+
const [r, g, b, a] = par.map((x) => parseInt(x, 16));
|
|
43
|
+
return [r, g, b, a === undefined ? 1 : a / 255];
|
|
44
|
+
}
|
|
45
|
+
// (1) oklab/oklch/color(): o navegador devolve isso sempre que há opacidade. Em vez de
|
|
46
|
+
// escrever um parser de espaço de cor, deixamos o próprio navegador converter, pintando
|
|
47
|
+
// 1px num canvas — o resultado volta em RGBA de verdade.
|
|
48
|
+
if (!/^rgba?\(/.test(valor)) {
|
|
49
|
+
const c = document.createElement("canvas");
|
|
50
|
+
c.width = c.height = 1;
|
|
51
|
+
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
52
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
53
|
+
ctx.fillStyle = valor;
|
|
54
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
55
|
+
const d = ctx.getImageData(0, 0, 1, 1).data;
|
|
56
|
+
// (3) O ImageData do canvas 2D é NÃO pré-multiplicado: `rgba(255,255,255,.5)` volta como
|
|
57
|
+
// [255,255,255,128], não [128,128,128,128] (medido neste navegador, não suposto). Ou seja,
|
|
58
|
+
// os canais já são a cor pura e dividir por alpha é o erro, não a correção: numa cor fora
|
|
59
|
+
// do sRGB isso estourava a faixa (um oklch deu R=404). O alpha entra uma vez só, na
|
|
60
|
+
// composição sobre o fundo.
|
|
61
|
+
return [d[0], d[1], d[2], d[3] / 255];
|
|
62
|
+
}
|
|
63
|
+
const n = valor.match(/[\d.]+/g).map(Number);
|
|
64
|
+
return [n[0], n[1], n[2], n[3] === undefined ? 1 : n[3]];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Compõe `frente` (com alpha) sobre `fundo` (opaco). */
|
|
68
|
+
function compor([r, g, b, a], [R, G, B]) {
|
|
69
|
+
return [r * a + R * (1 - a), g * a + G * (1 - a), b * a + B * (1 - a), 1];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function luminancia([r, g, b]) {
|
|
73
|
+
const f = (v) => {
|
|
74
|
+
v /= 255;
|
|
75
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
76
|
+
};
|
|
77
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function razao(corA, corB) {
|
|
81
|
+
const [L1, L2] = [luminancia(corA), luminancia(corB)].sort((x, y) => y - x);
|
|
82
|
+
return (L1 + 0.05) / (L2 + 0.05);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Fundo efetivo de um elemento: sobe a árvore COMPONDO cada camada até achar uma opaca. */
|
|
86
|
+
function fundoEfetivo(el) {
|
|
87
|
+
const camadas = [];
|
|
88
|
+
let n = el;
|
|
89
|
+
let sobreImagem = false;
|
|
90
|
+
while (n && n !== document.documentElement.parentNode) {
|
|
91
|
+
const s = getComputedStyle(n);
|
|
92
|
+
if (s.backgroundImage && s.backgroundImage !== "none") sobreImagem = true;
|
|
93
|
+
const c = corParaRgba(s.backgroundColor);
|
|
94
|
+
if (c[3] > 0) {
|
|
95
|
+
camadas.push(c);
|
|
96
|
+
// (2) só paramos numa camada OPACA: véu translúcido continua subindo, porque a cor
|
|
97
|
+
// efetiva depende de tudo o que está atrás dele.
|
|
98
|
+
if (c[3] === 1) break;
|
|
99
|
+
}
|
|
100
|
+
n = n.parentElement;
|
|
101
|
+
}
|
|
102
|
+
let fundo = [255, 255, 255, 1]; // o branco do papel, só se nada opaco apareceu
|
|
103
|
+
for (const c of camadas.reverse()) fundo = compor(c, fundo);
|
|
104
|
+
return { fundo, sobreImagem };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const MIN_GRANDE = 3.0; // >=24px, ou >=18.66px em negrito
|
|
108
|
+
const MIN_NORMAL = 4.5;
|
|
109
|
+
|
|
110
|
+
/** Varre o texto visível e devolve o que reprova. */
|
|
111
|
+
function contraste({ apenasFalhas = true } = {}) {
|
|
112
|
+
const saida = [];
|
|
113
|
+
for (const el of document.querySelectorAll("body *")) {
|
|
114
|
+
const texto = [...el.childNodes].filter((n) => n.nodeType === 3 && n.textContent.trim()).map((n) => n.textContent.trim()).join(" ");
|
|
115
|
+
if (!texto) continue;
|
|
116
|
+
const r = el.getBoundingClientRect();
|
|
117
|
+
if (r.width === 0 || r.height === 0) continue;
|
|
118
|
+
const s = getComputedStyle(el);
|
|
119
|
+
if (s.visibility === "hidden" || s.display === "none" || Number(s.opacity) === 0) continue;
|
|
120
|
+
|
|
121
|
+
const { fundo, sobreImagem } = fundoEfetivo(el);
|
|
122
|
+
const frente = compor(corParaRgba(s.color), fundo);
|
|
123
|
+
const px = parseFloat(s.fontSize);
|
|
124
|
+
const grande = px >= 24 || (px >= 18.66 && Number(s.fontWeight) >= 700);
|
|
125
|
+
const minimo = grande ? MIN_GRANDE : MIN_NORMAL;
|
|
126
|
+
const valor = razao(frente, fundo);
|
|
127
|
+
const passa = valor >= minimo;
|
|
128
|
+
if (apenasFalhas && passa && !sobreImagem) continue;
|
|
129
|
+
saida.push({
|
|
130
|
+
texto: texto.slice(0, 60),
|
|
131
|
+
razao: Number(valor.toFixed(2)),
|
|
132
|
+
minimo,
|
|
133
|
+
passa,
|
|
134
|
+
// Texto sobre foto NÃO é aprovado por esta conta: o fundo real são os pixels da imagem.
|
|
135
|
+
aviso: sobreImagem ? "sobre imagem: medir à mão (amostrar os pixels sob o texto)" : undefined,
|
|
136
|
+
seletor: el.tagName.toLowerCase() + (el.className && typeof el.className === "string" ? "." + el.className.split(/\s+/).slice(0, 2).join(".") : ""),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
console.table(saida);
|
|
140
|
+
return saida;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Classifica uma cor da marca. É o uso principal: define se a cor pode receber texto.
|
|
145
|
+
* superfície = passa contra tinta OU contra branco (>=4.5) → pode ser fundo de texto
|
|
146
|
+
* acento = não passa em nenhum → contorno, ícone, faixa. NUNCA fundo de texto.
|
|
147
|
+
*
|
|
148
|
+
* O CLI não "corrige" cor de marca sozinho para passar no WCAG: isso descaracteriza a marca.
|
|
149
|
+
* Ele mede, aplica o que a marca pediu, e escreve o número ao lado do token com a alternativa.
|
|
150
|
+
*/
|
|
151
|
+
function classificar(hex, { tinta = "#18181B", claro = "#FFFFFF" } = {}) {
|
|
152
|
+
const c = corParaRgba(hex);
|
|
153
|
+
const contraTinta = razao(c, corParaRgba(tinta));
|
|
154
|
+
const contraClaro = razao(c, corParaRgba(claro));
|
|
155
|
+
const papel = Math.max(contraTinta, contraClaro) >= 4.5 ? "superfície" : "acento";
|
|
156
|
+
const r = {
|
|
157
|
+
cor: hex,
|
|
158
|
+
contraTinta: Number(contraTinta.toFixed(2)),
|
|
159
|
+
contraBranco: Number(contraClaro.toFixed(2)),
|
|
160
|
+
papel,
|
|
161
|
+
textoPorCima: papel === "superfície" ? (contraTinta > contraClaro ? "tinta" : "branco") : "NENHUM",
|
|
162
|
+
recomendacao:
|
|
163
|
+
papel === "superfície"
|
|
164
|
+
? `use como fundo com texto ${contraTinta > contraClaro ? "escuro" : "branco"} por cima`
|
|
165
|
+
: "contorno, sombra, ícone ou faixa decorativa. Para usar como fundo de texto, gere a versão funda (-deep) e ponha branco em cima",
|
|
166
|
+
};
|
|
167
|
+
console.log(`${hex}: ${papel} (tinta ${r.contraTinta} · branco ${r.contraBranco}) — ${r.recomendacao}`);
|
|
168
|
+
return r;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Sem `export`: o arquivo é feito para ser COLADO NO CONSOLE do DevTools, e `export` quebra
|
|
172
|
+
// isso com "Unexpected token". No navegador as funções ficam em window; em Node, no module.
|
|
173
|
+
if (typeof window !== "undefined") {
|
|
174
|
+
Object.assign(window, { contraste, classificar, razao });
|
|
175
|
+
console.log("contraste.js pronto: contraste() varre a página · classificar('#HEX') define o papel da cor");
|
|
176
|
+
}
|
|
177
|
+
if (typeof module !== "undefined") module.exports = { contraste, classificar, razao };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Raio-X da loja real: imprime promoções (shopSales), cupons, política de assinatura,
|
|
2
|
+
// formas de pagamento e catálogo, direto do backend Unbox. Use ANTES de preencher
|
|
3
|
+
// lib/store-config.ts — a regra de ouro da loja é só prometer o que aparece aqui.
|
|
4
|
+
//
|
|
5
|
+
// npm run unbox:dump
|
|
6
|
+
import "./load-env"; // SEMPRE o primeiro import — parser de env idêntico ao do app
|
|
7
|
+
import { UnboxClient } from "../lib/unbox/client";
|
|
8
|
+
|
|
9
|
+
const client = new UnboxClient({
|
|
10
|
+
apiKey: process.env.UNBOX_API_KEY ?? "",
|
|
11
|
+
shopId: process.env.UNBOX_SHOP_ID ?? "",
|
|
12
|
+
partnerApiKey: process.env.UNBOX_PARTNER_API_KEY,
|
|
13
|
+
partnerGqlUrl: process.env.UNBOX_PARTNER_GRAPHQL_URL,
|
|
14
|
+
captchaBypass: process.env.UNBOX_CAPTCHA_BYPASS,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const brl = (n: number) => `R$${n.toFixed(2).replace(".", ",")}`;
|
|
18
|
+
const h = (title: string) => console.log(`\n═══ ${title} ═══`);
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
await client.signIn(process.env.UNBOX_USER!, process.env.UNBOX_PASS!);
|
|
22
|
+
const shop = await client.getShop(process.env.UNBOX_SHOP_SLUG ?? "");
|
|
23
|
+
|
|
24
|
+
h("LOJA");
|
|
25
|
+
console.log(`${shop.name} (slug: ${shop.slug}, id: ${shop._id})`);
|
|
26
|
+
console.log(`Pagamentos: cartão ${shop.acceptsCreditCard ? "sim" : "não"} · boleto ${shop.acceptsBoleto ? "sim" : "não"}`);
|
|
27
|
+
|
|
28
|
+
h("PROMOÇÕES AUTOMÁTICAS (shopSales)");
|
|
29
|
+
const sales: any[] = shop.shopSales ?? [];
|
|
30
|
+
if (!sales.length) console.log("Nenhuma. NÃO exiba desconto, brinde ou régua de frete na loja.");
|
|
31
|
+
for (const s of sales) {
|
|
32
|
+
console.log(`• ${s.label ?? s.code ?? s._id} [${s.calculation?.__typename ?? "?"}] ${s.enabled ? "ATIVA" : "desativada"}`);
|
|
33
|
+
for (const t of s.calculation?.tiers ?? []) {
|
|
34
|
+
console.log(` a partir de ${brl(t.cartSubtotalGTE)} → brinde: ${t.catalogProductVariant?.title ?? t.catalogProductVariant?._id}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const tierSale = sales.find((s) => s.enabled && s.calculation?.__typename === "CalculationFreeItemByTier");
|
|
38
|
+
if (tierSale?.calculation?.tiers?.length) {
|
|
39
|
+
console.log(` → GIFT_TIERS sugerido p/ lib/store-config.ts:`);
|
|
40
|
+
console.log(` ${JSON.stringify(tierSale.calculation.tiers.map((t: any) => ({ name: t.catalogProductVariant?.title ?? "Brinde", threshold: t.cartSubtotalGTE })))}`);
|
|
41
|
+
}
|
|
42
|
+
if (sales.some((s) => s.enabled && s.calculation?.__typename === "CalculationDiscountByPaymentMethod")) {
|
|
43
|
+
console.log(` → Existe desconto por forma de pagamento: confirme o percentual no painel e preencha PIX_DISCOUNT_PCT.`);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(` → Sem CalculationDiscountByPaymentMethod: mantenha PIX_DISCOUNT_PCT = 0.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
h("CUPONS (discountCodes)");
|
|
49
|
+
try {
|
|
50
|
+
const codes = await client.listDiscountCodes(50);
|
|
51
|
+
if (!codes.nodes?.length) console.log("Nenhum cupom cadastrado.");
|
|
52
|
+
for (const c of codes.nodes ?? []) {
|
|
53
|
+
console.log(`• ${c.code ?? c.label} [${c.calculation?.__typename ?? "?"}] ${c.enabled ? "ativo" : "inativo"}`);
|
|
54
|
+
}
|
|
55
|
+
} catch (e: any) {
|
|
56
|
+
console.log(`(indisponível: ${e.message})`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
h("ASSINATURA (recurringOrdersPolicy)");
|
|
60
|
+
const pol = shop.recurringOrdersPolicy;
|
|
61
|
+
if (!pol?.enabled) {
|
|
62
|
+
console.log("Desabilitada. NÃO exiba oferta de assinatura.");
|
|
63
|
+
} else {
|
|
64
|
+
const pp = pol.pricingPolicy;
|
|
65
|
+
console.log(`Habilitada. Desconto de assinante: ${pp ? `${pp.value}${pp.type === "PERCENTAGE_OFF" ? "%" : ""} (${pp.type})` : "nenhum"}`);
|
|
66
|
+
for (const f of pol.allowedFrequencies ?? []) console.log(` • ${f.title} (${f._id})`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h("FORMAS DE PAGAMENTO");
|
|
70
|
+
try {
|
|
71
|
+
// Consulta direto no core com o _id que acabamos de buscar (o shopId do .env é opcional).
|
|
72
|
+
const d = await client.gql<{ availablePaymentMethods: any[] }>(
|
|
73
|
+
`query($s:ID!){availablePaymentMethods(shopId:$s){name displayName isEnabled}}`, { s: shop._id },
|
|
74
|
+
);
|
|
75
|
+
for (const m of d.availablePaymentMethods) console.log(`• ${m.displayName ?? m.name} ${m.isEnabled ? "" : "(desabilitada)"}`);
|
|
76
|
+
} catch (e: any) {
|
|
77
|
+
console.log(`(indisponível: ${e.message})`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
h("CATÁLOGO (até 50 itens)");
|
|
81
|
+
const cat = await client.getCatalog({ first: 50 });
|
|
82
|
+
console.log(`Total na loja: ${cat.totalCount}`);
|
|
83
|
+
for (const n of cat.nodes ?? []) {
|
|
84
|
+
const p: any = (n as any).product ?? n;
|
|
85
|
+
if (!p?.title) continue;
|
|
86
|
+
const flags = [p.isVisible === false && "OCULTO", p.isSoldOut && "ESGOTADO"].filter(Boolean).join(" ");
|
|
87
|
+
const price = p.pricing?.[0]?.displayPrice ?? p.variants?.[0]?.pricing?.[0]?.displayPrice ?? "sem preço";
|
|
88
|
+
console.log(`• ${p.title} — ${price} · ${p.variants?.length ?? 0} variação(ões) ${flags}`.trim());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`\nRegra de ouro: lib/store-config.ts só recebe o que apareceu acima.`);
|
|
92
|
+
console.log(`O que não está aqui não existe no backend e não pode ser prometido na loja.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main().catch((e) => { console.error(`Falha no dump: ${e.message}`); process.exit(1); });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Carrega .env.local/.env com o MESMO parser do Next (@next/env).
|
|
2
|
+
//
|
|
3
|
+
// Por que não `tsx --env-file=.env.local`? O parser do Node e o do Next divergem no
|
|
4
|
+
// escaping de valores com `#`, `$` e aspas — a mesma senha funcionava no `next dev` e
|
|
5
|
+
// falhava no `npm run unbox:test` (ou vice-versa). Importando este módulo PRIMEIRO em
|
|
6
|
+
// todo script, o valor que o script enxerga é idêntico ao que o app enxerga.
|
|
7
|
+
//
|
|
8
|
+
// Regra prática pro .env.local: valores com caracteres especiais (#, $, &, *, espaço)
|
|
9
|
+
// sempre entre aspas duplas — ex.: UNBOX_PASS="#minhaSenha$123".
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { loadEnvConfig } from "@next/env";
|
|
12
|
+
|
|
13
|
+
loadEnvConfig(path.join(import.meta.dirname, ".."));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Roda os medidores de sistema visual (sistema.py e vocabulario.py) em MODO RELATÓRIO:
|
|
3
|
+
// imprime e sai 0, sempre. Ainda não é gate.
|
|
4
|
+
//
|
|
5
|
+
// Por que relatório e não gate: os tetos dos scripts (8 tamanhos de tipo, 5 raios, 0 hex solto)
|
|
6
|
+
// vieram de uma medição só. Medido aqui contra cinco lojas geradas e contra a própria foundation,
|
|
7
|
+
// TODAS estouram, a foundation inclusive: ligar como gate hoje reprovaria o build de todo mundo,
|
|
8
|
+
// inclusive de um scaffold recém-criado que ninguém tocou. A régua se escolhe depois de ver a
|
|
9
|
+
// distribuição, e é isso que este comando serve para acumular.
|
|
10
|
+
//
|
|
11
|
+
// Python: os medidores são .py. Onde não houver python3 (imagem de build enxuta), este wrapper
|
|
12
|
+
// AVISA e segue. Um medidor que não rodou não pode derrubar deploy de loja.
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const RAIZ = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const MEDIDORES = [
|
|
20
|
+
{ arquivo: "sistema.py", args: [], titulo: "Orçamento de sistema visual" },
|
|
21
|
+
{ arquivo: "vocabulario.py", args: [], titulo: "Cobertura do vocabulário gráfico" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function acharPython() {
|
|
25
|
+
for (const cmd of ["python3", "python"]) {
|
|
26
|
+
const r = spawnSync(cmd, ["--version"], { encoding: "utf8" });
|
|
27
|
+
if (r.status === 0) return cmd;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const python = acharPython();
|
|
33
|
+
if (!python) {
|
|
34
|
+
console.log("\nℹ️ medição de sistema visual pulada: python3 não encontrado nesta máquina.");
|
|
35
|
+
console.log(" Os medidores são opcionais e não bloqueiam o build. Para rodar localmente:");
|
|
36
|
+
console.log(" npm run unbox:sistema · npm run unbox:vocabulario\n");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const { arquivo, args, titulo } of MEDIDORES) {
|
|
41
|
+
const script = path.join(RAIZ, "scripts", arquivo);
|
|
42
|
+
if (!fs.existsSync(script)) continue;
|
|
43
|
+
console.log(`\n── ${titulo} (relatório, não bloqueia) ──`);
|
|
44
|
+
const r = spawnSync(python, [script, RAIZ, ...args], { encoding: "utf8" });
|
|
45
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
46
|
+
if (r.status === 2) {
|
|
47
|
+
// 2 é "o gate não rodou". Em modo relatório isso não derruba nada, mas precisa APARECER:
|
|
48
|
+
// medidor que varre o vazio e diz "limpo" aprova sem ter olhado.
|
|
49
|
+
process.stdout.write(r.stderr || "");
|
|
50
|
+
console.log(" (o medidor não rodou: o resultado acima não vale como aprovação)");
|
|
51
|
+
} else if (r.stderr) {
|
|
52
|
+
process.stdout.write(r.stderr);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.log("\n Números acima são linha de base, não reprovação. Régua ainda não definida.\n");
|
|
56
|
+
process.exit(0);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ⚠️ EFEITO REAL: cria um pedido PENDING via Pix na loja de produção (gera QR; NÃO cobra até o pagamento).
|
|
2
|
+
// Exige o argumento --confirm para evitar execução acidental.
|
|
3
|
+
//
|
|
4
|
+
// npm run unbox:order:pix -- --confirm
|
|
5
|
+
import "./load-env"; // SEMPRE o primeiro import — parser de env idêntico ao do app
|
|
6
|
+
import { UnboxClient } from "../lib/unbox/client";
|
|
7
|
+
|
|
8
|
+
if (!process.argv.includes("--confirm")) {
|
|
9
|
+
console.error("Recusado. Isto cria um pedido REAL (Pix PENDING). Rode com: npm run unbox:order:pix -- --confirm");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ADDR = {
|
|
14
|
+
fullName: "Cliente Teste", taxPayerId: "08383142951", postal: "04551-080",
|
|
15
|
+
address1: "Rua São Tomé", number: "73", neighborhood: "Vila Olímpia",
|
|
16
|
+
city: "São Paulo", region: "SP", phone: "11999990000",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
(async () => {
|
|
20
|
+
const client = new UnboxClient({
|
|
21
|
+
apiKey: process.env.UNBOX_API_KEY ?? "",
|
|
22
|
+
shopId: process.env.UNBOX_SHOP_ID!,
|
|
23
|
+
// API de parceiros: presente = signIn e leituras compativeis roteiam pra ela
|
|
24
|
+
partnerApiKey: process.env.UNBOX_PARTNER_API_KEY,
|
|
25
|
+
partnerGqlUrl: process.env.UNBOX_PARTNER_GRAPHQL_URL,
|
|
26
|
+
captchaBypass: process.env.UNBOX_CAPTCHA_BYPASS,
|
|
27
|
+
});
|
|
28
|
+
await client.signIn(process.env.UNBOX_USER!, process.env.UNBOX_PASS!);
|
|
29
|
+
|
|
30
|
+
const cat = await client.getCatalog({ first: 20 });
|
|
31
|
+
const picked = (cat.nodes as any[]).map((n) => n.product).find((p) => !p.isSoldOut && p.variants?.[0]?.pricing?.[0]?.price);
|
|
32
|
+
if (!picked) throw new Error("nenhum produto vendável encontrado");
|
|
33
|
+
const v = picked.variants[0];
|
|
34
|
+
console.log(`Produto: ${picked.title} @ ${v.pricing[0].displayPrice}`);
|
|
35
|
+
|
|
36
|
+
const cart = await client.createCart([{ productId: picked.productId, productVariantId: v._id, price: v.pricing[0].price, quantity: 1 }]);
|
|
37
|
+
const fgId = await client.setShippingAddress(cart.cartId, cart.cartToken, ADDR as any);
|
|
38
|
+
const opts = await client.quoteShipping(cart.cartId, cart.cartToken, fgId);
|
|
39
|
+
const finalCart = await client.selectShipping(cart.cartId, cart.cartToken, fgId, opts[0].fulfillmentMethod._id);
|
|
40
|
+
|
|
41
|
+
const order = await client.placeOrder({
|
|
42
|
+
cartId: cart.cartId,
|
|
43
|
+
email: process.env.UNBOX_TEST_EMAIL ?? "teste@example.com",
|
|
44
|
+
address: ADDR as any,
|
|
45
|
+
fulfillmentMethodId: opts[0].fulfillmentMethod._id,
|
|
46
|
+
total: finalCart.checkout.summary.total.amount,
|
|
47
|
+
items: client.buildOrderItems(finalCart),
|
|
48
|
+
payment: { type: "pix" },
|
|
49
|
+
// antifraude: pedido originado no servidor (sem navegador) → device API no ROOT.
|
|
50
|
+
// Se a Unbox aceitar (pedido criado), o root placement do device está confirmado.
|
|
51
|
+
device: { type: "API" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const o = order.orders[0];
|
|
55
|
+
console.log(`✅ Pedido criado: #${o.referenceId} (status ${o.status})`);
|
|
56
|
+
const qr = o.payments?.find((p: any) => p?.data?.qrCode)?.data?.qrCode;
|
|
57
|
+
console.log(qr ? `QR Pix (copia-e-cola):\n${qr}` : "sem QR retornado");
|
|
58
|
+
})().catch((e) => {
|
|
59
|
+
console.error("❌", e.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// CAPTURA DE TELA DO QA VISUAL — npm run unbox:qa
|
|
3
|
+
//
|
|
4
|
+
// POR QUE ESTE SCRIPT EXISTE, e por que NÃO use `chrome --screenshot --window-size=390,844`:
|
|
5
|
+
//
|
|
6
|
+
// No macOS o Chrome trava a largura mínima da janela. Pedindo 390 de largura, o PNG SAI
|
|
7
|
+
// com 390 — e é por isso que ninguém percebe — mas a página renderizou a 500. Medido nesta
|
|
8
|
+
// máquina, nos dois modos headless:
|
|
9
|
+
//
|
|
10
|
+
// --window-size=390,844 → window.innerWidth = 500
|
|
11
|
+
//
|
|
12
|
+
// Ou seja: o "mobile" do QA era o layout de 500px reduzido. Metade da revisão visual de
|
|
13
|
+
// toda loja aprovava uma tela que nenhum usuário vê.
|
|
14
|
+
//
|
|
15
|
+
// A emulação de dispositivo do CDP (Emulation.setDeviceMetricsOverride) é independente do
|
|
16
|
+
// tamanho da janela e devolve 390x844 dpr=2 de verdade, com user agent e eventos de toque
|
|
17
|
+
// de celular. Sem dependência nova: Node 22+ tem WebSocket e fetch nativos.
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
const BASE = process.env.QA_BASE ?? "http://localhost:3000";
|
|
24
|
+
const OUT = process.env.QA_OUT ?? "qa";
|
|
25
|
+
const PORTA = 9333;
|
|
26
|
+
|
|
27
|
+
// Orçamento de frames — ver agents/definitions/16-qa-visual.md. O que pega defeito é dobra,
|
|
28
|
+
// transição entre seções e extremos, não o meio de um bloco. Cobrir "toda região" custa 70+
|
|
29
|
+
// frames por rodada e a maioria mostra o que a anterior já mostrou.
|
|
30
|
+
const PLANO = [
|
|
31
|
+
{ nome: "home", url: "/", frames: 4 },
|
|
32
|
+
{ nome: "catalogo", url: "/produtos", frames: 1 },
|
|
33
|
+
{ nome: "carrinho", url: "/carrinho", frames: 1 },
|
|
34
|
+
{ nome: "checkout", url: "/checkout", frames: 1 },
|
|
35
|
+
// PDP: o script não sabe o slug; passe QA_PDP=/produto/<slug> (2 frames: dobra + conteúdo).
|
|
36
|
+
...(process.env.QA_PDP ? [{ nome: "pdp", url: process.env.QA_PDP, frames: 2 }] : []),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const PERFIS = [
|
|
40
|
+
{ nome: "1440", width: 1440, height: 900, dpr: 1, mobile: false },
|
|
41
|
+
{ nome: "390", width: 390, height: 844, dpr: 2, mobile: true },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function acharChrome() {
|
|
45
|
+
const cands = [
|
|
46
|
+
process.env.CHROME_PATH,
|
|
47
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
48
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
49
|
+
"/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser",
|
|
50
|
+
].filter(Boolean);
|
|
51
|
+
const achado = cands.find((c) => { try { return fs.existsSync(c); } catch { return false; } });
|
|
52
|
+
if (!achado) {
|
|
53
|
+
console.error("✗ Chrome não encontrado. Instale o Google Chrome ou aponte CHROME_PATH=/caminho/do/chrome");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
return achado;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function conectar(chrome) {
|
|
60
|
+
const perfil = path.join(process.env.TMPDIR ?? "/tmp", `unbox-qa-${process.pid}`);
|
|
61
|
+
const proc = spawn(chrome, [
|
|
62
|
+
"--headless=new", "--disable-gpu", "--hide-scrollbars",
|
|
63
|
+
`--remote-debugging-port=${PORTA}`, `--user-data-dir=${perfil}`,
|
|
64
|
+
"--no-first-run", "--no-default-browser-check", "about:blank",
|
|
65
|
+
], { stdio: "ignore" });
|
|
66
|
+
|
|
67
|
+
let alvos = null;
|
|
68
|
+
for (let i = 0; i < 60; i++) {
|
|
69
|
+
try { alvos = await (await fetch(`http://127.0.0.1:${PORTA}/json/list`)).json(); break; }
|
|
70
|
+
catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
71
|
+
}
|
|
72
|
+
if (!alvos) { proc.kill(); throw new Error("Chrome não abriu a porta de depuração"); }
|
|
73
|
+
|
|
74
|
+
const alvo = alvos.find((t) => t.type === "page");
|
|
75
|
+
const ws = new WebSocket(alvo.webSocketDebuggerUrl);
|
|
76
|
+
const pend = new Map();
|
|
77
|
+
let id = 0;
|
|
78
|
+
ws.onmessage = (e) => {
|
|
79
|
+
const m = JSON.parse(e.data);
|
|
80
|
+
if (m.id && pend.has(m.id)) { pend.get(m.id)(m); pend.delete(m.id); }
|
|
81
|
+
};
|
|
82
|
+
await new Promise((r, rej) => { ws.onopen = r; ws.onerror = rej; });
|
|
83
|
+
|
|
84
|
+
const cdp = (method, params = {}) => new Promise((res, rej) => {
|
|
85
|
+
const i = ++id;
|
|
86
|
+
pend.set(i, (m) => (m.error ? rej(new Error(`${method}: ${m.error.message}`)) : res(m.result)));
|
|
87
|
+
ws.send(JSON.stringify({ id: i, method, params }));
|
|
88
|
+
});
|
|
89
|
+
return { cdp, fechar: () => { ws.close(); proc.kill(); fs.rmSync(perfil, { recursive: true, force: true }); } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const esperar = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
fs.mkdirSync(OUT, { recursive: true });
|
|
96
|
+
const { cdp, fechar } = await conectar(acharChrome());
|
|
97
|
+
let n = 0;
|
|
98
|
+
const avisos = [];
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await cdp("Page.enable");
|
|
102
|
+
for (const perfil of PERFIS) {
|
|
103
|
+
await cdp("Emulation.setDeviceMetricsOverride", {
|
|
104
|
+
width: perfil.width, height: perfil.height,
|
|
105
|
+
deviceScaleFactor: perfil.dpr, mobile: perfil.mobile,
|
|
106
|
+
});
|
|
107
|
+
// Confere que a emulação PEGOU. Sem esta checagem o script poderia repetir em
|
|
108
|
+
// silêncio exatamente o bug que ele existe para corrigir.
|
|
109
|
+
for (const alvoPagina of PLANO) {
|
|
110
|
+
await cdp("Page.navigate", { url: BASE + alvoPagina.url });
|
|
111
|
+
await esperar(1400);
|
|
112
|
+
|
|
113
|
+
const { result: larg } = await cdp("Runtime.evaluate", { expression: "window.innerWidth" });
|
|
114
|
+
if (larg.value !== perfil.width) {
|
|
115
|
+
avisos.push(`${alvoPagina.nome}@${perfil.nome}: viewport renderizou ${larg.value}px, não ${perfil.width}px`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { result: alt } = await cdp("Runtime.evaluate", {
|
|
119
|
+
expression: "Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)",
|
|
120
|
+
});
|
|
121
|
+
const total = Math.max(alt.value ?? perfil.height, perfil.height);
|
|
122
|
+
const passos = alvoPagina.frames;
|
|
123
|
+
// Distribui os frames pela página: dobra, transições e o fim (rodapé).
|
|
124
|
+
const max = Math.max(0, total - perfil.height);
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < passos; i++) {
|
|
127
|
+
const y = passos === 1 ? 0 : Math.round((max * i) / (passos - 1));
|
|
128
|
+
await cdp("Runtime.evaluate", { expression: `window.scrollTo(0, ${y})` });
|
|
129
|
+
await esperar(450);
|
|
130
|
+
const { data } = await cdp("Page.captureScreenshot", { format: "png" });
|
|
131
|
+
const arq = path.join(OUT, `${alvoPagina.nome}-${perfil.nome}-${i}.png`);
|
|
132
|
+
fs.writeFileSync(arq, Buffer.from(data, "base64"));
|
|
133
|
+
n++;
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(` ✓ ${alvoPagina.nome} @ ${perfil.nome}px — ${passos} frame(s)\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
fechar();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`\n${n} frames em ${OUT}/`);
|
|
143
|
+
if (avisos.length) {
|
|
144
|
+
console.log("\n⚠ Emulação divergiu do pedido — NÃO avalie estes frames como mobile:");
|
|
145
|
+
for (const a of avisos) console.log(` • ${a}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch((e) => { console.error("✗", e.message); process.exit(1); });
|