@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,821 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// PRIMITIVOS EDITÁVEIS — a regra das "4 latas".
|
|
5
|
+
//
|
|
6
|
+
// Editabilidade não é um catálogo de seções: é uma propriedade de COMO a seção
|
|
7
|
+
// é escrita. Onde o construtor escreveria `<h2>{TITULO}</h2>`, escreve
|
|
8
|
+
// `<Editable.Text path="titulo" fallback={TITULO} as="h2" />`. Se a home
|
|
9
|
+
// decidiu não ter banner e ter quatro latas, cada lata é um `Editable.Image`
|
|
10
|
+
// e o lojista clica na lata e troca a foto. Nenhuma lista central precisa saber
|
|
11
|
+
// que a lata existe: ela se registra sozinha e aparece no manifesto.
|
|
12
|
+
//
|
|
13
|
+
// Em produção os primitivos renderizam o elemento puro (mesmo HTML de antes).
|
|
14
|
+
// Só em modo edição ganham `data-editor-*`.
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
import * as React from "react";
|
|
17
|
+
import Image, { type ImageProps } from "next/image";
|
|
18
|
+
import Link from "next/link";
|
|
19
|
+
import { caminhoTemContainer, type EditableType, type EditableValue, type ImageValue, type LinkValue, type PreviaDaVitrine, produtosDaVitrine, recusaDeHtml, type SectionKind, SUFIXO_HTML, joinPath, resolveStyle, resolveValue, vitrineValida, type VitrineValue } from "./document";
|
|
20
|
+
import { EditableContextProvider, useEditableContext } from "./provider";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* RECADO PARA QUEM CONSTRUIU A LOJA, só em desenvolvimento e uma vez por defeito. Não é recado para o
|
|
24
|
+
* lojista: ele não tem como consertar um container que ninguém declarou, e a tela dele não conta a
|
|
25
|
+
* história interna do sistema. Quem vê isto é o construtor (console do `next dev`) e o gate.
|
|
26
|
+
*/
|
|
27
|
+
const jaAvisado = new Set<string>();
|
|
28
|
+
function avisarConstrutor(chave: string, mensagem: string) {
|
|
29
|
+
if (process.env.NODE_ENV === "production") return;
|
|
30
|
+
if (jaAvisado.has(chave)) return;
|
|
31
|
+
jaAvisado.add(chave);
|
|
32
|
+
console.error(mensagem);
|
|
33
|
+
}
|
|
34
|
+
function avisoForaDeContainer(full: string, label?: string) {
|
|
35
|
+
return `[Editable] o editável "${full}"${label ? ` (${label})` : ""} está fora de qualquer container: ele gravaria na raiz do documento, onde o mesmo nome valeria para o site inteiro e bateria com o de outra página. A loja deixou o valor do código na tela e NÃO o tornou editável. Envolva esta seção em <Editable.Section id="..."> dentro de um <Editable.Sections container="<a-página>">.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useRegistration<T extends EditableValue>(path: string, type: EditableType, fallback: T, label?: string) {
|
|
39
|
+
const ctx = useEditableContext();
|
|
40
|
+
const full = joinPath(ctx.scope, path);
|
|
41
|
+
const ref = React.useRef<Element | null>(null);
|
|
42
|
+
// FORA DE CONTAINER = FORA DO DOCUMENTO (foundation 11). Este caminho cairia na RAIZ, onde valeria
|
|
43
|
+
// para o site inteiro e colidiria com o de outra página (duas seções soltas gravando `titulo`). A
|
|
44
|
+
// loja não o registra e IGNORA o documento nele: renderiza o literal do código, íntegra. Quem
|
|
45
|
+
// reclama é o console em desenvolvimento e o gate, pelo manifesto. Ver `caminhoTemContainer`.
|
|
46
|
+
const semContainer = !caminhoTemContainer(ctx.container, ctx.scope, path);
|
|
47
|
+
const value = semContainer ? fallback : resolveValue(ctx.doc, full, fallback);
|
|
48
|
+
const { editing, register, foraDeContainer } = ctx;
|
|
49
|
+
// no servidor não roda efeito: o aviso sai no terminal do `next dev` durante o render, uma vez por
|
|
50
|
+
// caminho, para o construtor ver sem abrir o console do navegador
|
|
51
|
+
if (semContainer && typeof window === "undefined") avisarConstrutor(`path:${full}`, avisoForaDeContainer(full, label));
|
|
52
|
+
// fallback por valor (não por referência) pra não re-registrar a cada render
|
|
53
|
+
const fallbackKey = JSON.stringify(fallback);
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (!editing || semContainer) return;
|
|
56
|
+
return register({ path: full, type, label, fallback: JSON.parse(fallbackKey) as T, el: () => ref.current });
|
|
57
|
+
}, [editing, register, full, type, label, fallbackKey, semContainer]);
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
if (!semContainer) return;
|
|
60
|
+
avisarConstrutor(`path:${full}`, avisoForaDeContainer(full, label));
|
|
61
|
+
if (!editing) return;
|
|
62
|
+
return foraDeContainer({ path: full, type, label });
|
|
63
|
+
}, [semContainer, editing, foraDeContainer, full, type, label]);
|
|
64
|
+
const attrs =
|
|
65
|
+
editing && !semContainer
|
|
66
|
+
? ({ "data-editor-path": full, "data-editor-type": type, "data-editor-label": label } as Record<string, string | undefined>)
|
|
67
|
+
: {};
|
|
68
|
+
// cor/fundo SÓ deste elemento, por cima do token ("desacoplar do mapa de cores")
|
|
69
|
+
const style = semContainer ? undefined : resolveStyle(ctx.doc, full);
|
|
70
|
+
// valor CRU do documento, sem o fallback e sem a defesa do `resolveValue`. Só o bloco de HTML usa:
|
|
71
|
+
// é com ele que o primitivo consegue DIZER "o que você colou foi recusado" em vez de mostrar o
|
|
72
|
+
// conteúdo do código calado — trocar em silêncio é o que a casa não faz.
|
|
73
|
+
const guardado = semContainer ? undefined : ctx.doc?.values[full];
|
|
74
|
+
return { value, ref, attrs, editing, full, style, guardado };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Valor editável que não vira elemento (cor de fundo, href de um botão, flag). */
|
|
78
|
+
export function useEditable<T extends EditableValue>(path: string, fallback: T, opts?: { type?: EditableType; label?: string }): T {
|
|
79
|
+
const type = opts?.type ?? (typeof fallback === "string" ? "text" : "src" in (fallback as object) ? "image" : "link");
|
|
80
|
+
return useRegistration(path, type, fallback, opts?.label).value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type TextProps = {
|
|
84
|
+
path: string;
|
|
85
|
+
fallback: string;
|
|
86
|
+
label?: string;
|
|
87
|
+
as?: React.ElementType;
|
|
88
|
+
/** quebra de linha vira <br/>; sem isso, "\n" é espaço */
|
|
89
|
+
multiline?: boolean;
|
|
90
|
+
} & Omit<React.HTMLAttributes<HTMLElement>, "children">;
|
|
91
|
+
|
|
92
|
+
function Text({ path, fallback, label, as = "span", multiline = false, style: styleProp, ...rest }: TextProps) {
|
|
93
|
+
const { value, ref, attrs, style } = useRegistration(path, "text", fallback, label);
|
|
94
|
+
const Tag = as as React.ElementType;
|
|
95
|
+
const estilo = style || styleProp ? { ...(styleProp ?? {}), ...(style ?? {}) } : undefined;
|
|
96
|
+
const content = multiline
|
|
97
|
+
? value.split("\n").map((line, i, arr) => (
|
|
98
|
+
<React.Fragment key={i}>
|
|
99
|
+
{line}
|
|
100
|
+
{i < arr.length - 1 ? <br /> : null}
|
|
101
|
+
</React.Fragment>
|
|
102
|
+
))
|
|
103
|
+
: value;
|
|
104
|
+
return (
|
|
105
|
+
<Tag ref={ref} {...attrs} {...rest} style={estilo}>
|
|
106
|
+
{content}
|
|
107
|
+
</Tag>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type EditableImageProps = {
|
|
112
|
+
path: string;
|
|
113
|
+
fallback: ImageValue;
|
|
114
|
+
label?: string;
|
|
115
|
+
} & Omit<ImageProps, "src" | "alt">;
|
|
116
|
+
|
|
117
|
+
/** next/image com src/alt editáveis. Todas as outras props (fill, sizes, priority…) passam direto. */
|
|
118
|
+
function EditableImage({ path, fallback, label, ...rest }: EditableImageProps) {
|
|
119
|
+
const { value, ref, attrs } = useRegistration(path, "image", fallback, label);
|
|
120
|
+
const v = value as ImageValue;
|
|
121
|
+
return <Image ref={ref as React.Ref<HTMLImageElement>} src={v.src} alt={v.alt ?? fallback.alt ?? ""} {...attrs} {...rest} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type EditableImgProps = {
|
|
125
|
+
path: string;
|
|
126
|
+
fallback: ImageValue;
|
|
127
|
+
label?: string;
|
|
128
|
+
} & Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `<img>` PURO com src/alt editáveis — pra onde o next/image mudaria o HTML
|
|
132
|
+
* (assets da marca servidos direto de /public, `<picture>` com duas artes,
|
|
133
|
+
* logo sem width/height). Rende exatamente o `<img>` de antes; o resto das
|
|
134
|
+
* props (className, width, height, loading…) passa direto.
|
|
135
|
+
*/
|
|
136
|
+
function EditableImg({ path, fallback, label, ...rest }: EditableImgProps) {
|
|
137
|
+
const { value, ref, attrs } = useRegistration(path, "image", fallback, label);
|
|
138
|
+
const v = value as ImageValue;
|
|
139
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
140
|
+
return <img ref={ref as React.Ref<HTMLImageElement>} src={v.src} alt={v.alt ?? fallback.alt ?? ""} {...attrs} {...rest} />;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type EditableLinkProps = {
|
|
144
|
+
path: string;
|
|
145
|
+
fallback: LinkValue;
|
|
146
|
+
label?: string;
|
|
147
|
+
children?: React.ReactNode;
|
|
148
|
+
} & Omit<React.ComponentProps<typeof Link>, "href" | "children">;
|
|
149
|
+
|
|
150
|
+
/** next/link com href (e opcionalmente o rótulo) editáveis. Com children, o rótulo vem deles. */
|
|
151
|
+
function EditableLink({ path, fallback, label, children, style: styleProp, ...rest }: EditableLinkProps) {
|
|
152
|
+
const { value, ref, attrs, style } = useRegistration(path, "link", fallback, label);
|
|
153
|
+
const v = value as LinkValue;
|
|
154
|
+
const estilo = style || styleProp ? { ...(styleProp ?? {}), ...(style ?? {}) } : undefined;
|
|
155
|
+
return (
|
|
156
|
+
<Link ref={ref as React.Ref<HTMLAnchorElement>} href={v.href} {...attrs} {...rest} style={estilo}>
|
|
157
|
+
{v.label !== undefined && v.label !== fallback.label ? v.label : (children ?? v.label ?? fallback.label)}
|
|
158
|
+
</Link>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Válvula de escape: qualquer elemento vira editável por render-prop.
|
|
164
|
+
* `<Editable.Slot path="fundo" type="image" fallback={{src}}>{(v, attrs, ref) => <div ref={ref} {...attrs} style={{backgroundImage:`url(${v.src})`}} />}</Editable.Slot>`
|
|
165
|
+
*/
|
|
166
|
+
function Slot<T extends EditableValue>({
|
|
167
|
+
path,
|
|
168
|
+
type,
|
|
169
|
+
fallback,
|
|
170
|
+
label,
|
|
171
|
+
children,
|
|
172
|
+
}: {
|
|
173
|
+
path: string;
|
|
174
|
+
type: EditableType;
|
|
175
|
+
fallback: T;
|
|
176
|
+
label?: string;
|
|
177
|
+
/** o 4º argumento é a cor/fundo que o lojista deu SÓ a este elemento — aplique em `style` */
|
|
178
|
+
children: (value: T, attrs: Record<string, string | undefined>, ref: React.RefCallback<Element>, style?: { color?: string; background?: string }) => React.ReactNode;
|
|
179
|
+
}) {
|
|
180
|
+
const { value, ref, attrs, style } = useRegistration(path, type, fallback, label);
|
|
181
|
+
const setRef = React.useCallback<React.RefCallback<Element>>(
|
|
182
|
+
(el) => {
|
|
183
|
+
ref.current = el;
|
|
184
|
+
},
|
|
185
|
+
[ref],
|
|
186
|
+
);
|
|
187
|
+
return <>{children(value as T, attrs, setRef, style)}</>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* VITRINE: quais produtos a seção mostra. O documento guarda só a ESCOLHA (categoria da Unbox, lista de
|
|
192
|
+
* produtos ou busca); quem resolve isso em produtos é a LOJA, no servidor, com o cliente da Unbox que ela já
|
|
193
|
+
* usa — o editor nunca vê credencial nem catálogo, e preço/estoque continuam vindo do painel.
|
|
194
|
+
*
|
|
195
|
+
* Uso na loja (a escolha é lida no servidor com `escolhaDaVitrine`, os produtos são buscados lá, e este
|
|
196
|
+
* primitivo registra o caminho para o editor poder trocar):
|
|
197
|
+
*
|
|
198
|
+
* <Editable.Vitrine path="vitrine" label="Produtos desta vitrine" fallback={{ modo: "produtos", produtos: ids }}>
|
|
199
|
+
* {(escolha, attrs, ref, previa) => <div ref={ref} {...attrs}>{cards(previa?.produtos ?? doServidor)}</div>}
|
|
200
|
+
* </Editable.Vitrine>
|
|
201
|
+
*
|
|
202
|
+
* ── NA PRÉVIA É DIFERENTE, e é por isso que existe o 4º argumento ──────────────────────────────
|
|
203
|
+
* O rascunho não passa pelo servidor da loja: ele chega ao iframe por `postMessage` e vive no
|
|
204
|
+
* CLIENTE. Então, em modo edição, este primitivo pergunta à PRÓPRIA LOJA (`POST /api/unbox/vitrine`,
|
|
205
|
+
* autenticada pelo token do editor) o que a escolha do rascunho vira em produtos, e entrega o
|
|
206
|
+
* resultado em `previa`. Sem isso o lojista escolhia cinco produtos, clicava em aplicar e a tela não
|
|
207
|
+
* mudava — numa loja que nunca publicou, não mudava nunca.
|
|
208
|
+
*
|
|
209
|
+
* O 4º argumento é ACRÉSCIMO: em produção ele é `undefined` e um render-prop que só recebe
|
|
210
|
+
* `(escolha, attrs, ref)` continua valendo, byte por byte, como antes.
|
|
211
|
+
*
|
|
212
|
+
* ── SEÇÃO PRESA A UM PRODUTO SÓ ───────────────────────────────────────────────────────────────
|
|
213
|
+
* O mesmo primitivo, sem tipo novo: `fallback={vinculoDeProduto(slug)}` (modo `produtos`, um item,
|
|
214
|
+
* `limite: 1`) e a seção lê `produtoDoVinculo(previa, doServidor)` — o primeiro produto que voltar.
|
|
215
|
+
* É assim que o bloco de um variante, um card da grade ou o combo deixam o lojista TROCAR o produto
|
|
216
|
+
* que aparece ali, com o mesmo seletor e a mesma rota da vitrine de vários.
|
|
217
|
+
*/
|
|
218
|
+
function Vitrine({
|
|
219
|
+
path,
|
|
220
|
+
label,
|
|
221
|
+
fallback,
|
|
222
|
+
children,
|
|
223
|
+
}: {
|
|
224
|
+
path: string;
|
|
225
|
+
label?: string;
|
|
226
|
+
/** o que o CÓDIGO mostra quando o lojista ainda não escolheu */
|
|
227
|
+
fallback: VitrineValue;
|
|
228
|
+
/**
|
|
229
|
+
* O 4º argumento é a PRÉVIA: os produtos que a escolha do rascunho vira, buscados na loja aqui
|
|
230
|
+
* mesmo, no navegador. Ele é `undefined` em produção — lá quem resolve é o servidor, e o
|
|
231
|
+
* render-prop segue recebendo exatamente os três argumentos de sempre.
|
|
232
|
+
*/
|
|
233
|
+
children: (escolha: VitrineValue, attrs: Record<string, string | undefined>, ref: React.RefCallback<Element>, previa?: PreviaDaVitrine) => React.ReactNode;
|
|
234
|
+
}) {
|
|
235
|
+
const { value, ref, attrs, editing, guardado } = useRegistration(path, "vitrine", fallback, label);
|
|
236
|
+
const { previewToken, renovarToken } = useEditableContext();
|
|
237
|
+
const setRef = React.useCallback<React.RefCallback<Element>>((el) => {
|
|
238
|
+
ref.current = el;
|
|
239
|
+
}, [ref]);
|
|
240
|
+
|
|
241
|
+
// A ESCOLHA DO RASCUNHO, e só ela. Sem valor no documento não há o que perguntar: a seção mostra o
|
|
242
|
+
// que o CÓDIGO mostra, e disparar uma consulta para descobrir isso seria pedir à Unbox uma coisa
|
|
243
|
+
// que já está na tela. Vai como string porque é o que entra na lista de dependências do efeito:
|
|
244
|
+
// objeto novo a cada render re-dispararia a busca para sempre.
|
|
245
|
+
const chave = editing && vitrineValida(guardado) ? JSON.stringify(guardado) : "";
|
|
246
|
+
const [previa, setPrevia] = React.useState<PreviaDaVitrine>(PREVIA_PARADA);
|
|
247
|
+
|
|
248
|
+
React.useEffect(() => {
|
|
249
|
+
if (!chave) {
|
|
250
|
+
setPrevia(PREVIA_PARADA);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!previewToken) {
|
|
254
|
+
// Sem o token na URL (link colado à mão) não há como perguntar à loja. O lojista lê o MESMO
|
|
255
|
+
// aviso de qualquer outra falha, porque a saída dele é a mesma: recarregar a prévia. POR QUE o
|
|
256
|
+
// pedido não saiu é assunto nosso, e a tarja da loja não é lugar de contar isso.
|
|
257
|
+
setPrevia({ produtos: null, carregando: false, erro: ERRO_VITRINE });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const ac = new AbortController();
|
|
261
|
+
let vivo = true;
|
|
262
|
+
// os produtos de antes ficam na tela enquanto o novo pedido corre: piscar para vazio e voltar é
|
|
263
|
+
// pior de ler do que a lista antiga com o aviso de "buscando" em cima
|
|
264
|
+
setPrevia((p) => ({ produtos: p.produtos, carregando: true, erro: null }));
|
|
265
|
+
// trocar a escolha letra a letra (ou clicar em cinco produtos seguidos) não pode virar cinco
|
|
266
|
+
// pedidos: o último ganha, e o anterior é ABORTADO pela limpeza deste efeito
|
|
267
|
+
const timer = setTimeout(() => {
|
|
268
|
+
void (async () => {
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch("/api/unbox/vitrine", {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "content-type": "application/json", "x-editor-token": previewToken },
|
|
273
|
+
body: JSON.stringify({ escolha: JSON.parse(chave) as VitrineValue }),
|
|
274
|
+
signal: ac.signal,
|
|
275
|
+
cache: "no-store",
|
|
276
|
+
});
|
|
277
|
+
const corpo = (await res.json().catch(() => null)) as { produtos?: unknown; error?: unknown } | null;
|
|
278
|
+
if (res.status === 401) {
|
|
279
|
+
// token de prévia vencido (ele vale 15 min): pede outro ao editor. Quando ele chegar,
|
|
280
|
+
// `previewToken` muda e este efeito roda de novo sozinho. A renovação é mecânica NOSSA e
|
|
281
|
+
// não é narrada ao lojista: ele lê o aviso único, que some sozinho se o token novo vier.
|
|
282
|
+
renovarToken();
|
|
283
|
+
throw new Error(ERRO_VITRINE);
|
|
284
|
+
}
|
|
285
|
+
if (!res.ok) throw new Error(typeof corpo?.error === "string" && corpo.error ? corpo.error : ERRO_VITRINE);
|
|
286
|
+
const produtos = produtosDaVitrine(corpo);
|
|
287
|
+
// resposta fora do contrato é ERRO, não vitrine vazia: fingir que não há produto é como o
|
|
288
|
+
// lojista acaba publicando uma seção em branco sem nunca ter sido avisado
|
|
289
|
+
if (!produtos) throw new Error(ERRO_VITRINE);
|
|
290
|
+
if (vivo) setPrevia({ produtos, carregando: false, erro: null });
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (ac.signal.aborted || !vivo) return; // pedido cancelado por outro mais novo: não é falha
|
|
293
|
+
setPrevia({ produtos: null, carregando: false, erro: err instanceof Error ? err.message : ERRO_VITRINE });
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
}, ESPERA_VITRINE_MS);
|
|
297
|
+
return () => {
|
|
298
|
+
vivo = false;
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
ac.abort();
|
|
301
|
+
};
|
|
302
|
+
}, [chave, previewToken, renovarToken]);
|
|
303
|
+
|
|
304
|
+
// ESCOLHA QUE NÃO ACHOU NADA. Lista vazia é resposta legítima da loja ("hoje essa escolha não
|
|
305
|
+
// mostra produto nenhum"), e é o caso em que a tela NÃO muda depois de aplicar: a seção cai nos
|
|
306
|
+
// produtos do código, que é o que a página publicada também faria. Sem dizer isso, o lojista lê
|
|
307
|
+
// "não funcionou" — a mesma leitura que fez a função inteira parecer quebrada no teste do dono.
|
|
308
|
+
const vazia = Boolean(previa.produtos && previa.produtos.length === 0 && !previa.carregando && !previa.erro);
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<>
|
|
312
|
+
{editing && (previa.carregando || previa.erro || vazia) ? <AvisoDaVitrine carregando={previa.carregando} erro={previa.erro} vazia={vazia} /> : null}
|
|
313
|
+
{children(value as VitrineValue, attrs, setRef, editing ? previa : undefined)}
|
|
314
|
+
</>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* UM aviso para toda falha de busca. Token vencido, token ausente e resposta fora do contrato são
|
|
320
|
+
* três defeitos NOSSOS com uma única saída para o lojista — recarregar a prévia. Três frases
|
|
321
|
+
* diferentes para a mesma coisa a fazer só ensinam a ele o nosso encanamento.
|
|
322
|
+
*/
|
|
323
|
+
const ERRO_VITRINE = "Não consegui buscar os produtos na loja agora.";
|
|
324
|
+
|
|
325
|
+
/** Nada resolvido pela prévia: a seção mostra o que o servidor entregou (ou o que o código traz). */
|
|
326
|
+
const PREVIA_PARADA: PreviaDaVitrine = { produtos: null, carregando: false, erro: null };
|
|
327
|
+
|
|
328
|
+
/** Espera antes de perguntar à loja. Clicar em vários produtos seguidos vira UM pedido. */
|
|
329
|
+
const ESPERA_VITRINE_MS = 300;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* A tarja de estado da vitrine, SÓ em modo edição.
|
|
333
|
+
*
|
|
334
|
+
* Ela existe porque o lojista clicou em "aplicar" e precisa ver que algo está acontecendo: sem
|
|
335
|
+
* isso, a espera da Unbox é indistinguível de "não funcionou" — que foi exatamente a leitura do
|
|
336
|
+
* dono no teste. Estilo em `style` inline de propósito: o primitivo não pode depender do CSS da
|
|
337
|
+
* loja, e em produção esta tarja não existe.
|
|
338
|
+
*/
|
|
339
|
+
// A tarja é ferramenta do EDITOR, não copy da loja: `data-editor-ignore` tira do gate de cobertura
|
|
340
|
+
// o texto dela — senão "Buscando os produtos na loja…" apareceria na lista de "fora dos primitivos"
|
|
341
|
+
// de quem rodasse o gate no exato segundo em que uma vitrine estivesse carregando.
|
|
342
|
+
const AVISO_FORA_DA_COBERTURA = { "data-editor-ignore": "" } as Record<string, string>;
|
|
343
|
+
|
|
344
|
+
function AvisoDaVitrine({ carregando, erro, vazia = false }: { carregando: boolean; erro: string | null; /** a escolha resolveu, e não trouxe produto nenhum */ vazia?: boolean }) {
|
|
345
|
+
const falhou = Boolean(erro);
|
|
346
|
+
// três tarjas, três cores: vermelha = a loja não respondeu; âmbar = respondeu, e não tem produto;
|
|
347
|
+
// azul = buscando. Confundir as duas primeiras é como o lojista publica uma seção às cegas.
|
|
348
|
+
const tom = falhou ? { borda: "#e4b4b4", fundo: "#fdeeee", texto: "#8a1f1f" } : vazia ? { borda: "#e6d3a3", fundo: "#fdf6e6", texto: "#6b5312" } : { borda: "#d3d8e8", fundo: "#eef1f8", texto: "#39415c" };
|
|
349
|
+
return (
|
|
350
|
+
<div
|
|
351
|
+
role="status"
|
|
352
|
+
aria-live="polite"
|
|
353
|
+
data-editor-vitrine-aviso={falhou ? "erro" : vazia ? "vazio" : "carregando"}
|
|
354
|
+
{...AVISO_FORA_DA_COBERTURA}
|
|
355
|
+
style={{
|
|
356
|
+
margin: "0 0 10px",
|
|
357
|
+
padding: "7px 11px",
|
|
358
|
+
borderRadius: 10,
|
|
359
|
+
border: `1px solid ${tom.borda}`,
|
|
360
|
+
background: tom.fundo,
|
|
361
|
+
color: tom.texto,
|
|
362
|
+
font: "500 13px/1.45 system-ui, -apple-system, Segoe UI, sans-serif",
|
|
363
|
+
// A vitrine de UM produto costuma ser um CARD dentro de uma grade (`<ul class="grid">`), e a
|
|
364
|
+
// tarja é irmã dele: sem isto ela ocuparia uma célula e embaralharia as colunas. Numa
|
|
365
|
+
// grade, atravessa a linha inteira e lê como aviso da seção; em qualquer outro pai, esta
|
|
366
|
+
// linha é ignorada e ela continua sendo o bloco de sempre.
|
|
367
|
+
gridColumn: "1 / -1",
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
{falhou ? (
|
|
371
|
+
<>
|
|
372
|
+
{erro}{" "}
|
|
373
|
+
{/* A busca só é refeita quando a ESCOLHA muda: clicar em aplicar de novo, com a mesma
|
|
374
|
+
escolha, não mudaria nada no documento e não dispararia pedido nenhum. Recarregar a
|
|
375
|
+
prévia dispara — e dizer isso é melhor do que deixar o lojista clicando em aplicar. */}
|
|
376
|
+
<span style={{ opacity: 0.75 }}>Recarregue a prévia para tentar de novo.</span>
|
|
377
|
+
</>
|
|
378
|
+
) : vazia ? (
|
|
379
|
+
<>
|
|
380
|
+
Nenhum produto veio dessa escolha.{" "}
|
|
381
|
+
{/* É o que a página publicada faria também: sem produto resolvido, a seção volta aos
|
|
382
|
+
produtos ORIGINAIS. Dizer isso evita a leitura de "cliquei em aplicar e não mudou nada".
|
|
383
|
+
"Originais" e não "o que vem do código": o lojista não vê código, vê a seção como ela
|
|
384
|
+
nasceu — e é a mesma palavra do "voltar ao original" do painel. */}
|
|
385
|
+
<span style={{ opacity: 0.75 }}>A seção está mostrando os produtos originais. Escolha outro produto ou outra categoria.</span>
|
|
386
|
+
</>
|
|
387
|
+
) : (
|
|
388
|
+
"Buscando os produtos na loja…"
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* VÍDEO: mesmo valor da imagem (`{ src, alt? }`), tipo próprio no documento. O tipo é o que faz o painel
|
|
396
|
+
* mostrar prévia de vídeo, aceitar arquivo de até 32 MB (mp4/webm) e não oferecer o vídeo onde cabe foto.
|
|
397
|
+
*
|
|
398
|
+
* É RENDER-PROP, e não um `<video src>` pronto, porque um vídeo de verdade tem filhos e vizinhos que só
|
|
399
|
+
* a loja conhece: `<source>`, pôster, `key` para trocar de arquivo, o `ref` imperativo do play/pause e a
|
|
400
|
+
* escolha entre arquivo de computador e de celular. Renderizar cego forçaria toda loja a abandonar isso.
|
|
401
|
+
*
|
|
402
|
+
* <Editable.Video path="video" label="Vídeo de abertura" fallback={{ src: "/brand/hero.mp4" }}>
|
|
403
|
+
* {(v, attrs, ref) => <video ref={ref} {...attrs} src={v.src} muted loop playsInline />}
|
|
404
|
+
* </Editable.Video>
|
|
405
|
+
*
|
|
406
|
+
* A loja que já tem um `useRef` no `<video>` precisa juntar os dois refs numa função só — o `ref` daqui
|
|
407
|
+
* é callback; guardar o elemento no ref próprio E chamar este é o que mantém play/pause funcionando.
|
|
408
|
+
*/
|
|
409
|
+
function Video({
|
|
410
|
+
path,
|
|
411
|
+
label,
|
|
412
|
+
fallback,
|
|
413
|
+
children,
|
|
414
|
+
}: {
|
|
415
|
+
path: string;
|
|
416
|
+
label?: string;
|
|
417
|
+
/** o arquivo que o CÓDIGO usa enquanto o lojista não trocar */
|
|
418
|
+
fallback: ImageValue;
|
|
419
|
+
children: (valor: ImageValue, attrs: Record<string, string | undefined>, ref: React.RefCallback<Element>) => React.ReactNode;
|
|
420
|
+
}) {
|
|
421
|
+
const { value, ref, attrs } = useRegistration(path, "video", fallback, label);
|
|
422
|
+
const setRef = React.useCallback<React.RefCallback<Element>>((el) => {
|
|
423
|
+
ref.current = el;
|
|
424
|
+
}, [ref]);
|
|
425
|
+
return <>{children(value as ImageValue, attrs, setRef)}</>;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ícone substituível: renderiza o ícone do código (children) até o lojista subir uma
|
|
430
|
+
* imagem no lugar. Fallback `{src: ""}` = "ícone original". Funciona para SVG inline,
|
|
431
|
+
* componente de ícone ou <img>.
|
|
432
|
+
*/
|
|
433
|
+
function Icon({
|
|
434
|
+
path,
|
|
435
|
+
label,
|
|
436
|
+
size,
|
|
437
|
+
className,
|
|
438
|
+
children,
|
|
439
|
+
}: {
|
|
440
|
+
path: string;
|
|
441
|
+
label?: string;
|
|
442
|
+
/** tamanho em px da imagem enviada (default: o tamanho do ícone original, via CSS) */
|
|
443
|
+
size?: number;
|
|
444
|
+
className?: string;
|
|
445
|
+
children: React.ReactNode;
|
|
446
|
+
}) {
|
|
447
|
+
const { value, ref, attrs } = useRegistration(path, "image", { src: "", alt: label ?? "" }, label);
|
|
448
|
+
const v = value as ImageValue;
|
|
449
|
+
if (v.src) {
|
|
450
|
+
return <img ref={ref as React.Ref<HTMLImageElement>} src={v.src} alt={v.alt ?? label ?? ""} width={size} height={size} className={className} style={{ display: "inline-block", objectFit: "contain" }} {...attrs} />;
|
|
451
|
+
}
|
|
452
|
+
return (
|
|
453
|
+
<span ref={ref as React.Ref<HTMLSpanElement>} className={className} style={{ display: "inline-flex", lineHeight: 0 }} {...attrs}>
|
|
454
|
+
{children}
|
|
455
|
+
</span>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** marcador do bloco vazio e aviso de recusa: estilo INLINE, para não depender do CSS da loja */
|
|
460
|
+
const MARCADOR_HTML: React.CSSProperties = {
|
|
461
|
+
display: "block",
|
|
462
|
+
padding: "14px 16px",
|
|
463
|
+
border: "1px dashed currentColor",
|
|
464
|
+
borderRadius: 8,
|
|
465
|
+
font: "500 13px/1.45 system-ui, -apple-system, Segoe UI, sans-serif",
|
|
466
|
+
opacity: 0.7,
|
|
467
|
+
cursor: "pointer",
|
|
468
|
+
};
|
|
469
|
+
const AVISO_HTML: React.CSSProperties = { ...MARCADOR_HTML, border: "1px solid #b42318", color: "#b42318", opacity: 1 };
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* A CERCA DO BLOCO DE HTML — o que impede o que está DENTRO do bloco de sair por cima da loja.
|
|
473
|
+
*
|
|
474
|
+
* O modo de falha real, e por que a limpeza do editor sozinha não fecha isto: a limpeza olha o
|
|
475
|
+
* `style=` (é lá que ela tira `position: fixed`), mas `class=` ela DEIXA passar de propósito, porque
|
|
476
|
+
* é assim que um selo colado usa as classes da própria loja. Só que as classes da loja incluem as do
|
|
477
|
+
* Tailwind, e `class="fixed inset-0 z-50"` faz exatamente o que o `style=` estava proibido de fazer:
|
|
478
|
+
* cobre a página inteira com um retângulo, e o botão de comprar fica embaixo. Nenhuma lista de
|
|
479
|
+
* classes proibidas resolveria — a loja escolhe o CSS dela, e a lista envelheceria sozinha. Então a
|
|
480
|
+
* regra não é sobre o QUE está escrito lá dentro, é sobre até onde o resultado pode chegar.
|
|
481
|
+
*
|
|
482
|
+
* `contain: layout paint` faz as duas coisas de que isto depende, e é por isso que são as duas:
|
|
483
|
+
* • LAYOUT: o invólucro vira o bloco que contém os `position: fixed` e `absolute` de dentro. O
|
|
484
|
+
* `fixed` para de se medir pela janela e passa a se medir por este `<div>`; `inset-0` cobre o
|
|
485
|
+
* bloco, não a tela. Vira também um contexto de empilhamento próprio, então o `z-50` de dentro
|
|
486
|
+
* disputa com os irmãos DELE, e nunca com o cabeçalho da loja.
|
|
487
|
+
* • PAINT: o que for maior que o invólucro é recortado nele, em vez de ser pintado por cima do
|
|
488
|
+
* resto da página (um `width: 100vw`, uma margem negativa de mil pixels).
|
|
489
|
+
*
|
|
490
|
+
* O que fica DE FORA da lista, e não é esquecimento: `size`. Contenção de tamanho manda o navegador
|
|
491
|
+
* calcular a altura do invólucro como se ele estivesse VAZIO, e o bloco inteiro colapsaria para zero.
|
|
492
|
+
*
|
|
493
|
+
* `position: relative` é o degrau de baixo, para navegador que não conheça `contain`: ele já prende
|
|
494
|
+
* o `absolute` (o `fixed`, não). Vem junto por ser de graça e não mudar nada do desenho.
|
|
495
|
+
*
|
|
496
|
+
* Isto é aplicado DEPOIS do `style` que a receita da loja passou: contenção que a receita pode
|
|
497
|
+
* desligar sem querer não é contenção. O `overflowX: auto` continua antes, esse sim negociável.
|
|
498
|
+
*/
|
|
499
|
+
const CERCA_DO_HTML: React.CSSProperties = { contain: "layout paint", position: "relative" };
|
|
500
|
+
|
|
501
|
+
type HtmlProps = {
|
|
502
|
+
path: string;
|
|
503
|
+
/** o HTML que o CÓDIGO traz. Quase sempre vazio: o bloco nasce para o lojista colar. */
|
|
504
|
+
fallback?: string;
|
|
505
|
+
label?: string;
|
|
506
|
+
} & Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML">;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* BLOCO DE HTML: o lojista cola HTML pronto (um selo do fornecedor, uma tabela, um trecho que veio
|
|
510
|
+
* de fora). O chat NÃO escreve aqui — o agente aponta o bloco e quem cola é o lojista.
|
|
511
|
+
*
|
|
512
|
+
* O caminho ganha o sufixo `.html` AQUI (`path="conteudo"` → `home.bloco-1.conteudo.html`): é ele que
|
|
513
|
+
* diz ao servidor da loja que o valor é HTML, e a loja lê o publicado sem manifesto nenhum.
|
|
514
|
+
*
|
|
515
|
+
* Este é o terceiro lugar em que a lista de recusa roda, e o ÚNICO que existe na PRÉVIA — ali o
|
|
516
|
+
* rascunho chega por postMessage e não passa por servidor. Por isso aqui é RECUSAR, não limpar: HTML
|
|
517
|
+
* perigoso não é injetado, e em modo edição o bloco diz o motivo em vez de mostrar outra coisa calado.
|
|
518
|
+
*
|
|
519
|
+
* E o que NENHUMA das três camadas resolve com lista, porque `class=` passa de propósito, é o alcance
|
|
520
|
+
* do que ficou: quem prende isso é a CERCA (`CERCA_DO_HTML`, logo acima) no invólucro deste bloco.
|
|
521
|
+
*
|
|
522
|
+
* Em produção, bloco vazio não rende NADA — nem invólucro. Um `<div>` de 0px ainda come o `gap` do
|
|
523
|
+
* container e abre uma faixa branca no meio da página.
|
|
524
|
+
*/
|
|
525
|
+
function Html({ path, fallback = "", label, style: styleProp, ...rest }: HtmlProps) {
|
|
526
|
+
// sufixo idempotente: o construtor escreve `path="conteudo"`, mas `path="conteudo.html"` não vira `.html.html`
|
|
527
|
+
const caminho = path.endsWith(SUFIXO_HTML) ? path : path + SUFIXO_HTML;
|
|
528
|
+
const { value, ref, attrs, editing, guardado } = useRegistration(caminho, "html", fallback, label);
|
|
529
|
+
const html = typeof value === "string" ? value : "";
|
|
530
|
+
// recusa no CLIENTE: vale para o rascunho da prévia e para o `fallback` do próprio código
|
|
531
|
+
const recusaNoRender = html ? recusaDeHtml(html) : null;
|
|
532
|
+
// e o que o lojista GRAVOU já tinha sido recusado lá no `resolveValue` (que devolveu o fallback)?
|
|
533
|
+
// sem esta linha, o bloco mostraria o conteúdo do código e ninguém saberia por quê
|
|
534
|
+
const recusaGuardada = typeof guardado === "string" && guardado !== html ? recusaDeHtml(guardado) : null;
|
|
535
|
+
const motivo = recusaGuardada ?? recusaNoRender;
|
|
536
|
+
const seguro = recusaNoRender ? "" : html;
|
|
537
|
+
if (!seguro && !editing) return null;
|
|
538
|
+
const estilo: React.CSSProperties = { overflowX: "auto", ...styleProp, ...CERCA_DO_HTML };
|
|
539
|
+
const caixa = { ...attrs, ...rest, style: estilo };
|
|
540
|
+
const aviso = editing && motivo ? <span style={AVISO_HTML}>Este HTML não foi aplicado: {motivo}</span> : null;
|
|
541
|
+
if (!seguro) {
|
|
542
|
+
return (
|
|
543
|
+
<div ref={ref as React.Ref<HTMLDivElement>} {...caixa}>
|
|
544
|
+
{aviso ?? <span style={MARCADOR_HTML}>Bloco de HTML vazio: clique aqui e cole o seu HTML</span>}
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
// recusa + conteúdo do código ao mesmo tempo: o aviso vem ANTES, e o HTML entra num filho
|
|
549
|
+
// `display: contents` (sem caixa própria) para o layout não mudar por causa do aviso
|
|
550
|
+
if (aviso) {
|
|
551
|
+
return (
|
|
552
|
+
<div ref={ref as React.Ref<HTMLDivElement>} {...caixa}>
|
|
553
|
+
{aviso}
|
|
554
|
+
<div style={{ display: "contents" }} dangerouslySetInnerHTML={{ __html: seguro }} />
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
// `dangerouslySetInnerHTML` e `children` não convivem no mesmo elemento — por isso os três retornos.
|
|
559
|
+
// O HTML aqui já passou pela lista; o teto entra na conta porque `recusaDeHtml` o cobre.
|
|
560
|
+
return <div ref={ref as React.Ref<HTMLDivElement>} {...caixa} dangerouslySetInnerHTML={{ __html: seguro }} />;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Seção: dá identidade estável (id) e escopo de caminho (`<container>.<id>.…`).
|
|
565
|
+
* Em produção, seção oculta não renderiza. O wrapper é `display:contents` — não
|
|
566
|
+
* cria caixa, não muda layout.
|
|
567
|
+
*/
|
|
568
|
+
function Section({
|
|
569
|
+
id,
|
|
570
|
+
label,
|
|
571
|
+
container,
|
|
572
|
+
fixed = false,
|
|
573
|
+
clone = false,
|
|
574
|
+
criada = false,
|
|
575
|
+
tipo,
|
|
576
|
+
kind,
|
|
577
|
+
item = false,
|
|
578
|
+
ordemNoCodigo,
|
|
579
|
+
children,
|
|
580
|
+
}: {
|
|
581
|
+
id: string;
|
|
582
|
+
label?: string;
|
|
583
|
+
container?: string;
|
|
584
|
+
/** cabeçalho/rodapé: aparece no manifesto, mas não move nem oculta */
|
|
585
|
+
fixed?: boolean;
|
|
586
|
+
/** cópia feita pelo lojista (Editable.Sections marca sozinho) */
|
|
587
|
+
clone?: boolean;
|
|
588
|
+
/** seção ADICIONADA pelo lojista, instanciada do catálogo (Editable.Sections marca sozinho) */
|
|
589
|
+
criada?: boolean;
|
|
590
|
+
/** tipo do catálogo que originou a seção criada (só com `criada`) */
|
|
591
|
+
tipo?: string;
|
|
592
|
+
/** tipo da seção no vocabulário fechado (`SECTION_KINDS`): o nome que qualquer pessoa entende no painel */
|
|
593
|
+
kind?: SectionKind;
|
|
594
|
+
/** item de uma lista dentro da seção (dentro de `<Editable.Sections nested>`): não precisa de kind */
|
|
595
|
+
item?: boolean;
|
|
596
|
+
/**
|
|
597
|
+
* Posição desta seção NO CÓDIGO (quem preenche é `Editable.Sections`). Não muda quando o lojista
|
|
598
|
+
* reordena — é o que permite ao editor saber a ordem original mesmo lendo o DOM já reordenado.
|
|
599
|
+
*/
|
|
600
|
+
ordemNoCodigo?: number;
|
|
601
|
+
children: React.ReactNode;
|
|
602
|
+
}) {
|
|
603
|
+
const ctx = useEditableContext();
|
|
604
|
+
// SEM CONTAINER, SEM ESCOPO (foundation 11). O padrão era `ctx.container = "home"`, e por causa dele
|
|
605
|
+
// uma página que envolvesse suas seções sem declarar container entrava escrevendo NA HOME e passava
|
|
606
|
+
// a disputar a ordem da home. Agora, sem container declarado, a seção RENDERIZA (a loja não cai) e
|
|
607
|
+
// não é editável: não se registra, não obedece ordem nem ocultar, e o escopo vazio faz os primitivos
|
|
608
|
+
// de dentro recusarem a raiz do documento, cada um dizendo por quê.
|
|
609
|
+
const cont = container ?? ctx.container;
|
|
610
|
+
const ref = React.useRef<HTMLDivElement | null>(null);
|
|
611
|
+
// seção fixa (cabeçalho/rodapé) nunca some, mesmo que um documento diga o contrário
|
|
612
|
+
const hidden = Boolean(cont) && !fixed && ctx.layout && (ctx.doc.sections[cont as string]?.hidden ?? []).includes(id);
|
|
613
|
+
const { editing, registerSection } = ctx;
|
|
614
|
+
const semLayout = !ctx.layout;
|
|
615
|
+
React.useEffect(() => {
|
|
616
|
+
if (!editing || !cont) return;
|
|
617
|
+
return registerSection({ container: cont, id, label, clone, criada, tipo, fixed, kind, item, ordemNoCodigo, semLayout, el: () => ref.current });
|
|
618
|
+
}, [editing, registerSection, cont, id, label, clone, criada, tipo, fixed, kind, item, ordemNoCodigo, semLayout]);
|
|
619
|
+
React.useEffect(() => {
|
|
620
|
+
if (cont) return;
|
|
621
|
+
avisarConstrutor(
|
|
622
|
+
`section:${id}`,
|
|
623
|
+
`[Editable] a seção "${id}" está fora de um <Editable.Sections container="...">: ela aparece na tela, mas nada dentro dela é editável, porque os caminhos cairiam na raiz do documento. Declare o container desta página (<Editable.Sections container="sobre">) ou passe container= nesta seção. Duas páginas que declaram o MESMO container dividem ordem e copy de propósito; página que quer copy própria declara container próprio.`,
|
|
624
|
+
);
|
|
625
|
+
}, [cont, id]);
|
|
626
|
+
const value = React.useMemo(() => ({ ...ctx, container: cont, section: cont ? id : undefined, scope: cont ? [cont, id] : [] }), [ctx, cont, id]);
|
|
627
|
+
// fundo da seção inteira (`<container>.<id>.estilo`): o invólucro é `display: contents`, então
|
|
628
|
+
// a cor vai numa variável herdada e a regra global pinta o filho direto (ver EditableProvider)
|
|
629
|
+
const fundo = cont ? resolveStyle(ctx.doc, `${cont}.${id}`)?.background : undefined;
|
|
630
|
+
if (hidden && !editing) return null;
|
|
631
|
+
return (
|
|
632
|
+
<EditableContextProvider value={value}>
|
|
633
|
+
<div
|
|
634
|
+
ref={ref}
|
|
635
|
+
style={fundo ? ({ display: "contents", "--unbox-sec-bg": fundo } as React.CSSProperties) : { display: "contents" }}
|
|
636
|
+
data-unbox-sec-bg={fundo ? "1" : undefined}
|
|
637
|
+
{...(editing && cont
|
|
638
|
+
? { "data-editor-section": id, "data-editor-container": cont, "data-editor-label": label, "data-editor-kind": kind, "data-editor-item": item ? "1" : undefined, "data-editor-fixed": fixed ? "1" : undefined, "data-editor-clone": clone ? "1" : undefined, "data-editor-criada": criada ? "1" : undefined, "data-editor-tipo": tipo, "data-editor-section-hidden": hidden ? "1" : undefined }
|
|
639
|
+
: {})}
|
|
640
|
+
>
|
|
641
|
+
{children}
|
|
642
|
+
</div>
|
|
643
|
+
</EditableContextProvider>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* UM TIPO DO CATÁLOGO DA LOJA — o que o lojista pode ADICIONAR neste container.
|
|
649
|
+
*
|
|
650
|
+
* O catálogo é CURADO e é da loja (6 a 8 tipos, só os que renderizam bem sem props de receita); o
|
|
651
|
+
* editor nunca tem lista própria — 100 lojas, 100 catálogos. Isto não abre exceção na regra das "4
|
|
652
|
+
* latas" (não existe catálogo de EDITABILIDADE): o que a loja declara aqui é o catálogo de COMPONENTES
|
|
653
|
+
* que ela já tem, e a seção adicionada nasce com os LITERAIS do componente (os `fallback`), sem nada
|
|
654
|
+
* escrito no documento.
|
|
655
|
+
*/
|
|
656
|
+
export interface TipoDeSecao {
|
|
657
|
+
/** rótulo que o lojista lê no "+" ("Perguntas frequentes") */
|
|
658
|
+
label: string;
|
|
659
|
+
/** uma frase dizendo o que a seção mostra (sem imagem de prévia no v1) */
|
|
660
|
+
descricao?: string;
|
|
661
|
+
/** tipo no vocabulário fechado (`SECTION_KINDS`) */
|
|
662
|
+
kind?: SectionKind;
|
|
663
|
+
/** o que renderizar. Fecha por closure os dados que a loja já buscou no servidor. */
|
|
664
|
+
render: () => React.ReactNode;
|
|
665
|
+
}
|
|
666
|
+
/** tipo → como instanciá-lo. A CHAVE é o que entra no id da seção criada (`novo-<tipo>-<n>`). */
|
|
667
|
+
export type CatalogoDeSecoes = Record<string, TipoDeSecao>;
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Container de seções: reordena/oculta os filhos pelo `id` deles conforme o
|
|
671
|
+
* documento. Cada filho direto precisa ser um `<Editable.Section id=…>` (ou
|
|
672
|
+
* qualquer elemento com prop `id`).
|
|
673
|
+
*/
|
|
674
|
+
type SectionsPropsBase = {
|
|
675
|
+
/** false = renderiza na ordem do código, sem ocultar (ex.: página de produto que reusa a home) */
|
|
676
|
+
layout?: boolean;
|
|
677
|
+
/**
|
|
678
|
+
* O que o lojista pode ADICIONAR neste container. Sem catálogo, o container não oferece "adicionar
|
|
679
|
+
* seção" — e é o padrão: quem não declara nada continua exatamente como antes.
|
|
680
|
+
*/
|
|
681
|
+
catalogo?: CatalogoDeSecoes;
|
|
682
|
+
children: React.ReactNode;
|
|
683
|
+
};
|
|
684
|
+
/**
|
|
685
|
+
* O CONTAINER É OBRIGATÓRIO (foundation 11), e o compilador cobra. Ele era `?? "home"`, e o padrão
|
|
686
|
+
* fazia qualquer página que esquecesse de declarar entrar escrevendo na HOME. A regra que vale agora:
|
|
687
|
+
* a ORDEM é do container e a COPY é do container. Duas páginas que declaram o mesmo container
|
|
688
|
+
* dividem ordem e copy DE PROPÓSITO (é assim que o `chrome` vale no site inteiro); a página que quer
|
|
689
|
+
* copy própria declara container próprio. Uma chave de container nova é inerte para todo código já no
|
|
690
|
+
* ar, então declarar não custa migração nenhuma.
|
|
691
|
+
*
|
|
692
|
+
* `nested` é a única forma sem container explícito, e não é exceção: ali o container É a seção em
|
|
693
|
+
* volta (o escopo atual, ex.: "home.faq"), que já foi declarada por alguém.
|
|
694
|
+
*/
|
|
695
|
+
type SectionsProps = SectionsPropsBase &
|
|
696
|
+
(
|
|
697
|
+
| {
|
|
698
|
+
container: string;
|
|
699
|
+
nested?: false;
|
|
700
|
+
}
|
|
701
|
+
| {
|
|
702
|
+
container?: undefined;
|
|
703
|
+
/**
|
|
704
|
+
* LISTA DENTRO DE UMA SEÇÃO (bloco B9): `nested` faz o container ser o escopo atual (ex.: "home.faq"),
|
|
705
|
+
* então cada filho `<Editable.Section item id="pergunta-1">` mantém os caminhos de sempre
|
|
706
|
+
* (`home.faq.pergunta-1.pergunta`) e ganha ordem, ocultar, duplicar e remover — sem catálogo.
|
|
707
|
+
*/
|
|
708
|
+
nested: true;
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
function Sections(props: SectionsProps) {
|
|
713
|
+
const { container: containerProp, layout = true, nested = false, catalogo, children } = props as SectionsPropsBase & { container?: string; nested?: boolean };
|
|
714
|
+
const ctx = useEditableContext();
|
|
715
|
+
// sem container declarado, NADA aqui dentro é editável: `container` vazio desce como `undefined`,
|
|
716
|
+
// as seções não se registram e os primitivos delas recusam a raiz do documento. A lista continua
|
|
717
|
+
// renderizando na ordem do código, porque derrubar a loja não conserta o defeito de quem a montou.
|
|
718
|
+
// `nested` só vale DENTRO de uma seção: é ela o container. Um `Editable.Scope` solto na raiz também
|
|
719
|
+
// enche o escopo ("atributos"), e por isso a pergunta é pela seção em vigor, não pelo escopo
|
|
720
|
+
const container = (nested ? (ctx.section ? ctx.scope.join(".") : "") : containerProp) || undefined;
|
|
721
|
+
React.useEffect(() => {
|
|
722
|
+
if (container) return;
|
|
723
|
+
avisarConstrutor(
|
|
724
|
+
nested ? "sections:nested-sem-secao" : "sections:sem-container",
|
|
725
|
+
nested
|
|
726
|
+
? `[Editable] <Editable.Sections nested> fora de uma <Editable.Section>: a lista aninhada usa a seção em volta como container, e aqui não há nenhuma. A lista aparece na tela, mas nada dentro dela é editável.`
|
|
727
|
+
: `[Editable] <Editable.Sections> sem container: declare o container desta página (<Editable.Sections container="sobre">). A lista aparece na tela, mas nada dentro dela é editável, porque os caminhos cairiam na raiz do documento.`,
|
|
728
|
+
);
|
|
729
|
+
}, [container, nested]);
|
|
730
|
+
const state = layout && container ? ctx.doc.sections[container] : undefined;
|
|
731
|
+
const items = React.Children.toArray(children).filter(React.isValidElement) as React.ReactElement<{ id?: string }>[];
|
|
732
|
+
const ids = items.map((c) => c.props.id).filter((x): x is string => typeof x === "string");
|
|
733
|
+
if (process.env.NODE_ENV !== "production" && ids.length !== items.length) {
|
|
734
|
+
console.warn("[Editable.Sections] todo filho direto precisa de `id`; os sem id foram descartados");
|
|
735
|
+
}
|
|
736
|
+
const byId = new Map(items.filter((c) => typeof c.props.id === "string").map((c) => [c.props.id as string, c]));
|
|
737
|
+
// cópias do lojista: o MESMO componente da origem, com id (= escopo) próprio → todos
|
|
738
|
+
// os textos/imagens da cópia nascem editáveis, sem código novo
|
|
739
|
+
const clones = layout ? Object.entries(state?.clones ?? {}).filter(([, src]) => byId.has(src)) : [];
|
|
740
|
+
// seções ADICIONADAS pelo lojista: só as de tipo que ESTA loja tem no catálogo. Um tipo que ela não
|
|
741
|
+
// conhece (catálogo mudou, loja atrás da versão) fica de fora da TELA sem estourar — o painel continua
|
|
742
|
+
// listando a seção, porque `manifestWithClones` a deriva do rascunho, e é lá que ele explica.
|
|
743
|
+
// `render` de verdade, não só "a chave existe": um tipo herdado do prototype ("constructor",
|
|
744
|
+
// "toString") vindo de um documento antigo devolveria um valor truthy e estouraria no render
|
|
745
|
+
const doCatalogo = (tipo: string): TipoDeSecao | undefined => {
|
|
746
|
+
const t = catalogo && Object.prototype.hasOwnProperty.call(catalogo, tipo) ? catalogo[tipo] : undefined;
|
|
747
|
+
return t && typeof t.render === "function" ? t : undefined;
|
|
748
|
+
};
|
|
749
|
+
const criadas = layout && catalogo ? Object.entries(state?.criadas ?? {}).filter(([, tipo]) => Boolean(doCatalogo(tipo))) : [];
|
|
750
|
+
const todos = [...ids, ...clones.map(([id]) => id).filter((id) => !byId.has(id)), ...criadas.map(([id]) => id).filter((id) => !byId.has(id))];
|
|
751
|
+
const order = state?.order ?? [];
|
|
752
|
+
const present = new Set(todos);
|
|
753
|
+
const ordered = [...order.filter((id) => present.has(id)), ...todos.filter((id) => !order.includes(id))];
|
|
754
|
+
const origem = new Map(clones);
|
|
755
|
+
const tipoDaCriada = new Map(criadas);
|
|
756
|
+
const value = React.useMemo(() => ({ ...ctx, container, layout }), [ctx, container, layout]);
|
|
757
|
+
// o catálogo vira uma ASSINATURA de texto: o objeto é recriado a cada render da loja, e depender
|
|
758
|
+
// dele por referência re-registraria os tipos sem parar (o manifesto nunca assentaria)
|
|
759
|
+
const { editing, registerTipos } = ctx;
|
|
760
|
+
const assinaturaDoCatalogo = catalogo && layout && container ? Object.entries(catalogo).map(([tipo, d]) => [tipo, d.label, d.descricao ?? "", d.kind ?? ""].join("\u0000")).join("\u0001") : "";
|
|
761
|
+
React.useEffect(() => {
|
|
762
|
+
if (!editing || !assinaturaDoCatalogo || !container) return;
|
|
763
|
+
const tipos = assinaturaDoCatalogo.split("\u0001").map((linha) => {
|
|
764
|
+
const [tipo, label, descricao, kind] = linha.split("\u0000");
|
|
765
|
+
return { tipo, label, descricao: descricao || undefined, kind: (kind || undefined) as SectionKind | undefined };
|
|
766
|
+
});
|
|
767
|
+
return registerTipos(container, tipos);
|
|
768
|
+
}, [editing, registerTipos, container, assinaturaDoCatalogo]);
|
|
769
|
+
return (
|
|
770
|
+
<EditableContextProvider value={value}>
|
|
771
|
+
{ordered.map((id) => {
|
|
772
|
+
// seção criada: o componente vem do CATÁLOGO, não dos filhos do JSX
|
|
773
|
+
const tipo = tipoDaCriada.get(id);
|
|
774
|
+
const doTipo = tipo ? doCatalogo(tipo) : undefined;
|
|
775
|
+
if (tipo && doTipo) {
|
|
776
|
+
return (
|
|
777
|
+
<Section key={id} id={id} container={container} criada tipo={tipo} kind={doTipo.kind} label={doTipo.label}>
|
|
778
|
+
{doTipo.render()}
|
|
779
|
+
</Section>
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const src = origem.get(id);
|
|
783
|
+
const el = byId.get(src ?? id);
|
|
784
|
+
// um id que não é filho do código, nem cópia, nem criada renderizável não pode DERRUBAR a
|
|
785
|
+
// página (era um `!` não-nulo): fica de fora da tela e o painel resolve pelo documento
|
|
786
|
+
if (!el) return null;
|
|
787
|
+
const label = src ? `${(el.props as { label?: string }).label ?? src} (cópia)` : undefined;
|
|
788
|
+
// `ordemNoCodigo` = posição do filho no CÓDIGO (da origem, no caso de uma cópia): o editor precisa
|
|
789
|
+
// dela para saber a ordem original mesmo lendo um DOM já reordenado (Astra v3.5, achado 1)
|
|
790
|
+
// só com `layout` esta lista MANDA na ordem: a página de produto reusa o container "home" com
|
|
791
|
+
// layout={false} só para reaproveitar componentes, e não pode ditar a ordem da home (Astra v3.6, 1)
|
|
792
|
+
const ordemNoCodigo = layout ? ids.indexOf(src ?? id) : undefined;
|
|
793
|
+
return React.cloneElement(el, { key: id, id, container, ordemNoCodigo, ...(src ? { clone: true, label } : {}) } as Partial<{ id: string; container: string; clone: boolean; label: string; item: boolean; ordemNoCodigo: number }>);
|
|
794
|
+
})}
|
|
795
|
+
</EditableContextProvider>
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Exportações NOMEADAS para server components: o objeto `Editable` é de um módulo
|
|
800
|
+
// "use client", e um server component que importa um OBJETO de client module recebe
|
|
801
|
+
// undefined (só componentes atravessam a fronteira). Nomeados atravessam.
|
|
802
|
+
export { Section as EditableSection, Text as EditableText, EditableImage, EditableImg, EditableLink, Slot as EditableSlot, Icon as EditableIcon, Vitrine as EditableVitrine, Video as EditableVideo, Html as EditableHtml, Sections as EditableSections };
|
|
803
|
+
|
|
804
|
+
export const Editable = {
|
|
805
|
+
Text,
|
|
806
|
+
Image: EditableImage,
|
|
807
|
+
Img: EditableImg,
|
|
808
|
+
Link: EditableLink,
|
|
809
|
+
Slot,
|
|
810
|
+
Icon,
|
|
811
|
+
Vitrine,
|
|
812
|
+
Video,
|
|
813
|
+
Html,
|
|
814
|
+
Section,
|
|
815
|
+
Sections,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// Exports NOMEADOS pros server components (cabeçalho, rodapé): um server
|
|
819
|
+
// component só recebe referências de cliente por nome de export — `Editable.X`
|
|
820
|
+
// chega como undefined lá ("Element type is invalid"). Nos client components
|
|
821
|
+
// o namespace `Editable.*` continua sendo a forma de uso.
|