@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,165 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// CATÁLOGO DE SEÇÕES QUE O LOJISTA PODE ADICIONAR ("+ Adicionar seção" do editor)
|
|
3
|
+
//
|
|
4
|
+
// Quem declara o que pode ser adicionado é a LOJA, nunca o editor. O editor não tem, e não pode ter,
|
|
5
|
+
// lista própria: são 100 lojas, 100 catálogos. O que existe aqui não é um catálogo de EDITABILIDADE
|
|
6
|
+
// (isso continua sendo as "4 latas" da foundation); é o catálogo de COMPONENTES desta loja, que já
|
|
7
|
+
// mora em `registry.ts`, recortado na fatia que o lojista consegue usar sozinho.
|
|
8
|
+
//
|
|
9
|
+
// ── O CORTE: 8 dos 24 do registry ─────────────────────────────────────────────
|
|
10
|
+
// Um tipo só entra se, instanciado SEM props da receita, ele:
|
|
11
|
+
// 1. RENDERIZA algo (não cai em `return null` por lista vazia), e
|
|
12
|
+
// 2. o que ele renderiza é EDITÁVEL: a copy nasce dentro de primitivo (`Editable.Text` &cia.).
|
|
13
|
+
// A regra 1 é a que mais corta nesta foundation: por honestidade, várias seções só renderizam com dado
|
|
14
|
+
// da receita (depoimentos reais, vídeo real, ficha do produto, atributos da marca, promessa de
|
|
15
|
+
// newsletter com endereço de envio). Adicionar uma delas pelo "+" entregaria ao lojista uma seção que
|
|
16
|
+
// não aparece em lugar nenhum. Pior que não oferecer.
|
|
17
|
+
//
|
|
18
|
+
// ── A EXCEÇÃO DA REGRA 1: o bloco de HTML ─────────────────────────────────────
|
|
19
|
+
// "bloco-html" é a única entrada que, recém-adicionada, renderiza NADA na loja publicada, e isso é o
|
|
20
|
+
// desenho, não o modo de falha que a regra 1 persegue. As seções cortadas somem porque falta DADO que
|
|
21
|
+
// o lojista não tem como fornecer; esta nasce vazia porque o conteúdo dela é justamente o que ele vai
|
|
22
|
+
// colar, e no editor (o único lugar onde ele acabou de clicar no "+") ela SEMPRE aparece, com o convite
|
|
23
|
+
// para colar. Sumir da loja enquanto está vazia é o certo.
|
|
24
|
+
//
|
|
25
|
+
// ── PLACEHOLDER NA SEÇÃO CRIADA ───────────────────────────────────────────────
|
|
26
|
+
// Benefícios, como funciona, cards de imagem e citação nascem com a arte de /brand/ph (o SVG que o
|
|
27
|
+
// create-unbox-store recolore com a paleta da marca) e com a copy neutra do componente. É a mesma arte
|
|
28
|
+
// e a mesma copy com que a home da foundation já nasce: o lojista clica na foto e sobe a dele. Se uma
|
|
29
|
+
// loja decidir que o "+" não pode entregar placeholder, tire a entrada daqui; nada mais muda.
|
|
30
|
+
//
|
|
31
|
+
// ── `tipo` É CHAVE PRIMÁRIA (não renomeie) ────────────────────────────────────
|
|
32
|
+
// A chave de cada entrada é o que vai para o documento (`sections.home.criadas: { id → tipo }`) e para
|
|
33
|
+
// o id da seção (`novo-<tipo>-<n>`). Renomear um `tipo` depois que alguém publicou: as seções já
|
|
34
|
+
// adicionadas apontam para um tipo que o catálogo não tem mais, `Editable.Sections` as filtra fora e
|
|
35
|
+
// elas SOMEM DA TELA em silêncio (o painel continua listando, porque ele deriva do rascunho). Tratar
|
|
36
|
+
// como id de seção da receita: nome em pt-br, escolhido uma vez, nunca mais mexido.
|
|
37
|
+
//
|
|
38
|
+
// ── `variant` sim, `props` não ────────────────────────────────────────────────
|
|
39
|
+
// A seção criada nasce com os LITERAIS do componente: nada é escrito em `values`. Por isso este
|
|
40
|
+
// arquivo não passa `sectionProps`: copy que morasse aqui viveria em dois lugares (receita e
|
|
41
|
+
// catálogo) e divergiria, e é justamente a copy que o lojista vai trocar no primeiro clique.
|
|
42
|
+
// `variant` é a exceção porque não é conteúdo, é QUAL composição o componente renderiza.
|
|
43
|
+
//
|
|
44
|
+
// ── FICARAM DE FORA (motivo curto, para quem for mexer depois) ────────────────
|
|
45
|
+
// attributes-marquee, trust-bar, trust-strip, spec-table, video-wall, reviews, reviews-carousel,
|
|
46
|
+
// comparison, founder-story, newsletter ..... `return null` sem dado da receita (regra 1). É
|
|
47
|
+
// decisão de honestidade da foundation 0.19, e vale igual no "+".
|
|
48
|
+
// category-pills, combos-carousel, savings, kits, purchase-hero ..... dependem do catálogo/da
|
|
49
|
+
// configuração comercial; somem ou esvaziam sem eles.
|
|
50
|
+
// social-row ..... renderiza com defaults, mas os defaults são gente placeholder ("@suamarca"), e a
|
|
51
|
+
// regra da seção é GENTE REAL com autorização: não entra pelo "+".
|
|
52
|
+
// stats-grid ..... renderiza e é editável, mas é a seção de NÚMERO com dado real; no "+" ela nasceria
|
|
53
|
+
// como quatro cards qualitativos e convidaria a inventar estatística.
|
|
54
|
+
//
|
|
55
|
+
// JSX não entra aqui porque o arquivo é `.ts` (o catálogo é declaração, não tela): a instância sai de
|
|
56
|
+
// `React.createElement`.
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
import * as React from "react";
|
|
59
|
+
import type { CatalogoDeSecoes, SectionKind } from "@/lib/editable";
|
|
60
|
+
import { SECTIONS, type HomeData, type SectionComponentProps, type SectionName } from "./registry";
|
|
61
|
+
|
|
62
|
+
interface EntradaDoCatalogo {
|
|
63
|
+
/** chave do catálogo: vira `novo-<tipo>-<n>` e o valor de `criadas`. NÃO RENOMEAR (ver topo). */
|
|
64
|
+
tipo: string;
|
|
65
|
+
/** componente do registry que instancia este tipo */
|
|
66
|
+
section: SectionName;
|
|
67
|
+
/** composição do componente; omitida = a default dele */
|
|
68
|
+
variant?: string;
|
|
69
|
+
/** o nome que o lojista lê no "+", nunca o nome de código */
|
|
70
|
+
label: string;
|
|
71
|
+
/** uma frase dizendo o que a seção mostra (o v1 não tem imagem de prévia) */
|
|
72
|
+
descricao: string;
|
|
73
|
+
/** tipo no vocabulário fechado da foundation (`SECTION_KINDS`) */
|
|
74
|
+
kind: SectionKind;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const CATALOGO_DA_HOME: EntradaDoCatalogo[] = [
|
|
78
|
+
{
|
|
79
|
+
tipo: "banner",
|
|
80
|
+
section: "hero",
|
|
81
|
+
// "imagem-full" é a única variante do banner que não escreve texto na tela: nasce com a arte da
|
|
82
|
+
// foundation, e o lojista sobe a imagem dele (uma para computador, outra para celular).
|
|
83
|
+
variant: "imagem-full",
|
|
84
|
+
label: "Banner",
|
|
85
|
+
descricao: "Uma imagem larga, com versão para computador e para celular, que leva aos destaques da loja.",
|
|
86
|
+
kind: "banner",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
// A VITRINE. Cada uma adicionada ganha id próprio (`novo-vitrine-de-produtos-1`, `-2`, …), e o
|
|
90
|
+
// caminho da escolha sai desse id (`home.<id>.vitrine`): DUAS vitrines na mesma página têm DUAS
|
|
91
|
+
// escolhas independentes, sem uma pisar na outra.
|
|
92
|
+
tipo: "vitrine-de-produtos",
|
|
93
|
+
section: "product-showcase",
|
|
94
|
+
label: "Vitrine de produtos",
|
|
95
|
+
descricao: "Uma grade de produtos que você escolhe (uma categoria inteira, uma lista sua ou uma busca), com foto, preço e botão de comprar em cada card.",
|
|
96
|
+
kind: "vitrine-de-produtos",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
tipo: "beneficios",
|
|
100
|
+
section: "benefits",
|
|
101
|
+
label: "Benefícios",
|
|
102
|
+
descricao: "Título, duas colunas de benefícios com texto curto e uma foto no meio.",
|
|
103
|
+
kind: "beneficios",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
tipo: "como-funciona",
|
|
107
|
+
section: "ritual",
|
|
108
|
+
label: "Como funciona",
|
|
109
|
+
descricao: "Fotos que se alternam de um lado e, do outro, um selo, um título, uma lista de passos e um botão.",
|
|
110
|
+
kind: "como-funciona",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
tipo: "cards-de-imagem",
|
|
114
|
+
section: "media-cards",
|
|
115
|
+
label: "Cards de imagem",
|
|
116
|
+
descricao: "Título e uma fileira de cards com foto e legenda, que rolam para o lado.",
|
|
117
|
+
kind: "galeria",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
tipo: "citacao",
|
|
121
|
+
section: "quote-banner",
|
|
122
|
+
label: "Citação",
|
|
123
|
+
descricao: "Faixa de cor da marca com uma foto e uma frase grande assinada por alguém da loja.",
|
|
124
|
+
kind: "sobre-a-marca",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
// O BLOCO DE HTML: a única entrada que não desenha nada por conta própria (ver "A EXCEÇÃO DA
|
|
128
|
+
// REGRA 1" no topo). `kind: "outro"` é a escolha honesta: o vocabulário fechado da foundation não
|
|
129
|
+
// tem um tipo para "o que o lojista colar", e carimbar "beneficios" ou "sobre-a-marca" faria o
|
|
130
|
+
// painel afirmar um conteúdo que ninguém conferiu.
|
|
131
|
+
tipo: "bloco-html",
|
|
132
|
+
section: "bloco-html",
|
|
133
|
+
label: "Bloco de HTML",
|
|
134
|
+
// A frase diz o que NÃO entra porque a recusa acontece depois de colar: sem esse aviso aqui, o
|
|
135
|
+
// lojista descobre a regra levando um "não" com o texto já no campo.
|
|
136
|
+
descricao: "Para colar um código pronto que te passaram: um selo, uma tabela de medidas, um trecho de conteúdo. Código que roda (script) e formulário não entram.",
|
|
137
|
+
kind: "outro",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* O catálogo pronto para `Editable.Sections catalogo={…}`.
|
|
143
|
+
*
|
|
144
|
+
* Recebe `data` porque é o que a home já buscou no servidor: `render` fecha por closure e as seções
|
|
145
|
+
* criadas enxergam o mesmo catálogo de produtos das que vêm do código. Pode ser chamado a cada render:
|
|
146
|
+
* `Editable.Sections` compara o catálogo por uma ASSINATURA de texto, não por referência, então um
|
|
147
|
+
* objeto novo por render não re-registra os tipos nem impede o manifesto de assentar.
|
|
148
|
+
*/
|
|
149
|
+
export function catalogoDaHome(data: HomeData): CatalogoDeSecoes {
|
|
150
|
+
return Object.fromEntries(
|
|
151
|
+
CATALOGO_DA_HOME.map((e) => {
|
|
152
|
+
// o componente do registry é um union de todas as seções; todas recebem `SectionComponentProps`
|
|
153
|
+
const Componente = SECTIONS[e.section] as React.ComponentType<SectionComponentProps>;
|
|
154
|
+
return [
|
|
155
|
+
e.tipo,
|
|
156
|
+
{
|
|
157
|
+
label: e.label,
|
|
158
|
+
descricao: e.descricao,
|
|
159
|
+
kind: e.kind,
|
|
160
|
+
render: () => React.createElement(Componente, { data, variant: e.variant }),
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// PILLS de categorias — chips com thumb, quebram em várias linhas pra exibir todas.
|
|
4
|
+
//
|
|
5
|
+
// EDITOR: nada aqui é copy da marca. Nome, ordem e imagem das categorias vêm do catálogo da Unbox
|
|
6
|
+
// (o lojista muda no painel da plataforma), então a seção não abre ponto editável e sai do gate por
|
|
7
|
+
// `data-editor-ignore` (README do editor, §8: produto do catálogo não vira primitivo). O que o
|
|
8
|
+
// lojista edita nela é o lugar na página: ordem e ocultar, pela seção.
|
|
9
|
+
import Link from "next/link";
|
|
10
|
+
import Image from "next/image";
|
|
11
|
+
import { collImageSrc } from "@/components/catalog/catalog-client";
|
|
12
|
+
import type { SectionComponentProps } from "./registry";
|
|
13
|
+
|
|
14
|
+
export function CategoryPillsSection({ data }: SectionComponentProps) {
|
|
15
|
+
if (data.categories.length === 0) return null;
|
|
16
|
+
return (
|
|
17
|
+
<div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[18px] sm:px-6">
|
|
18
|
+
<div className="flex flex-wrap gap-2.5" data-editor-ignore="">
|
|
19
|
+
{data.categories.map((c) => (
|
|
20
|
+
<Link
|
|
21
|
+
key={c.slug}
|
|
22
|
+
href={`/categoria/${encodeURIComponent(c.slug)}`}
|
|
23
|
+
className="flex flex-none items-center gap-[11px] rounded-full border-[1.5px] border-[var(--store-line)] bg-white py-[7px] pl-2 pr-5 no-underline transition-all hover:border-[var(--store-primary,#18181B)] hover:shadow-[var(--store-shadow-soft)]"
|
|
24
|
+
>
|
|
25
|
+
<Image src={collImageSrc(c.name)} alt="" width={44} height={44} className="h-11 w-11 flex-none object-contain" />
|
|
26
|
+
<span className="font-display whitespace-nowrap text-[14px] font-semibold not-italic text-[var(--store-ink)]">{c.name}</span>
|
|
27
|
+
</Link>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Carrossel de produtos em destaque (combos/ofertas) com add-to-cart funcional.
|
|
4
|
+
// Âncora #destaques — alvo do CTA do hero.
|
|
5
|
+
//
|
|
6
|
+
// ── EDITOR: esta é a VITRINE DA HOME ────────────────────────────────────────
|
|
7
|
+
// `Editable.Vitrine` deixa o LOJISTA escolher o que aparece aqui (uma categoria da Unbox, uma lista
|
|
8
|
+
// de produtos ou uma busca). O documento guarda só a ESCOLHA; quem a transforma em produto com preço
|
|
9
|
+
// e estoque é a LOJA, no servidor (lib/vitrine.ts), e os produtos chegam prontos em `data.vitrines`.
|
|
10
|
+
//
|
|
11
|
+
// O CAMINHO NÃO É CONSTANTE. `path={CAMPO_VITRINE}` é RELATIVO à seção: o primitivo o resolve para
|
|
12
|
+
// `<container>.<id da seção>.vitrine` pelo escopo, e aqui `joinPath(ctx.scope, CAMPO_VITRINE)` deriva
|
|
13
|
+
// EXATAMENTE a mesma string para achar os produtos já resolvidos. Escrever o caminho completo à mão
|
|
14
|
+
// descola os dois lados no primeiro id renomeado, em silêncio.
|
|
15
|
+
//
|
|
16
|
+
// FALLBACK = o que o código mostra hoje (`data.combos`, a regra da página: kits, senão ofertas, senão
|
|
17
|
+
// destaques). Enquanto o lojista não escolher, e também quando a escolha não resolve em nada (Unbox
|
|
18
|
+
// fora, categoria esvaziada), esta seção renderiza o mesmo HTML de antes. Vitrine vazia lê como loja
|
|
19
|
+
// quebrada.
|
|
20
|
+
//
|
|
21
|
+
// O produto resolvido pela escolha traz id, título, foto, preço e estoque, mas não a VARIANTE. O card
|
|
22
|
+
// do catálogo já carregado (com variante, preço antigo e selo) é reaproveitado quando o produto está
|
|
23
|
+
// nele; fora dele, o card nasce mínimo: liga à página do produto e não tem o "+" do carrinho (regra do
|
|
24
|
+
// ProductGridCard: sem variantId, sem botão). Preço e nome continuam sendo dado do catálogo (§8).
|
|
25
|
+
//
|
|
26
|
+
// Na PRÉVIA os produtos vêm pelo 4º argumento do render-prop: o rascunho não passa pelo servidor da
|
|
27
|
+
// loja, então o primitivo pergunta à própria loja (`POST /api/unbox/vitrine`) e entrega em `previa`.
|
|
28
|
+
// `previa.produtos` GANHA de `data.vitrines`. Em produção `previa` é `undefined` e nada aqui muda.
|
|
29
|
+
import * as React from "react";
|
|
30
|
+
import Link from "next/link";
|
|
31
|
+
import { CaretLeft, CaretRight, ArrowRight } from "@phosphor-icons/react/dist/ssr";
|
|
32
|
+
import { useCart } from "@/components/cart/cart-provider";
|
|
33
|
+
import type { CatalogProductItem } from "@/components/catalog/catalog-client";
|
|
34
|
+
import { ProductGridCard } from "@/components/catalog/product-grid-card";
|
|
35
|
+
import { CAMPO_VITRINE, Editable, joinPath, useEditableContext, type VitrineProdutoResolvido, type VitrineValue } from "@/lib/editable";
|
|
36
|
+
import { formatBRL } from "@/lib/format";
|
|
37
|
+
import type { SectionComponentProps } from "./registry";
|
|
38
|
+
|
|
39
|
+
/** Quantos produtos a vitrine mostra quando a escolha do lojista não diz (o mesmo teto da página). */
|
|
40
|
+
const TETO_PADRAO = 10;
|
|
41
|
+
|
|
42
|
+
/** Produto resolvido pela escolha do lojista → item de card (ver o cabeçalho). */
|
|
43
|
+
function itemDaVitrine(p: VitrineProdutoResolvido, catalogo: CatalogProductItem[]): CatalogProductItem {
|
|
44
|
+
const doCatalogo = catalogo.find((c) => c.productId === p.id || c.slug === p.slug);
|
|
45
|
+
if (doCatalogo) return doCatalogo;
|
|
46
|
+
return {
|
|
47
|
+
slug: p.slug,
|
|
48
|
+
title: p.titulo,
|
|
49
|
+
weight: "",
|
|
50
|
+
categories: [],
|
|
51
|
+
price: p.preco ?? 0,
|
|
52
|
+
// `preco: null` = produto sem variante com preço no painel; `formatBRL` devolve "—" (ausência honesta)
|
|
53
|
+
displayPrice: formatBRL(p.preco),
|
|
54
|
+
oldPrice: null,
|
|
55
|
+
displayOld: null,
|
|
56
|
+
offPct: null,
|
|
57
|
+
imageUrl: p.imagem,
|
|
58
|
+
productId: p.id,
|
|
59
|
+
variantId: null,
|
|
60
|
+
badge: null,
|
|
61
|
+
soldOut: p.esgotado,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function CombosCarouselSection({ data }: SectionComponentProps) {
|
|
66
|
+
const { add } = useCart();
|
|
67
|
+
const [busyId, setBusyId] = React.useState<string | null>(null);
|
|
68
|
+
const rowRef = React.useRef<HTMLDivElement | null>(null);
|
|
69
|
+
|
|
70
|
+
// O MESMO caminho que o primitivo registra logo abaixo, derivado do escopo da seção.
|
|
71
|
+
const ctx = useEditableContext();
|
|
72
|
+
const doServidor = data.vitrines?.[joinPath(ctx.scope, CAMPO_VITRINE)] ?? null;
|
|
73
|
+
// A escolha que o CÓDIGO representa: os produtos desta seção hoje, na ordem em que aparecem.
|
|
74
|
+
// Vínculo pelo productId, não pelo slug: ver product-showcase.tsx. Slug muda no painel e o
|
|
75
|
+
// produto some da seção em silêncio; o id é estável.
|
|
76
|
+
const fallbackVitrine: VitrineValue = { modo: "produtos", produtos: data.combos.map((p) => p.productId || p.slug) };
|
|
77
|
+
|
|
78
|
+
// Sem catálogo nenhum a seção não tem o que mostrar. No modo edição ela CONTINUA na tela: sumindo
|
|
79
|
+
// dali, o lojista não conseguiria selecionar a vitrine para trocar a escolha.
|
|
80
|
+
if (data.combos.length === 0 && !doServidor?.length && !ctx.editing) return null;
|
|
81
|
+
|
|
82
|
+
async function quickAdd(p: CatalogProductItem) {
|
|
83
|
+
if (!p.productId || !p.variantId || p.soldOut) return;
|
|
84
|
+
setBusyId(p.variantId);
|
|
85
|
+
await add({ productId: p.productId, variantId: p.variantId, price: p.price, quantity: 1, thumbnail: p.imageUrl ?? undefined, title: p.title });
|
|
86
|
+
setBusyId(null);
|
|
87
|
+
}
|
|
88
|
+
const scrollRow = (dir: number) => rowRef.current?.scrollBy({ left: dir * 300, behavior: "smooth" });
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div id="destaques" className="mx-auto max-w-[var(--container-max,1240px)] scroll-mt-24 px-4 pt-[42px] sm:px-6">
|
|
92
|
+
<div className="mb-[18px] flex items-end justify-between gap-3">
|
|
93
|
+
<div>
|
|
94
|
+
{/* o título de partida é o que a página derivou do catálogo ("Destaques", "Ofertas em destaque") */}
|
|
95
|
+
<Editable.Text as="h2" path="titulo" fallback={data.combosTitle} label="Título" className="font-display text-[26px] font-extrabold" />
|
|
96
|
+
<Editable.Text as="p" path="subtitulo" fallback="Produtos em destaque para você." label="Subtítulo" className="mt-1 text-sm text-[var(--store-muted)]" />
|
|
97
|
+
</div>
|
|
98
|
+
<div className="hidden gap-2.5 sm:flex">
|
|
99
|
+
<button type="button" onClick={() => scrollRow(-1)} aria-label="Anterior" className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-[1.5px] border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)]"><CaretLeft weight="bold" className="text-[var(--store-ink-2)]" /></button>
|
|
100
|
+
<button type="button" onClick={() => scrollRow(1)} aria-label="Próximo" className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-[1.5px] border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)]"><CaretRight weight="bold" className="text-[var(--store-ink-2)]" /></button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
{/* O primitivo só REGISTRA o caminho e devolve os atributos de seleção; em produção ele não
|
|
104
|
+
põe nada no DOM: sem escolha do lojista, o HTML aqui é o mesmo de antes. */}
|
|
105
|
+
<Editable.Vitrine path={CAMPO_VITRINE} label="Produtos desta vitrine" fallback={fallbackVitrine}>
|
|
106
|
+
{(escolha, attrs, ref, previa) => {
|
|
107
|
+
// No modo PRODUTOS a lista É a resposta: mostra tantos quantos o lojista escolheu. Nos
|
|
108
|
+
// outros modos o limite decide o tamanho, com o teto da página como padrão.
|
|
109
|
+
const teto = escolha.modo === "produtos" ? escolha.produtos.length : (escolha.limite ?? TETO_PADRAO);
|
|
110
|
+
// a prévia (rascunho de agora) ganha do que o servidor resolveu (documento publicado)
|
|
111
|
+
const resolvidos = previa?.produtos ?? doServidor;
|
|
112
|
+
// LISTA VAZIA cai nos produtos do CÓDIGO, não numa fileira em branco: é o que a página
|
|
113
|
+
// publicada faz (`resolverVitrinesDoDocumento` deixa de fora o caminho que não resolveu em
|
|
114
|
+
// nada). Quem avisa o lojista é a tarja âmbar do primitivo, que só existe na prévia.
|
|
115
|
+
const itens = resolvidos && resolvidos.length ? resolvidos.slice(0, teto).map((p) => itemDaVitrine(p, data.combos)) : data.combos;
|
|
116
|
+
return (
|
|
117
|
+
// dois refs num elemento só: o do carrossel (setas) e o do editor (seleção)
|
|
118
|
+
<div ref={(el) => { rowRef.current = el; ref(el); }} {...attrs} className="flex gap-[18px] overflow-x-auto pb-3.5 [scrollbar-width:thin]">
|
|
119
|
+
{itens.map((c) => (
|
|
120
|
+
<div key={c.slug} className="w-[260px] flex-none">
|
|
121
|
+
<ProductGridCard p={c} busy={busyId === c.variantId} onAdd={() => quickAdd(c)} />
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
{/* card final: ver todos os produtos (o destino é a mecânica do catálogo; as frases são copy) */}
|
|
125
|
+
<Link
|
|
126
|
+
href="/produtos"
|
|
127
|
+
className="group flex w-[260px] flex-none flex-col items-center justify-center gap-3.5 rounded-2xl border-[1.5px] border-dashed border-[var(--store-primary-soft)] bg-white/60 p-6 text-center no-underline transition-colors hover:border-[var(--store-primary,#18181B)] hover:bg-[var(--store-primary-soft,#F1F1F3)]"
|
|
128
|
+
>
|
|
129
|
+
<span className="flex h-14 w-14 items-center justify-center rounded-full bg-[var(--store-primary-soft,#F1F1F3)] text-[var(--store-primary,#18181B)] transition-colors group-hover:bg-[var(--store-primary,#18181B)] group-hover:text-white">
|
|
130
|
+
<ArrowRight weight="bold" className="text-2xl" />
|
|
131
|
+
</span>
|
|
132
|
+
<Editable.Text path="ver-todos.titulo" fallback="Ver todos os produtos" label="Título do card de ver todos" className="font-display text-[17px] font-extrabold leading-[1.2] text-[var(--store-primary,#18181B)]" />
|
|
133
|
+
<Editable.Text path="ver-todos.texto" fallback="Explore o catálogo completo" label="Texto do card de ver todos" className="text-[13px] text-[var(--store-muted)]" />
|
|
134
|
+
</Link>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}}
|
|
138
|
+
</Editable.Vitrine>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Comparativo "nós vs outros" — tabela ESTRUTURAL (a loja de referência usava imagem raster; aqui é
|
|
4
|
+
// componente de verdade: acessível, responsivo e recolorível). Conteúdo via receita.
|
|
5
|
+
// ⚠️ Compare ATRIBUTOS do seu produto, sem citar marcas concorrentes nem inventar dado.
|
|
6
|
+
//
|
|
7
|
+
// EDITOR: título e os dois cabeçalhos de coluna são caminhos da seção; as linhas são uma LISTA
|
|
8
|
+
// editável (`linhas.linha-N`). Em cada linha o atributo é texto; cada célula é OU um ícone
|
|
9
|
+
// (`nos-icone` / `outros-icone`, trocável por imagem) OU um texto (`nos` / `outros`), conforme a
|
|
10
|
+
// receita traz booleano ou frase. São caminhos diferentes de propósito: trocar o TIPO de um caminho
|
|
11
|
+
// descarta a edição do lojista. O divisor entre linhas é regra do container (`[&>*+*>div]`),
|
|
12
|
+
// porque o invólucro do item é display:contents e é ele o irmão no DOM.
|
|
13
|
+
import { CheckCircle, XCircle } from "@phosphor-icons/react/dist/ssr";
|
|
14
|
+
import { Editable, EditableScope } from "@/lib/editable";
|
|
15
|
+
import type { SectionComponentProps } from "./registry";
|
|
16
|
+
|
|
17
|
+
type CompRow = { label: string; us: boolean | string; others: boolean | string };
|
|
18
|
+
|
|
19
|
+
function Cell({ v, coluna }: { v: boolean | string; coluna: "nos" | "outros" }) {
|
|
20
|
+
const onde = coluna === "nos" ? "coluna da loja" : "coluna dos outros";
|
|
21
|
+
if (v === true) {
|
|
22
|
+
return (
|
|
23
|
+
<Editable.Icon path={`${coluna}-icone`} label={`Ícone de sim (${onde})`} size={22} className="mx-auto text-[22px] text-[var(--store-primary,#18181B)]">
|
|
24
|
+
<CheckCircle weight="fill" />
|
|
25
|
+
</Editable.Icon>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (v === false) {
|
|
29
|
+
return (
|
|
30
|
+
<Editable.Icon path={`${coluna}-icone`} label={`Ícone de não (${onde})`} size={22} className="mx-auto text-[22px] text-[var(--store-faint)]">
|
|
31
|
+
<XCircle weight="fill" />
|
|
32
|
+
</Editable.Icon>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return <Editable.Text path={coluna} fallback={v} label={`Texto da célula (${onde})`} className="text-[13px] text-[var(--store-muted)]" />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ComparisonSection({ sectionProps = {} }: SectionComponentProps) {
|
|
39
|
+
const {
|
|
40
|
+
title = "Compare e escolha bem",
|
|
41
|
+
usLabel = "Aqui",
|
|
42
|
+
othersLabel = "Por aí",
|
|
43
|
+
} = sectionProps as Record<string, string>;
|
|
44
|
+
const rows = ((sectionProps.rows as CompRow[] | undefined) ?? []).filter((r) => r?.label);
|
|
45
|
+
if (rows.length === 0) return null; // comparativo é afirmação sobre o produto: só com dado da marca
|
|
46
|
+
return (
|
|
47
|
+
<div className="mx-auto max-w-[880px] px-4 pt-[var(--section-gap,52px)] sm:px-6">
|
|
48
|
+
<Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-center text-[28px] font-extrabold leading-[1.15]" />
|
|
49
|
+
<div className="mt-7 overflow-hidden rounded-2xl border border-[var(--store-line-2)] bg-white">
|
|
50
|
+
<div className="grid grid-cols-[1.6fr_1fr_1fr] border-b border-[var(--store-line)] bg-[var(--store-surface)] text-center">
|
|
51
|
+
<div />
|
|
52
|
+
<Editable.Text as="div" path="nos" fallback={usLabel} label="Cabeçalho da coluna da loja" className="font-display py-3 text-[14px] font-extrabold text-[var(--store-primary,#18181B)]" />
|
|
53
|
+
<Editable.Text as="div" path="outros" fallback={othersLabel} label="Cabeçalho da coluna dos outros" className="font-display py-3 text-[14px] font-bold text-[var(--store-muted)]" />
|
|
54
|
+
</div>
|
|
55
|
+
<div className="[&>*+*>div]:border-t [&>*+*>div]:border-[var(--store-line)]">
|
|
56
|
+
<EditableScope path="linhas">
|
|
57
|
+
<Editable.Sections nested>
|
|
58
|
+
{rows.map((r, i) => (
|
|
59
|
+
<Editable.Section key={i} item id={`linha-${i + 1}`} label={`Linha ${i + 1}`}>
|
|
60
|
+
<div className="grid grid-cols-[1.6fr_1fr_1fr] items-center text-center">
|
|
61
|
+
<Editable.Text as="div" path="rotulo" fallback={r.label} label="Atributo comparado" className="px-4 py-3.5 text-left text-[14px] font-semibold text-[var(--store-ink-2)]" />
|
|
62
|
+
<div className="py-3.5"><Cell v={r.us} coluna="nos" /></div>
|
|
63
|
+
<div className="py-3.5"><Cell v={r.others} coluna="outros" /></div>
|
|
64
|
+
</div>
|
|
65
|
+
</Editable.Section>
|
|
66
|
+
))}
|
|
67
|
+
</Editable.Sections>
|
|
68
|
+
</EditableScope>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// História da marca: card com foto de fundo e bloco de texto sobreposto + assinatura.
|
|
4
|
+
// Portado de loja real em produção e genericizado. Âncora #nossa-historia (pode ser alvo do menu).
|
|
5
|
+
//
|
|
6
|
+
// EDITOR: foto, título, texto e assinatura são caminhos da seção. A assinatura só registra caminho
|
|
7
|
+
// quando a receita a traz: não abrimos ponto de "assinatura" numa história que não é assinada.
|
|
8
|
+
// A foto continua passando pelo `Foto` (otimizador do Next): o `Editable.Slot` só lhe entrega o
|
|
9
|
+
// src/alt do lojista e os atributos de seleção.
|
|
10
|
+
import { Editable } from "@/lib/editable";
|
|
11
|
+
import type { SectionComponentProps } from "./registry";
|
|
12
|
+
import { Foto } from "@/components/ui/foto";
|
|
13
|
+
|
|
14
|
+
export function FounderStorySection({ sectionProps = {} }: SectionComponentProps) {
|
|
15
|
+
const {
|
|
16
|
+
title = "Nossa história",
|
|
17
|
+
text = "",
|
|
18
|
+
signature = "",
|
|
19
|
+
image = "/brand/ph/photo-c.svg",
|
|
20
|
+
} = sectionProps as Record<string, string>;
|
|
21
|
+
// História é da marca: sem texto na receita, a seção não renderiza (nada de história genérica).
|
|
22
|
+
if (!text) return null;
|
|
23
|
+
return (
|
|
24
|
+
<div id="nossa-historia" className="reveal mx-auto max-w-[var(--container-max,1240px)] scroll-mt-24 px-4 pt-[var(--section-gap,52px)] sm:px-6">
|
|
25
|
+
<div className="relative overflow-hidden rounded-2xl">
|
|
26
|
+
{/* TODO: foto real de bastidor/fundador (desktop ~21:9) */}
|
|
27
|
+
<Editable.Slot path="imagem" type="image" fallback={{ src: image, alt: "" }} label="Foto de fundo">
|
|
28
|
+
{(v, attrs, ref) => <Foto ref={ref} attrs={attrs} src={v.src} alt={v.alt ?? ""} width={2480} height={840} sizes="(min-width: 1280px) 1240px, 100vw" className="h-[420px] w-full object-cover sm:h-[460px]" />}
|
|
29
|
+
</Editable.Slot>
|
|
30
|
+
<div className="absolute inset-0 flex items-end p-5 sm:items-center sm:p-10">
|
|
31
|
+
<div className="max-w-[480px] rounded-2xl bg-white/95 p-6 sm:p-8">
|
|
32
|
+
<Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[24px] font-extrabold" />
|
|
33
|
+
<Editable.Text as="p" path="texto" fallback={text} label="Texto da história" multiline className="mt-3 text-[14.5px] leading-[1.6] text-[var(--store-ink-2)]" />
|
|
34
|
+
{signature && <Editable.Text as="div" path="assinatura" fallback={signature} label="Assinatura" className="font-display mt-4 text-[18px] font-bold italic text-[var(--store-primary,#18181B)]" />}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|