@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.
Files changed (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. 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.