@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,861 @@
1
+ "use client";
2
+
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ // PROVIDER DO CONTEÚDO EDITÁVEL + PONTE COM O EDITOR
5
+ //
6
+ // Em produção: recebe o documento publicado (lido no servidor, com ISR) e o
7
+ // distribui por contexto aos primitivos. Zero JS a mais além do contexto.
8
+ //
9
+ // Em modo edição (a página aberta com `?unbox_editor=1`, dentro do iframe do
10
+ // editor): os primitivos ganham atributos `data-editor-*`, o provider publica o
11
+ // MANIFESTO (tudo que é editável nesta página, descoberto por quem se registrou)
12
+ // e passa a aceitar `apply {doc}` por postMessage — prévia instantânea, sem
13
+ // round-trip nem rebuild. Nada aqui grava nada: a única fonte de verdade do
14
+ // rascunho é o servidor do editor.
15
+ //
16
+ // O modo edição é cosmético: só acrescenta atributos e escuta mensagens da
17
+ // origem configurada. Sem origem configurada, aceita qualquer pai — o que um
18
+ // pai malicioso consegue é sobrepor texto NA PRÓPRIA PÁGINA DELE, o que ele já
19
+ // conseguia sem nós.
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ import * as React from "react";
22
+ import {
23
+ type ContentDocument,
24
+ type EditableType,
25
+ type EditableValue,
26
+ type Manifest,
27
+ type ManifestApps,
28
+ type ManifestEntry,
29
+ type ManifestSectionType,
30
+ type ManifestSemContainer,
31
+ emptyDocument,
32
+ isColor, normalizarPagina, resolveValue, type SectionKind, SECTION_KIND_LABEL } from "./document";
33
+
34
+ export interface EditableTokenSpec {
35
+ token: string;
36
+ label: string;
37
+ }
38
+
39
+ interface Registration {
40
+ path: string;
41
+ type: EditableType;
42
+ label?: string;
43
+ fallback: EditableValue;
44
+ el: () => Element | null;
45
+ }
46
+
47
+ /**
48
+ * O QUE FICOU FORA DE UM CONTAINER (foundation 11). Não é registro de editável: é o contrário disso.
49
+ * O primitivo que cairia na RAIZ do documento não se registra (a loja renderiza o literal do código),
50
+ * e passa por aqui só para o manifesto poder DECLARAR o defeito e o gate reprovar. Ver
51
+ * `caminhoTemContainer` em document.ts.
52
+ */
53
+ interface RegistroForaDeContainer {
54
+ path: string;
55
+ type: EditableType;
56
+ label?: string;
57
+ }
58
+
59
+ /** Um tipo do catálogo da loja, como a loja o declara (o container vem de quem registra). */
60
+ export type TipoDeSecaoDeclarado = Omit<ManifestSectionType, "container">;
61
+
62
+ interface SectionRegistration {
63
+ container: string;
64
+ id: string;
65
+ label?: string;
66
+ clone?: boolean;
67
+ /** seção ADICIONADA pelo lojista, instanciada a partir do catálogo da loja */
68
+ criada?: boolean;
69
+ /** tipo do catálogo (só em `criada`) */
70
+ tipo?: string;
71
+ fixed?: boolean;
72
+ kind?: SectionKind;
73
+ item?: boolean;
74
+ /** posição no CÓDIGO (não muda com a reordenação do lojista) */
75
+ ordemNoCodigo?: number;
76
+ /** a página reaproveita o container sem mandar nele (`Editable.Sections layout={false}`) */
77
+ semLayout?: boolean;
78
+ el: () => Element | null;
79
+ }
80
+
81
+ interface Ctx {
82
+ doc: ContentDocument;
83
+ editing: boolean;
84
+ selectMode: boolean;
85
+ scope: string[];
86
+ /**
87
+ * O CONTAINER em vigor. `undefined` = nenhum foi declarado ainda, e nada aqui dentro é editável:
88
+ * o padrão era "home", e por causa dele qualquer página que envolvesse suas seções sem dizer o
89
+ * container entrava escrevendo NA HOME e passava a disputar a ordem da home (foundation 11).
90
+ */
91
+ container?: string;
92
+ section?: string;
93
+ /** false = ignora ordem/ocultas do documento (páginas que reaproveitam a receita da home) */
94
+ layout: boolean;
95
+ register: (r: Registration) => () => void;
96
+ registerSection: (r: SectionRegistration) => () => void;
97
+ /** o CATÁLOGO daquele container: os tipos que esta loja sabe instanciar (`Editable.Sections catalogo`) */
98
+ registerTipos: (container: string, tipos: TipoDeSecaoDeclarado[]) => () => void;
99
+ select: (entry: ManifestEntry, el: Element) => void;
100
+ /** o primitivo que cairia na RAIZ do documento se declara aqui, para o manifesto poder acusar */
101
+ foraDeContainer: (r: RegistroForaDeContainer) => () => void;
102
+ /**
103
+ * O token de prévia (o `unbox_editor_token` com que o editor abriu este iframe), guardado só em
104
+ * memória — ele sai da URL na primeira renderização e nunca volta para lá.
105
+ *
106
+ * Serve para UMA coisa: em modo edição, o primitivo da vitrine perguntar à PRÓPRIA LOJA o que a
107
+ * escolha do rascunho vira em produtos (`POST /api/unbox/vitrine`). A credencial da Unbox
108
+ * continua só na loja — o que trafega aqui é o token do editor, que a loja verifica pelo JWKS.
109
+ * `null` fora do modo edição (e enquanto o efeito de captura não rodou).
110
+ */
111
+ previewToken: string | null;
112
+ /**
113
+ * Pede ao editor um token novo. O de prévia vale 15 minutos; uma sessão de edição passa disso
114
+ * com facilidade, e sem isto a primeira vitrine trocada depois do prazo responderia 401 e o
115
+ * lojista veria "não autorizado" sem ter feito nada errado.
116
+ */
117
+ renovarToken: () => void;
118
+ }
119
+
120
+ const noop = () => () => {};
121
+ const EditableContext = React.createContext<Ctx>({
122
+ doc: emptyDocument(""),
123
+ editing: false,
124
+ selectMode: false,
125
+ scope: [],
126
+ container: undefined,
127
+ layout: true,
128
+ register: noop,
129
+ registerSection: noop,
130
+ registerTipos: noop,
131
+ select: () => {},
132
+ foraDeContainer: noop,
133
+ previewToken: null,
134
+ renovarToken: () => {},
135
+ });
136
+
137
+ export function useEditableContext() {
138
+ return React.useContext(EditableContext);
139
+ }
140
+
141
+ /** Lê o flag de edição no cliente, depois da hidratação (evita mismatch). */
142
+ function detectEditing(): boolean {
143
+ if (typeof window === "undefined") return false;
144
+ try {
145
+ return new URLSearchParams(window.location.search).get("unbox_editor") === "1";
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ /** fundo por seção: o invólucro é `display: contents`; a variável herda e o filho direto pinta (e um gradiente do código sai da frente) */
152
+ const SECTION_CSS = `[data-unbox-sec-bg="1"]>*{background-color:var(--unbox-sec-bg) !important;background-image:none !important}`;
153
+
154
+ /** o primeiro texto visível da seção (título, se houver; senão o primeiro texto editável), até 60 caracteres */
155
+ function trechoDaSecao(node: Element | null): string | undefined {
156
+ if (!node) return undefined;
157
+ const cand = node.querySelector("h1[data-editor-path],h2[data-editor-path],h3[data-editor-path],[data-editor-type='text']");
158
+ const txt = (cand as HTMLElement | null)?.innerText?.replace(/\s+/g, " ").trim();
159
+ if (!txt) return undefined;
160
+ return txt.length > 60 ? txt.slice(0, 59) + "…" : txt;
161
+ }
162
+
163
+ /** rgb()/rgba() → [r,g,b,a]; #rrggbb → alfa 1; transparente → alfa 0 */
164
+ function rgba(cor: string | null | undefined): [number, number, number, number] | undefined {
165
+ const m = cor?.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+%?))?\s*\)/);
166
+ if (m) {
167
+ const a = m[4] === undefined ? 1 : m[4].endsWith("%") ? Number(m[4].slice(0, -1)) / 100 : Number(m[4]);
168
+ return [Number(m[1]), Number(m[2]), Number(m[3]), Number.isFinite(a) ? a : 1];
169
+ }
170
+ const h = cor?.match(/^#([0-9a-f]{6})$/i);
171
+ return h ? [parseInt(h[1].slice(0, 2), 16), parseInt(h[1].slice(2, 4), 16), parseInt(h[1].slice(4, 6), 16), 1] : undefined;
172
+ }
173
+ const hex = (c: [number, number, number, number]) => "#" + c.slice(0, 3).map((n) => Math.round(n).toString(16).padStart(2, "0")).join("");
174
+ /** cor computada → #rrggbb (o inspector usa <input type=color>); transparente → undefined */
175
+ function paraHex(cor: string | null | undefined): string | undefined {
176
+ const c = rgba(cor);
177
+ return c && c[3] > 0 ? hex(c) : undefined;
178
+ }
179
+ /**
180
+ * A cor de fundo que o olho vê: sobe pelos ancestrais compondo as camadas semitransparentes
181
+ * (rgba(255,0,0,.5) sobre branco é rosa, não vermelho) até fechar em uma cor opaca.
182
+ */
183
+ function fundoEmUso(el: Element | null): string | undefined {
184
+ const camadas: [number, number, number, number][] = [];
185
+ for (let e: Element | null = el; e; e = e.parentElement) {
186
+ const cs = getComputedStyle(e);
187
+ // imagem ou gradiente de fundo nesta camada: o que se vê não é UMA cor — "não representável"
188
+ if (cs.backgroundImage && cs.backgroundImage !== "none") return undefined;
189
+ const bruto = cs.backgroundColor;
190
+ const c = rgba(bruto);
191
+ // formato que não sabemos ler (oklch, color(), …) é uma camada OPACA desconhecida: não é
192
+ // transparente, então não atravessa — "não representável" em vez da cor do ancestral
193
+ if (!c && bruto && bruto !== "transparent") return undefined;
194
+ if (!c || c[3] === 0) continue;
195
+ camadas.push(c);
196
+ if (c[3] >= 1) break;
197
+ }
198
+ if (!camadas.length) return undefined;
199
+ // compõe de trás (mais opaca/mais funda) para a frente
200
+ let acc = camadas[camadas.length - 1];
201
+ if (acc[3] < 1) acc = [acc[0] * acc[3] + 255 * (1 - acc[3]), acc[1] * acc[3] + 255 * (1 - acc[3]), acc[2] * acc[3] + 255 * (1 - acc[3]), 1]; // fundo da página branco como base
202
+ for (let i = camadas.length - 2; i >= 0; i--) {
203
+ const c = camadas[i];
204
+ acc = [c[0] * c[3] + acc[0] * (1 - c[3]), c[1] * c[3] + acc[1] * (1 - c[3]), c[2] * c[3] + acc[2] * (1 - c[3]), 1];
205
+ }
206
+ return hex(acc);
207
+ }
208
+
209
+ /**
210
+ * DESTAQUE POR CAMADA (bloco B7, lição do editor da Shopify): em vez de `outline` em cada um dos
211
+ * ~150 editáveis (a loja parecia quebrada e o contorno sumia dentro de `overflow:hidden`),
212
+ * uma camada fixa por cima da página desenha três caixas — hover, selecionado e seção ativa —
213
+ * medidas por getBoundingClientRect, com um chip de nome. O pontilhado permanente virou opção
214
+ * ("mostrar o que dá para editar"), desligada por padrão.
215
+ *
216
+ * Esse pontilhado nascia INVISÍVEL, por dois motivos somados: era o mesmo azul da caixa de hover a 45%
217
+ * (a camada de destaque o engolia) e ficava FORA da borda (`outline-offset:2px`), onde qualquer ancestral
218
+ * com `overflow:hidden` o recorta — e num layout de loja quase toda seção tem um. Agora o traço é desenhado
219
+ * DENTRO da caixa (`outline-offset:-1px`): assim ele só some junto com o próprio elemento, que é honesto,
220
+ * e nunca por causa do recorte do vizinho. A cor é `currentColor` — a cor de texto que o designer já
221
+ * garantiu legível naquele fundo, clara no escuro e escura no claro, e que por construção não é o azul do
222
+ * hover. Redesenhar os ~150 contornos na camada `#unbox-editor-overlay` daria o mesmo resultado, mas
223
+ * custaria medir 150 retângulos a cada quadro de rolagem e só valeria com a camada montada — ela sai do ar
224
+ * no modo Navegar, e o pontilhado tem de continuar.
225
+ */
226
+ const EDITING_CSS = `
227
+ [data-editor-path]{cursor:pointer !important}
228
+ [data-editor-mostrar-editaveis="1"] [data-editor-path]{outline:1px dashed currentColor;outline-offset:-1px}
229
+ #unbox-editor-overlay{position:fixed;inset:0;pointer-events:none;z-index:2147483000}
230
+ #unbox-editor-overlay .ux-box{position:absolute;border-radius:3px;box-sizing:border-box;transition:all .08s ease-out}
231
+ #unbox-editor-overlay .ux-hover{border:2px solid rgba(37,99,235,.9)}
232
+ #unbox-editor-overlay .ux-sel{border:2px solid #f59e0b;box-shadow:0 0 0 4px rgba(245,158,11,.22)}
233
+ #unbox-editor-overlay .ux-sec{border:1.5px dashed rgba(27,26,33,.55);border-radius:6px}
234
+ #unbox-editor-overlay .ux-chip{position:absolute;transform:translateY(-100%);margin-top:-4px;left:0;max-width:60vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:600 11px/1.2 system-ui,-apple-system,Segoe UI,sans-serif;padding:3px 7px;border-radius:5px;color:#fff;background:#1b1a21;letter-spacing:.01em}
235
+ #unbox-editor-overlay .ux-hover .ux-chip{background:#2563eb}
236
+ #unbox-editor-overlay .ux-sel .ux-chip{background:#b45309}
237
+ #unbox-editor-overlay .ux-sec .ux-chip{background:#1b1a21;opacity:.85}
238
+ [data-editor-editing="1"]{outline:2px solid #1b1a21 !important;outline-offset:2px;box-shadow:0 0 0 4px rgba(27,26,33,.18);cursor:text !important;caret-color:#1b1a21}
239
+ [data-editor-section-hidden="1"]{display:none !important}
240
+ `;
241
+
242
+ interface OverlayHandle {
243
+ marcarSelecionado(el: HTMLElement | null, entry?: ManifestEntry): void;
244
+ marcarSecao(el: HTMLElement | null): void;
245
+ }
246
+ // o nome do que está sob o cursor, na palavra do lojista. Tipo sem entrada aqui deixa o chip só com o
247
+ // rótulo — o tipo CRU do documento ("vitrine", "html") é nome nosso e não vai para a tela dele
248
+ const TIPO_NOME: Record<string, string> = { text: "Texto", image: "Imagem", link: "Link", color: "Cor", vitrine: "Vitrine", video: "Vídeo", html: "Bloco de HTML" };
249
+ /** a camada de destaque: três caixas fixas por cima da loja, medidas a cada scroll/resize/apply */
250
+ const Overlay = React.forwardRef<OverlayHandle, object>(function Overlay(_props, ref) {
251
+ const raiz = React.useRef<HTMLDivElement | null>(null);
252
+ const alvos = React.useRef<{ hover: HTMLElement | null; sel: HTMLElement | null; selRotulo: string; sec: HTMLElement | null }>({ hover: null, sel: null, selRotulo: "", sec: null });
253
+ const pinta = React.useCallback(() => {
254
+ const r = raiz.current;
255
+ if (!r) return;
256
+ const caixa = (cls: string, el: HTMLElement | null, rotulo: string) => {
257
+ const box = r.querySelector(`.${cls}`) as HTMLElement | null;
258
+ if (!box) return;
259
+ if (!el || !el.isConnected) {
260
+ box.style.display = "none";
261
+ return;
262
+ }
263
+ const b = el.getBoundingClientRect();
264
+ if (b.width === 0 && b.height === 0) {
265
+ box.style.display = "none";
266
+ return;
267
+ }
268
+ box.style.display = "block";
269
+ box.style.left = `${b.left - 2}px`;
270
+ box.style.top = `${b.top - 2}px`;
271
+ box.style.width = `${b.width + 4}px`;
272
+ box.style.height = `${b.height + 4}px`;
273
+ const chip = box.querySelector(".ux-chip") as HTMLElement | null;
274
+ if (chip) {
275
+ if (chip.textContent !== rotulo) chip.textContent = rotulo;
276
+ chip.style.display = rotulo ? "block" : "none";
277
+ // chip cabe acima; se a caixa encosta no topo, o chip vai para dentro
278
+ chip.style.transform = b.top < 28 ? "translateY(0)" : "translateY(-100%)";
279
+ chip.style.marginTop = b.top < 28 ? "2px" : "-4px";
280
+ }
281
+ };
282
+ const a = alvos.current;
283
+ // Sem `label`, o chip fica só com o tipo ("Texto", "Perguntas frequentes"). O caminho
284
+ // (`home.faq.pergunta-1.pergunta`) e o id da seção (`hero`, `marquee-2`) são endereço do NOSSO
285
+ // documento: o lojista não os escreveu, não os reconhece e não tem como mudá-los. Falta de rótulo
286
+ // é defeito de quem construiu a loja (o gate reprova por isso) — não é recado para o dono dela.
287
+ const rotuloDe = (el: HTMLElement | null) => {
288
+ if (!el) return "";
289
+ const tipo = TIPO_NOME[el.dataset.editorType ?? ""] ?? "";
290
+ return [tipo, el.dataset.editorLabel ?? ""].filter(Boolean).join(" · ");
291
+ };
292
+ const rotuloSec = (el: HTMLElement | null) => {
293
+ if (!el) return "";
294
+ const kind = el.dataset.editorKind;
295
+ const tipo = (kind && SECTION_KIND_LABEL[kind as keyof typeof SECTION_KIND_LABEL]) || "";
296
+ return [tipo, el.dataset.editorLabel ?? ""].filter(Boolean).join(" · ");
297
+ };
298
+ caixa("ux-hover", a.hover && a.hover !== a.sel ? a.hover : null, rotuloDe(a.hover));
299
+ caixa("ux-sel", a.sel, a.selRotulo || rotuloDe(a.sel));
300
+ // a caixa da seção é o primeiro filho do invólucro display:contents
301
+ caixa("ux-sec", (a.sec?.firstElementChild as HTMLElement | null) ?? null, rotuloSec(a.sec));
302
+ }, []);
303
+ React.useImperativeHandle(ref, () => ({
304
+ marcarSelecionado(el, entry) {
305
+ alvos.current.sel = el;
306
+ alvos.current.selRotulo = entry ? [TIPO_NOME[entry.type] ?? "", entry.label ?? ""].filter(Boolean).join(" · ") : "";
307
+ alvos.current.sec = (el?.closest("[data-editor-section]") as HTMLElement | null) ?? alvos.current.sec;
308
+ pinta();
309
+ },
310
+ marcarSecao(el) {
311
+ alvos.current.sec = el;
312
+ alvos.current.sel = null;
313
+ pinta();
314
+ },
315
+ }), [pinta]);
316
+ React.useEffect(() => {
317
+ let raf = 0;
318
+ const agenda = () => {
319
+ cancelAnimationFrame(raf);
320
+ raf = requestAnimationFrame(pinta);
321
+ };
322
+ const onMove = (ev: MouseEvent) => {
323
+ const el = (ev.target as Element | null)?.closest("[data-editor-path]") as HTMLElement | null;
324
+ if (el !== alvos.current.hover) {
325
+ alvos.current.hover = el;
326
+ agenda();
327
+ }
328
+ };
329
+ const onLeave = () => {
330
+ alvos.current.hover = null;
331
+ agenda();
332
+ };
333
+ document.addEventListener("mousemove", onMove, true);
334
+ document.addEventListener("mouseleave", onLeave, true);
335
+ window.addEventListener("scroll", agenda, true);
336
+ window.addEventListener("resize", agenda);
337
+ // as mutações do PRÓPRIO overlay (a pintura mexe em style/textContent) não agendam outra pintura —
338
+ // senão pintar → mutação → pintar em ciclo (revisão v3 do Astra, achado 2)
339
+ const mo = new MutationObserver((recs) => {
340
+ const proprio = raiz.current;
341
+ if (proprio && recs.every((r) => proprio.contains(r.target))) return;
342
+ agenda();
343
+ });
344
+ mo.observe(document.body, { subtree: true, childList: true, attributes: true, characterData: true });
345
+ const t = setInterval(pinta, 500); // rede de segurança: fontes/imagens que mudam a medida sem evento
346
+ return () => {
347
+ document.removeEventListener("mousemove", onMove, true);
348
+ document.removeEventListener("mouseleave", onLeave, true);
349
+ window.removeEventListener("scroll", agenda, true);
350
+ window.removeEventListener("resize", agenda);
351
+ mo.disconnect();
352
+ clearInterval(t);
353
+ cancelAnimationFrame(raf);
354
+ };
355
+ }, [pinta]);
356
+ return (
357
+ <div id="unbox-editor-overlay" ref={raiz} aria-hidden>
358
+ <div className="ux-box ux-sec" style={{ display: "none" }}><span className="ux-chip" /></div>
359
+ <div className="ux-box ux-hover" style={{ display: "none" }}><span className="ux-chip" /></div>
360
+ <div className="ux-box ux-sel" style={{ display: "none" }}><span className="ux-chip" /></div>
361
+ </div>
362
+ );
363
+ });
364
+
365
+ export function EditableProvider({
366
+ doc: initialDoc,
367
+ shop,
368
+ tokens = [],
369
+ editorOrigin,
370
+ apps,
371
+ children,
372
+ }: {
373
+ doc: ContentDocument | null;
374
+ shop: string;
375
+ tokens?: EditableTokenSpec[];
376
+ /** Origem do editor (ex.: https://editor.unbox.com.br). Se definida, só ela pode aplicar rascunho. */
377
+ editorOrigin?: string;
378
+ /**
379
+ * APPS (foundation 12): o que a loja tem no AMBIENTE para rastreio, só presença, e o ESTADO da
380
+ * Conversions API do Meta (`EstadoDoCapi`: ativa, sem token, sem Pixel, ou Pixel diferente do token), e o
381
+ * contêiner contratual da Unbox (`unboxGtmId`, público). Vem de
382
+ * `presencaNoAmbiente(process.env, { unboxGtmId: UNBOX_GTM_ID })` (lib/editable/server.ts), calculado no
383
+ * servidor pelo app/layout.tsx com o documento publicado: este componente é de cliente e não enxerga o
384
+ * ambiente. Vai direto no manifesto, para o painel dizer de onde vem cada valor em vigor e o que a CAPI
385
+ * está fazendo, sem nunca ver o valor do ambiente.
386
+ */
387
+ apps?: ManifestApps;
388
+ children: React.ReactNode;
389
+ }) {
390
+ const [doc, setDoc] = React.useState<ContentDocument>(initialDoc ?? emptyDocument(shop));
391
+ const [editing, setEditing] = React.useState(false);
392
+ const [selectMode, setSelectMode] = React.useState(true);
393
+ // token de prévia: sai da URL e vive só aqui (ver `previewToken` no contexto)
394
+ const [previewToken, setPreviewToken] = React.useState<string | null>(null);
395
+ const registry = React.useRef(new Map<string, Registration>());
396
+ // um caminho pode ter VÁRIAS instâncias montadas (ícone absoluto compartilhado pelos itens de uma lista):
397
+ // o registro só some quando a última desmonta; até lá outra instância viva responde (Astra v3, achado 9)
398
+ const instancias = React.useRef(new Map<string, Set<Registration>>());
399
+ const sections = React.useRef(new Map<string, SectionRegistration>());
400
+ // editáveis que cairiam na RAIZ do documento: NÃO são editáveis, e o manifesto os declara para o gate
401
+ const semContainer = React.useRef(new Map<string, RegistroForaDeContainer>());
402
+ // catálogo por container: o que o lojista pode ADICIONAR ali. É da LOJA, nunca do editor.
403
+ const tiposPorContainer = React.useRef(new Map<string, TipoDeSecaoDeclarado[]>());
404
+ const selectedEl = React.useRef<Element | null>(null);
405
+ const overlayRef = React.useRef<OverlayHandle | null>(null);
406
+ const docRef = React.useRef(doc);
407
+ docRef.current = doc;
408
+
409
+ // publicado mudou (revalidação ISR + navegação) → segue o servidor, fora do modo edição
410
+ React.useEffect(() => {
411
+ if (!editing && initialDoc) setDoc(initialDoc);
412
+ }, [initialDoc, editing]);
413
+
414
+ React.useEffect(() => {
415
+ // Só dentro de um iframe e só com a origem do editor configurada: sem isso, um
416
+ // site qualquer abriria a loja num popup com ?unbox_editor=1 e mandaria `apply`
417
+ // (achado 6 da revisão adversarial).
418
+ const dentroDeFrame = typeof window !== "undefined" && window.parent !== window;
419
+ setEditing(detectEditing() && Boolean(editorOrigin) && dentroDeFrame);
420
+ // o token de prévia não fica na URL: GA/Meta mandam document.location inteiro (achado 7).
421
+ // Ele é GUARDADO EM MEMÓRIA antes de sair dali — é com ele que a vitrine pergunta à loja o que
422
+ // a escolha do rascunho vira em produtos. Apagar sem guardar deixava a prévia sem como perguntar.
423
+ try {
424
+ const u = new URL(window.location.href);
425
+ const t = u.searchParams.get("unbox_editor_token");
426
+ if (t) {
427
+ setPreviewToken(t);
428
+ u.searchParams.delete("unbox_editor_token");
429
+ window.history.replaceState(null, "", u.toString());
430
+ }
431
+ } catch {}
432
+ }, [editorOrigin]);
433
+
434
+ const post = React.useCallback(
435
+ (msg: Record<string, unknown>) => {
436
+ if (typeof window === "undefined" || window.parent === window || !editorOrigin) return;
437
+ window.parent.postMessage({ source: "unbox-loja", shop, ...msg }, editorOrigin);
438
+ },
439
+ [shop, editorOrigin],
440
+ );
441
+
442
+ // Token vencido: o editor emite outro e devolve por `unbox-editor:token`. Só um pedido de cada
443
+ // vez em voo — uma página com seis vitrines não pode disparar seis pedidos pelo mesmo motivo.
444
+ const pedindoToken = React.useRef(false);
445
+ const renovarToken = React.useCallback(() => {
446
+ if (pedindoToken.current) return;
447
+ pedindoToken.current = true;
448
+ post({ type: "unbox-editor:token-expirado" });
449
+ // se o editor não responder (rede caída, sessão dele expirada), a trava cai sozinha: sem isto
450
+ // UMA falha deixaria a prévia sem nunca mais tentar renovar, até recarregar a página
451
+ setTimeout(() => {
452
+ pedindoToken.current = false;
453
+ }, 15_000);
454
+ }, [post]);
455
+
456
+ const buildManifest = React.useCallback((): Manifest => {
457
+ const d = docRef.current;
458
+ // a PÁGINA vai em cada linha, não só no topo: o editor funde manifestos de páginas diferentes, e
459
+ // sem isto a origem de cada linha some na fusão (foundation 11)
460
+ const pagina = normalizarPagina(typeof window !== "undefined" ? window.location.pathname : "/");
461
+ const entries: ManifestEntry[] = [];
462
+ for (const r of registry.current.values()) {
463
+ const el = r.el();
464
+ const sec = el?.closest("[data-editor-section]") as HTMLElement | null;
465
+ entries.push({
466
+ path: r.path,
467
+ type: r.type,
468
+ label: r.label,
469
+ section: sec?.dataset.editorSection,
470
+ container: sec?.dataset.editorContainer,
471
+ pagina,
472
+ fallback: r.fallback,
473
+ current: d.values[r.path],
474
+ });
475
+ }
476
+ // ordem visual = ordem no DOM
477
+ const pos = (a: Element | null, b: Element | null) => {
478
+ if (!a || !b) return 0;
479
+ const c = a.compareDocumentPosition(b);
480
+ return c & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : c & Node.DOCUMENT_POSITION_PRECEDING ? 1 : 0;
481
+ };
482
+ const elOf = new Map(entries.map((e) => [e.path, registry.current.get(e.path)?.el() ?? null]));
483
+ entries.sort((a, b) => pos(elOf.get(a.path) ?? null, elOf.get(b.path) ?? null));
484
+ // caixa visível agora? (resposta fechada do FAQ, slide oculto, gaveta): a ficha da seção precisa saber
485
+ for (const e of entries) {
486
+ const el = elOf.get(e.path);
487
+ const r = el?.getBoundingClientRect();
488
+ e.visible = Boolean(r && (r.width > 0 || r.height > 0));
489
+ }
490
+ const secs = [...sections.current.values()]
491
+ .map((s) => ({ ...s, node: s.el() }))
492
+ .sort((a, b) => pos(a.node, b.node))
493
+ .map((s) => ({
494
+ container: s.container,
495
+ id: s.id,
496
+ label: s.label,
497
+ pagina,
498
+ semLayout: s.semLayout || undefined,
499
+ hidden: (d.sections[s.container]?.hidden ?? []).includes(s.id),
500
+ fixed: s.fixed || undefined,
501
+ clone: s.clone || undefined,
502
+ criada: s.criada || undefined,
503
+ tipo: s.tipo,
504
+ kind: s.kind,
505
+ item: s.item || undefined,
506
+ ordemNoCodigo: s.ordemNoCodigo,
507
+ // a linha do painel fala o conteúdo: o primeiro texto editável que a seção mostra
508
+ excerpt: trechoDaSecao(s.node),
509
+ background: fundoEmUso(s.node?.firstElementChild ?? null),
510
+ }));
511
+ const cs = typeof window !== "undefined" ? getComputedStyle(document.documentElement) : null;
512
+ // `original` = a cor do código, lida com a folha do rascunho desligada: é o que um "reset" devolve
513
+ const folha = typeof document !== "undefined" ? (document.querySelector("style[data-editor-tokens]") as HTMLStyleElement | null) : null;
514
+ let originais: Record<string, string | undefined> = {};
515
+ if (cs) {
516
+ if (folha) folha.disabled = true;
517
+ originais = Object.fromEntries(tokens.map((t) => [t.token, cs.getPropertyValue(t.token).trim() || undefined]));
518
+ if (folha) folha.disabled = false;
519
+ }
520
+ const toks = tokens.map((t) => ({ ...t, current: cs?.getPropertyValue(t.token).trim() || undefined, original: originais[t.token] }));
521
+ // os tipos que esta loja declara saber instanciar, achatados com o container de cada catálogo
522
+ const tipos: ManifestSectionType[] = [...tiposPorContainer.current.entries()].flatMap(([container, lista]) => lista.map((t) => ({ container, ...t })));
523
+ // a versão diz ao editor o que esta loja sabe renderizar (cópias, inline, estilo por elemento)
524
+ // 3 = fundo por seção e cores computadas na seleção (2 = inline, estilo por elemento, cópias, Icon)
525
+ // 7 = tipo "video" no documento (Editable.Video): abaixo disso o painel não oferece troca de vídeo,
526
+ // porque a loja não tem caminho de tipo video e a operação só produziria erro de validação
527
+ // 8 = seções ADICIONADAS (`add_section` + `Editable.Sections catalogo`): uma loja abaixo de 8 não
528
+ // renderiza `sections[container].criadas`, então o editor não pode oferecer o "+" ali — a seção
529
+ // entraria no documento e simplesmente não apareceria na tela
530
+ // 9 = BLOCO DE HTML (`Editable.Html`): tipo "html" no documento e sufixo `.html` no caminho. Abaixo
531
+ // de 9 a loja não tem nem o primitivo nem a lista de recusa — o painel não pode oferecer o campo
532
+ // de colar HTML, porque o valor entraria no documento e a loja renderizaria só o do código
533
+ // 10 = VITRINE RESOLVIDA NA PRÉVIA: a loja tem `POST /api/unbox/vitrine` e o primitivo pergunta a
534
+ // ela o que a escolha do RASCUNHO vira em produtos. Abaixo de 10 a prévia só muda depois de
535
+ // publicar — por isso o editor recarrega o iframe ao aplicar uma vitrine nessas lojas, e não
536
+ // nesta: aqui a troca aparece na hora
537
+ // 11 = cada linha do manifesto diz de que PÁGINA veio (`pagina`), a lista de seções diz quando a
538
+ // página só REAPROVEITA o container (`semLayout`), e a RAIZ do documento deixou de ser
539
+ // gravável: editável fora de container não se registra, e o manifesto o declara em
540
+ // `semContainer`. Abaixo de 11 o editor não pode confiar na página de cada linha ao fundir
541
+ // manifestos de páginas diferentes, porque ela simplesmente não vem.
542
+ // 12 = APPS: RASTREIO E MARKETING. A loja lê `doc.apps.rastreio` (`rastreioEmVigor`, pelo `<Rastreio>`
543
+ // de rastreio.tsx que o app/layout.tsx chama) e o manifesto diz o que ela tem no ambiente
544
+ // (`apps.rastreio`, só presença).
545
+ // Abaixo de 12 a loja não lê `apps`: o valor entraria no documento e nenhum script mudaria na
546
+ // página, e é por isso que `validateOp` recusa `set_app` contra um manifesto sem esta versão.
547
+ const fora: ManifestSemContainer[] = [...semContainer.current.values()].map((r) => ({ ...r, pagina }));
548
+ return { shop, capturedAt: new Date().toISOString(), url: pagina, foundation: 12, entries, sections: secs, tipos, semContainer: fora, tokens: toks, ...(apps ? { apps } : {}) };
549
+ }, [shop, tokens, apps]);
550
+
551
+ // manifesto: publica depois que os registros assentam (debounce)
552
+ const manifestTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
553
+ const scheduleManifest = React.useCallback(() => {
554
+ if (!editing) return;
555
+ if (manifestTimer.current) clearTimeout(manifestTimer.current);
556
+ manifestTimer.current = setTimeout(() => post({ type: "unbox-editor:manifest", manifest: buildManifest() }), 250);
557
+ }, [editing, post, buildManifest]);
558
+
559
+ const register = React.useCallback(
560
+ (r: Registration) => {
561
+ const conjunto = instancias.current.get(r.path) ?? new Set<Registration>();
562
+ conjunto.add(r);
563
+ instancias.current.set(r.path, conjunto);
564
+ registry.current.set(r.path, r);
565
+ scheduleManifest();
566
+ return () => {
567
+ conjunto.delete(r);
568
+ if (conjunto.size === 0) {
569
+ registry.current.delete(r.path);
570
+ instancias.current.delete(r.path);
571
+ } else if (registry.current.get(r.path) === r) {
572
+ registry.current.set(r.path, [...conjunto].at(-1)!);
573
+ }
574
+ scheduleManifest();
575
+ };
576
+ },
577
+ [scheduleManifest],
578
+ );
579
+ // a MESMA seção pode estar montada duas vezes (a faixa de anúncio no cabeçalho desktop e no mobile):
580
+ // o registro só some quando a última instância desmonta, senão a lista da página fica incompleta e o
581
+ // editor passa a achar que a seção não existe (verificação v3.7 do Astra, achado 2)
582
+ const instanciasDeSecao = React.useRef(new Map<string, Set<SectionRegistration>>());
583
+ const registerSection = React.useCallback(
584
+ (r: SectionRegistration) => {
585
+ const key = `${r.container}/${r.id}`;
586
+ const conjunto = instanciasDeSecao.current.get(key) ?? new Set<SectionRegistration>();
587
+ conjunto.add(r);
588
+ instanciasDeSecao.current.set(key, conjunto);
589
+ sections.current.set(key, r);
590
+ scheduleManifest();
591
+ return () => {
592
+ conjunto.delete(r);
593
+ if (conjunto.size === 0) {
594
+ sections.current.delete(key);
595
+ instanciasDeSecao.current.delete(key);
596
+ } else if (sections.current.get(key) === r) {
597
+ sections.current.set(key, [...conjunto].at(-1)!);
598
+ }
599
+ scheduleManifest();
600
+ };
601
+ },
602
+ [scheduleManifest],
603
+ );
604
+
605
+ // o catálogo do container é declarado uma vez por `Editable.Sections`; o último a registrar manda
606
+ // (a loja tem um catálogo por container, não vários), e desmontar só apaga o que ainda é o dele
607
+ const registerTipos = React.useCallback(
608
+ (container: string, tipos: TipoDeSecaoDeclarado[]) => {
609
+ tiposPorContainer.current.set(container, tipos);
610
+ scheduleManifest();
611
+ return () => {
612
+ if (tiposPorContainer.current.get(container) === tipos) tiposPorContainer.current.delete(container);
613
+ scheduleManifest();
614
+ };
615
+ },
616
+ [scheduleManifest],
617
+ );
618
+
619
+ // O DEFEITO SE DECLARA (foundation 11). Um mesmo caminho pode aparecer em várias instâncias soltas
620
+ // (dez itens da mesma faixa): a chave é o caminho, e a última a desmontar limpa.
621
+ const instanciasForaDeContainer = React.useRef(new Map<string, number>());
622
+ const foraDeContainer = React.useCallback((r: RegistroForaDeContainer) => {
623
+ const n = instanciasForaDeContainer.current.get(r.path) ?? 0;
624
+ instanciasForaDeContainer.current.set(r.path, n + 1);
625
+ semContainer.current.set(r.path, r);
626
+ scheduleManifest();
627
+ return () => {
628
+ const atual = (instanciasForaDeContainer.current.get(r.path) ?? 1) - 1;
629
+ if (atual <= 0) {
630
+ instanciasForaDeContainer.current.delete(r.path);
631
+ semContainer.current.delete(r.path);
632
+ } else instanciasForaDeContainer.current.set(r.path, atual);
633
+ scheduleManifest();
634
+ };
635
+ }, [scheduleManifest]);
636
+
637
+ const select = React.useCallback(
638
+ (entry: ManifestEntry, el: Element) => {
639
+ if (selectedEl.current) selectedEl.current.removeAttribute("data-editor-selected");
640
+ selectedEl.current = el;
641
+ el.setAttribute("data-editor-selected", "1");
642
+ overlayRef.current?.marcarSelecionado(el as HTMLElement, entry);
643
+ const r = el.getBoundingClientRect();
644
+ const sec = el.closest("[data-editor-section]") as HTMLElement | null;
645
+ const cs = getComputedStyle(el);
646
+ const fundo = fundoEmUso(el);
647
+ // texto semitransparente é composto sobre o fundo em uso (rgba(255,0,0,.5) sobre branco = rosa)
648
+ const cor = rgba(cs.color);
649
+ const corHex = cor ? (cor[3] >= 1 ? hex(cor) : fundo && rgba(fundo) ? hex([0, 1, 2].map((i) => cor[i] * cor[3] + rgba(fundo)![i] * (1 - cor[3])).concat([1]) as [number, number, number, number]) : undefined) : undefined;
650
+ post({
651
+ type: "unbox-editor:select",
652
+ entry: { ...entry, section: sec?.dataset.editorSection, container: sec?.dataset.editorContainer, current: docRef.current.values[entry.path], computed: { color: corHex, background: fundo } },
653
+ rect: { x: r.x, y: r.y, w: r.width, h: r.height },
654
+ });
655
+ },
656
+ [post],
657
+ );
658
+
659
+ // ponte: mensagens do editor
660
+ React.useEffect(() => {
661
+ if (!editing) return;
662
+ const onMessage = (ev: MessageEvent) => {
663
+ if (editorOrigin && ev.origin !== editorOrigin) return;
664
+ const m = ev.data as { source?: string; type?: string; doc?: ContentDocument; on?: boolean; path?: string; section?: string; container?: string; token?: string } | null;
665
+ if (!m || m.source !== "unbox-editor") return;
666
+ switch (m.type) {
667
+ case "unbox-editor:apply":
668
+ if (m.doc && m.doc.schema === 1) setDoc(m.doc);
669
+ break;
670
+ case "unbox-editor:token":
671
+ // token de prévia novo (resposta a `renovarToken`). Trocá-lo faz a vitrine que falhou
672
+ // por 401 tentar de novo sozinha — o efeito dela depende deste valor.
673
+ pedindoToken.current = false;
674
+ if (typeof m.token === "string" && m.token) setPreviewToken(m.token);
675
+ break;
676
+ case "unbox-editor:select-mode":
677
+ setSelectMode(Boolean(m.on));
678
+ break;
679
+ case "unbox-editor:show-editables":
680
+ document.documentElement.setAttribute("data-editor-mostrar-editaveis", (m as { on?: boolean }).on ? "1" : "0");
681
+ break;
682
+ case "unbox-editor:inline-result": {
683
+ // o editor recusou (ou o lojista cancelou a declaração): o texto digitado sai do DOM.
684
+ // Cada edição tem o próprio id: duas confirmações em trânsito são resolvidas cada uma
685
+ // pela sua resposta (um editor antigo, sem id, é atendido pelo caminho)
686
+ const r = m as { path?: string; ok?: boolean; inlineId?: number };
687
+ const pend = inlinePendentes.current;
688
+ const id = typeof r.inlineId === "number" ? r.inlineId : [...pend.keys()].find((k) => pend.get(k)?.path === r.path);
689
+ const u = id != null ? pend.get(id) : undefined;
690
+ if (u) {
691
+ // restaura a partir do DOCUMENTO (a verdade), não do texto capturado no clique: uma edição
692
+ // mais nova no mesmo caminho pode já ter sido aplicada
693
+ if (r.ok === false) {
694
+ const reg = registry.current.get(u.path);
695
+ const atual = reg ? resolveValue(docRef.current, u.path, reg.fallback) : undefined;
696
+ u.el.innerText = typeof atual === "string" ? atual : u.original;
697
+ }
698
+ pend.delete(id!);
699
+ }
700
+ break;
701
+ }
702
+ case "unbox-editor:ping":
703
+ post({ type: "unbox-editor:manifest", manifest: buildManifest() });
704
+ break;
705
+ case "unbox-editor:scroll-to": {
706
+ // container + id: "uso-1" existe em duas colunas; sem container, a primeira ocorrência (como antes)
707
+ const seletorSecao = m.section ? `${m.container ? `[data-editor-container="${CSS.escape(m.container)}"]` : ""}[data-editor-section="${CSS.escape(m.section)}"]` : "";
708
+ const target = m.path
709
+ ? document.querySelector(`[data-editor-path="${CSS.escape(m.path)}"]`)
710
+ : seletorSecao
711
+ ? document.querySelector(seletorSecao)?.firstElementChild
712
+ : null;
713
+ for (let d = target?.closest("details"); d; d = d.parentElement?.closest("details") ?? null) d.open = true;
714
+ if (!m.path && seletorSecao) overlayRef.current?.marcarSecao(document.querySelector(seletorSecao) as HTMLElement | null);
715
+ // aba oculta não anima (sem quadros): rola direto, senão a prévia nunca chega lá
716
+ target?.scrollIntoView({ behavior: document.visibilityState === "hidden" ? "auto" : "smooth", block: "center" });
717
+ if (target && m.path) {
718
+ const r = registry.current.get(m.path);
719
+ if (r) select({ path: r.path, type: r.type, label: r.label, fallback: r.fallback }, target);
720
+ }
721
+ break;
722
+ }
723
+ }
724
+ };
725
+ window.addEventListener("message", onMessage);
726
+ post({ type: "unbox-editor:ready", url: window.location.href });
727
+ scheduleManifest();
728
+ return () => window.removeEventListener("message", onMessage);
729
+ }, [editing, editorOrigin, post, buildManifest, scheduleManifest, select]);
730
+
731
+ // em modo seleção, clique escolhe o elemento em vez de navegar; texto vira
732
+ // editável ali mesmo (contentEditable): Enter confirma, Esc cancela, Shift+Enter
733
+ // quebra linha. O valor só sai daqui ao confirmar — quem grava é o editor.
734
+ const editandoRef = React.useRef<{ el: HTMLElement; path: string; original: string } | null>(null);
735
+ // edições inline em trânsito (por id), para restaurar o texto se o editor recusar (honestidade cancelada, erro)
736
+ const inlinePendentes = React.useRef(new Map<number, { el: HTMLElement; path: string; original: string }>());
737
+ const inlineSeq = React.useRef(0);
738
+ const encerrarInline = React.useCallback(
739
+ (confirmar: boolean) => {
740
+ const e = editandoRef.current;
741
+ if (!e) return;
742
+ editandoRef.current = null;
743
+ const novo = e.el.innerText.replace(/\u00a0/g, " ").replace(/\n{3,}/g, "\n\n").trim();
744
+ e.el.removeAttribute("contenteditable");
745
+ e.el.removeAttribute("data-editor-editing");
746
+ if (confirmar && novo && novo !== e.original) {
747
+ const inlineId = ++inlineSeq.current;
748
+ inlinePendentes.current.set(inlineId, e);
749
+ post({ type: "unbox-editor:inline", path: e.path, value: novo, inlineId });
750
+ } else e.el.innerText = e.original; // volta ao que estava (o React reconcilia no próximo apply)
751
+ },
752
+ [post],
753
+ );
754
+ React.useEffect(() => {
755
+ if (!editing || !selectMode) return;
756
+ const onClick = (ev: MouseEvent) => {
757
+ const t = ev.target as Element | null;
758
+ const el = t?.closest("[data-editor-path]") as HTMLElement | null;
759
+ if (editandoRef.current && el === editandoRef.current.el) return; // clique dentro do texto em edição
760
+ // acordeão (FAQ): o clique seleciona a pergunta E abre a resposta, senão ela nunca aparece para editar
761
+ const sumario = t?.closest("summary");
762
+ if (sumario?.parentElement instanceof HTMLDetailsElement) sumario.parentElement.open = true;
763
+ ev.preventDefault();
764
+ ev.stopPropagation();
765
+ if (editandoRef.current) encerrarInline(true);
766
+ if (!el) {
767
+ // clique em área "morta": nunca cai no vazio — seleciona a seção envolvente
768
+ const sec = t?.closest("[data-editor-section]") as HTMLElement | null;
769
+ if (sec) {
770
+ overlayRef.current?.marcarSecao(sec);
771
+ post({ type: "unbox-editor:select-section", container: sec.dataset.editorContainer, id: sec.dataset.editorSection, label: sec.dataset.editorLabel, kind: sec.dataset.editorKind });
772
+ }
773
+ return;
774
+ }
775
+ const r = registry.current.get(el.dataset.editorPath ?? "");
776
+ if (!r) return;
777
+ select({ path: r.path, type: r.type, label: r.label, fallback: r.fallback }, el);
778
+ if (r.type === "text" && !el.querySelector("img,svg,video,input,button")) {
779
+ editandoRef.current = { el, path: r.path, original: el.innerText };
780
+ el.setAttribute("contenteditable", "plaintext-only");
781
+ el.setAttribute("data-editor-editing", "1");
782
+ el.focus();
783
+ // texto todo selecionado: digitar substitui; um segundo clique posiciona o cursor
784
+ try {
785
+ const range = document.createRange();
786
+ range.selectNodeContents(el);
787
+ const sel = window.getSelection();
788
+ sel?.removeAllRanges();
789
+ sel?.addRange(range);
790
+ } catch {}
791
+ }
792
+ };
793
+ const onKey = (ev: KeyboardEvent) => {
794
+ const e = editandoRef.current;
795
+ if (!e || ev.target !== e.el) return;
796
+ if (ev.key === "Escape") {
797
+ ev.preventDefault();
798
+ encerrarInline(false);
799
+ } else if (ev.key === "Enter" && !ev.shiftKey) {
800
+ ev.preventDefault();
801
+ encerrarInline(true);
802
+ }
803
+ };
804
+ const onBlur = (ev: FocusEvent) => {
805
+ if (editandoRef.current && ev.target === editandoRef.current.el) encerrarInline(true);
806
+ };
807
+ document.addEventListener("click", onClick, true);
808
+ document.addEventListener("keydown", onKey, true);
809
+ document.addEventListener("blur", onBlur, true);
810
+ return () => {
811
+ document.removeEventListener("click", onClick, true);
812
+ document.removeEventListener("keydown", onKey, true);
813
+ document.removeEventListener("blur", onBlur, true);
814
+ };
815
+ }, [editing, selectMode, select, encerrarInline]);
816
+
817
+ // manifesto também quando o documento muda (hidden/order refletem)
818
+ React.useEffect(() => {
819
+ scheduleManifest();
820
+ }, [doc, scheduleManifest]);
821
+
822
+ const value = React.useMemo<Ctx>(
823
+ // `container: undefined` de propósito: a raiz do provider não é a home. Quem quer editar declara
824
+ // o container da sua página (`Editable.Sections container="sobre"`); quem não declara não edita.
825
+ () => ({ doc, editing, selectMode, scope: [], container: undefined, layout: true, register, registerSection, registerTipos, select, foraDeContainer, previewToken, renovarToken }),
826
+ [doc, editing, selectMode, register, registerSection, registerTipos, select, foraDeContainer, previewToken, renovarToken],
827
+ );
828
+
829
+ // tokens editados → :root. Só os da allowlist da loja.
830
+ const allowed = React.useMemo(() => new Set(tokens.map((t) => t.token)), [tokens]);
831
+ // segunda trava, no cliente: só token da allowlist E só valor em formato de cor —
832
+ // este texto entra num <style> cru, então o formato fechado é a defesa.
833
+ const tokenCss = Object.entries(doc.tokens ?? {})
834
+ .filter(([k, v]) => allowed.has(k) && /^--[a-z0-9-]+$/.test(k) && typeof v === "string" && isColor(v))
835
+ .map(([k, v]) => `${k}:${v.trim()}`)
836
+ .join(";");
837
+
838
+ return (
839
+ <EditableContext.Provider value={value}>
840
+ {tokenCss ? <style data-editor-tokens="">{`:root{${tokenCss}}`}</style> : null}
841
+ <style data-editor-base="">{SECTION_CSS}</style>
842
+ {editing ? <style data-editor-css="" dangerouslySetInnerHTML={{ __html: EDITING_CSS }} /> : null}
843
+ {editing && selectMode ? <Overlay ref={overlayRef} /> : null}
844
+ {children}
845
+ </EditableContext.Provider>
846
+ );
847
+ }
848
+
849
+ /** Escopo de caminho: tudo que estiver dentro ganha o prefixo. */
850
+ export function EditableScope({ path, children }: { path: string; children: React.ReactNode }) {
851
+ const ctx = useEditableContext();
852
+ const value = React.useMemo(() => ({ ...ctx, scope: [...ctx.scope, path] }), [ctx, path]);
853
+ return <EditableContext.Provider value={value}>{children}</EditableContext.Provider>;
854
+ }
855
+
856
+ export function useEditableScopeValue(container: string, section: string | undefined, scope: string[]) {
857
+ const ctx = useEditableContext();
858
+ return React.useMemo(() => ({ ...ctx, container, section, scope }), [ctx, container, section, scope]);
859
+ }
860
+
861
+ export const EditableContextProvider = EditableContext.Provider;