@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,177 @@
1
+ "use client";
2
+
3
+ // Seção de combos da home (abaixo dos reviews). Layout em 3 colunas inspirado na referência:
4
+ // imagem + selo · nome/descrição/destaques/preço/CTA · lista "Este combo contém".
5
+ //
6
+ // EDITOR: o cabeçalho (chapéu, título, subtítulo) é copy da marca e vira caminho da seção que
7
+ // renderiza esta lista (a seção "kits" da home). Os kits em si são DADO da loja (lib/enrichment/
8
+ // combos.ts: nome, itens, preço, desconto): ficam fora dos primitivos (README do editor, §8) e
9
+ // fora do gate por `data-editor-ignore`.
10
+ import * as React from "react";
11
+ import Link from "next/link";
12
+ import Image from "next/image";
13
+ import {
14
+ Package, Fire, Sparkle, Star, ShieldCheck, Heart, Tag, Gift,
15
+ PiggyBank, ShoppingCartSimple, Spinner, CaretLeft, CaretRight,
16
+ } from "@phosphor-icons/react/dist/ssr";
17
+ import { useCart } from "@/components/cart/cart-provider";
18
+ import { formatBRL } from "@/lib/format";
19
+ import { Editable } from "@/lib/editable";
20
+ import { ComboCardCompact } from "@/components/home/combo-card-compact";
21
+ import type { ResolvedCombo, ComboHighlight } from "@/lib/enrichment/combos";
22
+
23
+ const HIGHLIGHT_ICON: Record<ComboHighlight["icon"], React.ComponentType<any>> = {
24
+ box: Package, fire: Fire, sparkle: Sparkle, star: Star,
25
+ shield: ShieldCheck, heart: Heart, tag: Tag, gift: Gift,
26
+ };
27
+
28
+ export function CombosSection({ combos }: { combos: ResolvedCombo[] }) {
29
+ const { add, setOpen } = useCart();
30
+ const [busyId, setBusyId] = React.useState<string | null>(null);
31
+ const rowRef = React.useRef<HTMLDivElement | null>(null);
32
+
33
+ if (combos.length === 0) return null;
34
+
35
+ async function addCombo(combo: ResolvedCombo) {
36
+ setBusyId(combo.id);
37
+ try {
38
+ // Adiciona cada item com o preço já com desconto do combo (silencioso) e abre o carrinho ao fim.
39
+ for (const it of combo.items) {
40
+ await add(
41
+ {
42
+ productId: it.productId,
43
+ variantId: it.variantId,
44
+ price: it.discountedPrice,
45
+ quantity: it.qty,
46
+ thumbnail: it.imageUrl ?? undefined,
47
+ title: `${combo.name} · ${it.name}`,
48
+ },
49
+ { silent: true },
50
+ );
51
+ }
52
+ setOpen(true);
53
+ } finally {
54
+ setBusyId(null);
55
+ }
56
+ }
57
+
58
+ const scrollRow = (dir: number) => rowRef.current?.scrollBy({ left: dir * 720, behavior: "smooth" });
59
+
60
+ return (
61
+ <div className="mx-auto max-w-[1240px] px-4 pt-[42px] sm:px-6">
62
+ <div className="mb-[18px] flex items-end justify-between gap-3">
63
+ <div>
64
+ <Editable.Text as="div" path="chapeu" fallback="KITS DA LOJA" label="Chapéu acima do título" className="text-xs font-extrabold tracking-[1.5px] text-[var(--store-primary,#18181B)]" />
65
+ <Editable.Text as="h2" path="titulo" fallback="Monte seu kit com nossos combos" label="Título" className="font-display mt-1.5 text-[26px] font-extrabold" />
66
+ <Editable.Text as="p" path="subtitulo" fallback="Tudo combinado, com desconto e pronto pro carrinho." label="Subtítulo" className="mt-1 text-sm text-[var(--store-muted)]" />
67
+ </div>
68
+ {combos.length > 1 && (
69
+ <div className="hidden gap-2.5 sm:flex">
70
+ <button type="button" onClick={() => scrollRow(-1)} aria-label="Anterior" className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-[1.5px] border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)]"><CaretLeft weight="bold" className="text-[var(--store-ink-2)]" /></button>
71
+ <button type="button" onClick={() => scrollRow(1)} aria-label="Próximo" className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-[1.5px] border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)]"><CaretRight weight="bold" className="text-[var(--store-ink-2)]" /></button>
72
+ </div>
73
+ )}
74
+ </div>
75
+
76
+ {/* desktop: card grande em 3 colunas (dado da loja: fora do editor) */}
77
+ <div ref={rowRef} className="hidden snap-x gap-5 overflow-x-auto pb-3.5 [scrollbar-width:thin] md:flex" data-editor-ignore="">
78
+ {combos.map((c) => (
79
+ <ComboCard key={c.id} combo={c} busy={busyId === c.id} onAdd={() => addCombo(c)} />
80
+ ))}
81
+ </div>
82
+
83
+ {/* mobile: versão compacta em carrossel */}
84
+ <div className="flex snap-x gap-3.5 overflow-x-auto pb-3.5 [scrollbar-width:none] md:hidden" data-editor-ignore="">
85
+ {combos.map((c) => (
86
+ <ComboCardCompact key={c.id} combo={c} className="w-[230px] flex-none snap-start" />
87
+ ))}
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function ComboCard({ combo, busy, onAdd }: { combo: ResolvedCombo; busy: boolean; onAdd: () => void }) {
94
+ const highlights: ComboHighlight[] = [
95
+ { icon: "box", label: `${combo.itemCount} itens` },
96
+ ...combo.highlights,
97
+ ];
98
+ const hero = combo.imageUrl || combo.items[0]?.imageUrl || null;
99
+
100
+ return (
101
+ <div className="flex w-[min(96vw,1000px)] flex-none snap-start overflow-hidden rounded-2xl border border-[var(--store-line)] bg-[var(--store-surface)] lg:w-[1000px]">
102
+ <div className="grid w-full grid-cols-1 lg:grid-cols-[0.9fr_1.1fr_1fr]">
103
+ {/* coluna 1: imagem ocupa toda a coluna + selo sobreposto */}
104
+ <div className="relative min-h-[260px] overflow-hidden border-b border-[var(--store-line)] bg-white lg:border-b-0 lg:border-r">
105
+ {combo.badge && (
106
+ <span className="absolute left-5 top-5 z-[2] rounded-full bg-[var(--store-cta-soft)] px-3 py-1 text-[11px] font-extrabold tracking-[0.4px] text-[var(--store-cta-fg)]">{combo.badge}</span>
107
+ )}
108
+ {hero ? (
109
+ <Image src={hero} alt={combo.name} fill sizes="(max-width:1024px) 100vw, 320px" className="object-cover" />
110
+ ) : (
111
+ <span className="flex h-full items-center justify-center text-sm text-[var(--store-faint)]">{combo.name}</span>
112
+ )}
113
+ </div>
114
+
115
+ {/* coluna 2: detalhes + CTA */}
116
+ <div className="flex flex-col justify-center border-b border-[var(--store-line)] p-6 sm:p-7 lg:border-b-0 lg:border-r">
117
+ <h3 className="font-display text-[26px] font-extrabold leading-[1.08] text-[var(--store-ink)]">{combo.name}</h3>
118
+ <p className="mt-2 max-w-[360px] text-[15px] leading-[1.5] text-[var(--store-ink-2)]">{combo.description}</p>
119
+
120
+ <div className="my-5 grid grid-cols-3 gap-2 border-y border-[var(--store-line)] py-4">
121
+ {highlights.map((h, i) => {
122
+ const Icon = HIGHLIGHT_ICON[h.icon];
123
+ return (
124
+ <div key={i} className="flex flex-col items-center gap-2 text-center">
125
+ <Icon className="text-[28px] text-[var(--store-primary,#18181B)]" />
126
+ <span className="text-[13px] font-semibold text-[var(--store-ink-2)]">{h.label}</span>
127
+ </div>
128
+ );
129
+ })}
130
+ </div>
131
+
132
+ {combo.save > 0 && (
133
+ <div className="flex flex-wrap items-center gap-2.5">
134
+ <span className="text-[15px] text-[var(--store-faint)] line-through">{formatBRL(combo.subtotal)}</span>
135
+ <span className="rounded-md bg-[var(--store-cta,#D97706)] px-2 py-0.5 text-[12px] font-extrabold text-[var(--store-cta-fg,#1C1207)]">{combo.discountPct}% OFF</span>
136
+ </div>
137
+ )}
138
+ <div className="font-display text-[34px] font-extrabold leading-[1.05] text-[var(--store-primary,#18181B)]">{formatBRL(combo.total)}</div>
139
+ {combo.save > 0 && (
140
+ <div className="mt-1 flex items-center gap-1.5 text-[14px] font-bold text-[var(--store-primary,#18181B)]"><PiggyBank weight="fill" />Economize {formatBRL(combo.save)}</div>
141
+ )}
142
+
143
+ <button
144
+ type="button"
145
+ onClick={onAdd}
146
+ disabled={busy}
147
+ className="font-display mt-5 flex h-[54px] w-full cursor-pointer items-center justify-center gap-2.5 rounded-xl bg-[var(--store-primary,#18181B)] text-[15px] font-bold tracking-[0.4px] text-white shadow-[var(--store-shadow-cta)] transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60"
148
+ >
149
+ {busy ? <Spinner className="animate-spin text-lg" /> : <ShoppingCartSimple weight="bold" className="text-lg" />}
150
+ ADICIONAR AO CARRINHO
151
+ </button>
152
+ </div>
153
+
154
+ {/* coluna 3: itens do combo */}
155
+ <div className="flex flex-col gap-2.5 p-6 sm:p-7">
156
+ <div className="text-[15px] font-extrabold text-[var(--store-ink)]">Este combo contém:</div>
157
+ {combo.items.map((it) => (
158
+ <Link
159
+ key={it.variantId}
160
+ href={`/produto/${encodeURIComponent(it.slug)}`}
161
+ className="flex items-center gap-3.5 rounded-lg border border-[var(--store-line)] bg-white p-2.5 pl-3 no-underline transition-colors hover:border-[var(--store-primary,#18181B)]"
162
+ >
163
+ <span className="relative flex h-[68px] w-[68px] shrink-0 items-center justify-center">
164
+ {it.imageUrl ? <Image src={it.imageUrl} alt={it.name} fill sizes="68px" className="object-contain" /> : <Package className="text-[28px] text-[var(--store-faint)]" />}
165
+ </span>
166
+ <div className="min-w-0 flex-1">
167
+ <div className="truncate text-[14px] font-bold text-[var(--store-ink)]">{it.name}</div>
168
+ {it.weight && <div className="text-[12.5px] text-[var(--store-muted)]">{it.weight}</div>}
169
+ </div>
170
+ <span className="shrink-0 text-[14px] font-extrabold text-[var(--store-primary,#18181B)]">{it.qty}×</span>
171
+ </Link>
172
+ ))}
173
+ </div>
174
+ </div>
175
+ </div>
176
+ );
177
+ }
@@ -0,0 +1,41 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // RECEITA DA HOME — a ordem, as variantes e as props das seções.
3
+ // Escrita pelo create-unbox-store conforme o ESTILO escolhido no setup; editada
4
+ // pelo agente de branding no briefing (é AQUI que o layout muda — não reescreva
5
+ // o JSX das seções; catálogo de seções/variantes em agents/PADROES.md).
6
+ // `npm run typecheck` acusa seção/variante inexistente.
7
+ //
8
+ // EDITOR DE LOJA: cada entrada vira uma seção editável com id, tipo e nome próprios. Os três saem de
9
+ // IDENTIDADE_PADRAO (components/home/sections/registry.ts) quando a entrada não os declara: o hero
10
+ // vira "banner", o combos-carousel vira "vitrine", e assim por diante. Declare `id` à mão só numa
11
+ // seção REPETIDA na receita, ou num bloco com nome próprio (`{ section: "product-story", id:
12
+ // "tradicional", label: "Tradicional" }`): o id é chave primária depois que a loja publica, e o
13
+ // derivado das repetidas é posicional (`banner-2`), o que faz reordenar renomear.
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+ import type { HomeRecipe } from "@/components/home/sections/registry";
16
+
17
+ export const homeRecipe: HomeRecipe = [
18
+ {
19
+ section: "hero",
20
+ variant: "imagem-full",
21
+ props: {
22
+ imageDesktop: "/brand/hero-desktop.svg",
23
+ imageMobile: "/brand/hero-mobile.svg",
24
+ },
25
+ },
26
+ { section: "trust-bar" },
27
+ { section: "attributes-marquee" },
28
+ { section: "category-pills" },
29
+ { section: "combos-carousel" },
30
+ { section: "savings" },
31
+ { section: "benefits" },
32
+ { section: "media-cards" },
33
+ { section: "reviews", variant: "grid" },
34
+ { section: "ritual" },
35
+ { section: "founder-story" },
36
+ { section: "kits" },
37
+ { section: "spec-table" },
38
+ { section: "video-wall" },
39
+ { section: "trust-strip" },
40
+ { section: "newsletter", variant: "bloco" },
41
+ ];
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ // Marquee infinito de atributos da marca (artesanal, feito no Brasil, edição limitada...).
4
+ // Portado de uma loja Unbox real em produção e genericizado: itens via receita
5
+ // (props.items: [{ label, icon?, slashed? }]). Sem itens, não renderiza.
6
+ // Ícones por nome (mapa abaixo) pra receita ser serializável.
7
+ //
8
+ // ── EDITOR ──────────────────────────────────────────────────────────────────
9
+ // Cada palavra da faixa é um ITEM de lista (`Editable.Sections nested` + `Editable.Section item`):
10
+ // o lojista troca o texto, troca o ícone, reordena, oculta e duplica, sem código novo.
11
+ //
12
+ // A faixa é a MESMA lista repetida 4 vezes na tela (é assim que o `translateX(-50%)` fecha o loop
13
+ // sem emenda). Por isso só a PRIMEIRA sequência é a lista editável; as outras são ECO: leem o
14
+ // documento direto (`orderSections` + `resolveValue`) e não registram caminho nenhum. Sem isso a
15
+ // mesma palavra viraria 4 registros e 4 alvos de clique no painel, e as metades deixariam de bater.
16
+ //
17
+ // Os caminhos são RELATIVOS ao escopo do item: a seção entra na home e na landing de oferta, e um
18
+ // caminho com o id da seção dentro faria as duas instâncias brigarem pela mesma caixa.
19
+ import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
20
+ import { Leaf, Drop, Cube, Bread, Heart, Sparkle, ShieldCheck, Star } from "@phosphor-icons/react/dist/ssr";
21
+ import { Editable, joinPath, orderSections, resolveValue, useEditableContext, type ImageValue } from "@/lib/editable";
22
+ import type { SectionComponentProps } from "./registry";
23
+
24
+ const ICONS: Record<string, PhosphorIcon> = {
25
+ leaf: Leaf, drop: Drop, cube: Cube, bread: Bread,
26
+ heart: Heart, sparkle: Sparkle, shield: ShieldCheck, star: Star,
27
+ };
28
+
29
+ type Attribute = { label: string; icon?: string; text?: string; slashed?: boolean };
30
+
31
+ // ── Caminhos editáveis (relativos ao item) ──────────────────────────────────
32
+ const CAMINHO_TEXTO = "texto";
33
+ const CAMINHO_ICONE = "icone";
34
+ const ROTULO_ICONE = "Ícone";
35
+ const TAMANHO_ICONE = 18;
36
+
37
+ /** id do item na lista editável: posicional, o padrão da foundation. */
38
+ const idDoItem = (i: number) => `item-${i + 1}`;
39
+
40
+ // As classes viram constante porque a linha editável e o eco dela precisam
41
+ // renderizar EXATAMENTE o mesmo HTML: duas cópias do literal descolariam.
42
+ const CLASSE_PILULA = "rounded-full bg-[var(--store-primary-soft,#F1F1F3)]/70 px-4 py-2 text-[13px] font-semibold text-[var(--store-primary,#18181B)]";
43
+ const CLASSE_CIRCULO = "relative flex h-11 w-11 shrink-0 items-center justify-center rounded-full border-2 border-[var(--store-primary,#18181B)]/70 text-[var(--store-primary,#18181B)]";
44
+ const CLASSE_RISCO = "pointer-events-none absolute left-[-2px] right-[-2px] top-1/2 h-[2px] -translate-y-1/2 rotate-45 bg-[var(--store-primary,#18181B)]/70";
45
+ const CLASSE_SIGLA = "text-[9px] font-extrabold";
46
+ const CLASSE_ICONE = "text-[18px]";
47
+
48
+ /** Item da faixa já resolvido pelo documento (ordem, ocultas, cópias e valores do lojista). */
49
+ type ItemPublicado = { id: string; base: Attribute; texto: string; icone: ImageValue };
50
+
51
+ /**
52
+ * O ECO da lista: mesma ordem, mesmas ocultas, mesmas cópias e mesmos valores da primeira
53
+ * sequência, só que lido do documento, SEM registrar ponto editável. É o que mantém as
54
+ * metades do marquee do mesmo tamanho depois que o lojista mexe na lista.
55
+ */
56
+ function useItensPublicados(items: Attribute[]): ItemPublicado[] {
57
+ const ctx = useEditableContext();
58
+ // `Editable.Sections nested` usa o escopo atual como container; o eco lê o mesmo lugar.
59
+ const escopo = ctx.scope.join(".");
60
+ const ids = items.map((_, i) => idDoItem(i));
61
+ const estado = ctx.doc.sections[escopo];
62
+ const { visible } = orderSections(ids, estado);
63
+ const copias = estado?.clones ?? {};
64
+ return visible.flatMap((id) => {
65
+ const base = items[ids.indexOf(copias[id] ?? id)];
66
+ if (!base) return [];
67
+ const prefixo = [escopo, id].filter(Boolean).join(".");
68
+ return [{
69
+ id,
70
+ base,
71
+ texto: resolveValue(ctx.doc, `${prefixo}.${CAMINHO_TEXTO}`, base.label),
72
+ icone: resolveValue<ImageValue>(ctx.doc, `${prefixo}.${CAMINHO_ICONE}`, { src: "", alt: ROTULO_ICONE }),
73
+ }];
74
+ });
75
+ }
76
+
77
+ /** O texto do item já publicado, SEM registrar um segundo ponto: quem registra é o rótulo. */
78
+ function useTextoDoItem(fallback: string) {
79
+ const ctx = useEditableContext();
80
+ return resolveValue(ctx.doc, joinPath(ctx.scope, CAMINHO_TEXTO), fallback);
81
+ }
82
+
83
+ /** Sem ícone na receita, o círculo mostra as 3 primeiras letras: derivado, não é copy nova. */
84
+ const sigla = (t: string) => t.slice(0, 3).toUpperCase();
85
+
86
+ function CirculoDoIcone({ slashed, children }: { slashed?: boolean; children: React.ReactNode }) {
87
+ return (
88
+ <span className={CLASSE_CIRCULO}>
89
+ {children}
90
+ {slashed && <span className={CLASSE_RISCO} />}
91
+ </span>
92
+ );
93
+ }
94
+
95
+ /** Ícone da linha editável. A `className` do ícone vai para o primitivo (regra do `Editable.Icon`). */
96
+ function IconeEditavel({ item }: { item: Attribute }) {
97
+ const Icon = item.icon ? ICONS[item.icon] : undefined;
98
+ const texto = useTextoDoItem(item.label);
99
+ if (!Icon) return <span className={CLASSE_SIGLA}>{sigla(item.text ?? texto)}</span>;
100
+ return (
101
+ <Editable.Icon path={CAMINHO_ICONE} label={ROTULO_ICONE} size={TAMANHO_ICONE} className={CLASSE_ICONE}>
102
+ <Icon weight="bold" />
103
+ </Editable.Icon>
104
+ );
105
+ }
106
+
107
+ /** Espelho do `Editable.Icon` para as cópias da faixa: mesmo `<span>`/`<img>`, sem registro. */
108
+ function IconeEco({ item }: { item: ItemPublicado }) {
109
+ const Icon = item.base.icon ? ICONS[item.base.icon] : undefined;
110
+ if (!Icon) return <span className={CLASSE_SIGLA}>{sigla(item.base.text ?? item.texto)}</span>;
111
+ if (item.icone.src) {
112
+ // eslint-disable-next-line @next/next/no-img-element -- espelho do <img> que o Editable.Icon rende
113
+ return <img src={item.icone.src} alt={item.icone.alt ?? ROTULO_ICONE} width={TAMANHO_ICONE} height={TAMANHO_ICONE} className={CLASSE_ICONE} style={{ display: "inline-block", objectFit: "contain" }} />;
114
+ }
115
+ return (
116
+ <span className={CLASSE_ICONE} style={{ display: "inline-flex", lineHeight: 0 }}>
117
+ <Icon weight="bold" />
118
+ </span>
119
+ );
120
+ }
121
+
122
+ // Cada item carrega a própria margem à direita (em vez de gap no pai): a largura de um "set"
123
+ // já inclui o espaço até o próximo e a duplicata encaixa exata com translateX(-50%).
124
+ function BadgeRowEditavel({ items }: { items: Attribute[] }) {
125
+ return (
126
+ <Editable.Sections nested>
127
+ {items.map((a, i) => (
128
+ <Editable.Section key={idDoItem(i)} item id={idDoItem(i)} label={`Atributo ${i + 1}`}>
129
+ <span className="mr-6 flex shrink-0 items-center gap-3">
130
+ <CirculoDoIcone slashed={a.slashed}>
131
+ <IconeEditavel item={a} />
132
+ </CirculoDoIcone>
133
+ <Editable.Text path={CAMINHO_TEXTO} fallback={a.label} label="Texto do atributo" className={CLASSE_PILULA} />
134
+ </span>
135
+ </Editable.Section>
136
+ ))}
137
+ </Editable.Sections>
138
+ );
139
+ }
140
+
141
+ function BadgeRowEco({ itens }: { itens: ItemPublicado[] }) {
142
+ return (
143
+ <>
144
+ {itens.map((it) => (
145
+ <span key={it.id} className="mr-6 flex shrink-0 items-center gap-3">
146
+ <CirculoDoIcone slashed={it.base.slashed}>
147
+ <IconeEco item={it} />
148
+ </CirculoDoIcone>
149
+ <span className={CLASSE_PILULA}>{it.texto}</span>
150
+ </span>
151
+ ))}
152
+ </>
153
+ );
154
+ }
155
+
156
+ export function AttributesMarqueeSection({ sectionProps = {} }: SectionComponentProps) {
157
+ const items = ((sectionProps.items as Attribute[] | undefined) ?? []).filter((a) => a?.label);
158
+ const publicados = useItensPublicados(items);
159
+ if (items.length === 0) return null;
160
+ return (
161
+ // Dentro do container (--container-max), como as demais seções: uma faixa que sangrava
162
+ // de ponta a ponta enquanto hero e trust-bar ficam recuados lê como erro de layout.
163
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[18px] sm:px-6">
164
+ <div className="overflow-hidden rounded-2xl border border-[var(--store-primary,#18181B)]/10 bg-[var(--store-surface)] py-3">
165
+ {/* 4 cópias, não 2: com frases curtas, duas cópias dão ~700px por metade e a partir de
166
+ ~1400px de viewport abria um vão antes do loop fechar. A animação anda -50% (duas
167
+ cópias) em 44s — o dobro do tempo de antes, para a velocidade não mudar.
168
+ A 1ª é a lista editável; as outras três ecoam o documento. */}
169
+ <div className="flex w-max animate-marquee [animation-duration:44s]">
170
+ <div className="flex shrink-0 items-center"><BadgeRowEditavel items={items} /></div>
171
+ <div className="flex shrink-0 items-center" aria-hidden="true"><BadgeRowEco itens={publicados} /></div>
172
+ <div className="flex shrink-0 items-center" aria-hidden="true"><BadgeRowEco itens={publicados} /></div>
173
+ <div className="flex shrink-0 items-center" aria-hidden="true"><BadgeRowEco itens={publicados} /></div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ // Benefícios em grid 2+foto+2 (lista esquerda / imagem central / lista direita).
4
+ // Portado de uma loja Unbox real em produção e genericizado. Conteúdo via receita; defaults
5
+ // neutros SEM claims inventados (nada de estatística ou promessa de resultado fake).
6
+ //
7
+ // EDITOR: título, subtítulo e a foto do meio são caminhos da seção; cada coluna é uma LISTA
8
+ // editável própria (`esquerda.beneficio-N` e `direita.beneficio-N`): o lojista troca texto,
9
+ // reordena, oculta e duplica dentro da coluna. São duas listas, e não uma, porque cada coluna é um
10
+ // `flex` diferente e uma lista só não renderiza em dois pais. Os caminhos são RELATIVOS à seção:
11
+ // a mesma seção pode entrar na home e na landing sem que as duas disputem a mesma caixa.
12
+ // A foto continua passando pelo `Foto` (otimizador do Next quando o host permite): o `Editable.Slot`
13
+ // só lhe entrega o src/alt do lojista e os atributos de seleção.
14
+ import { Editable, EditableScope } from "@/lib/editable";
15
+ import type { SectionComponentProps } from "./registry";
16
+ import { Foto } from "@/components/ui/foto";
17
+
18
+ type Benefit = { title: string; text: string };
19
+
20
+ const DEFAULT_LEFT: Benefit[] = [
21
+ { title: "Seleção com critério", text: "Cada item do catálogo passa por curadoria de verdade antes de entrar na loja." },
22
+ { title: "Qualidade que se percebe", text: "Materiais escolhidos pra durar e entregar o que prometem." },
23
+ ];
24
+ const DEFAULT_RIGHT: Benefit[] = [
25
+ { title: "Entrega bem cuidada", text: "Embalagem pensada pra chegar inteiro, bonito e pronto pra usar." },
26
+ { title: "Suporte de gente", text: "Ficou com dúvida? A gente responde rápido e resolve junto." },
27
+ ];
28
+
29
+ function Item({ b }: { b: Benefit }) {
30
+ return (
31
+ <div>
32
+ <Editable.Text as="div" path="titulo" fallback={b.title} label="Título do benefício" className="font-display text-[17px] font-bold text-[var(--store-primary,#18181B)]" />
33
+ <Editable.Text as="p" path="texto" fallback={b.text} label="Texto do benefício" multiline className="mt-1.5 text-[14px] leading-[1.55] text-[var(--store-muted)]" />
34
+ </div>
35
+ );
36
+ }
37
+
38
+ /** Uma coluna de benefícios: lista editável com escopo próprio (`esquerda` / `direita`). */
39
+ function Coluna({ lado, nome, itens }: { lado: "esquerda" | "direita"; nome: string; itens: Benefit[] }) {
40
+ return (
41
+ <div className="flex flex-col gap-7">
42
+ <EditableScope path={lado}>
43
+ <Editable.Sections nested>
44
+ {itens.map((b, i) => (
45
+ <Editable.Section key={b.title} item id={`beneficio-${i + 1}`} label={`Benefício ${i + 1} (${nome})`}>
46
+ <Item b={b} />
47
+ </Editable.Section>
48
+ ))}
49
+ </Editable.Sections>
50
+ </EditableScope>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ export function BenefitsSection({ sectionProps = {} }: SectionComponentProps) {
56
+ const {
57
+ title = "Por que escolher a gente",
58
+ subtitle = "O essencial, bem feito, do jeito que você espera.",
59
+ image = "/brand/ph/photo-a.svg",
60
+ } = sectionProps as Record<string, string>;
61
+ const left = (sectionProps.left as Benefit[] | undefined) ?? DEFAULT_LEFT;
62
+ const right = (sectionProps.right as Benefit[] | undefined) ?? DEFAULT_RIGHT;
63
+
64
+ return (
65
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[var(--section-gap,52px)] sm:px-6">
66
+ <div className="mx-auto max-w-[620px] text-center">
67
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[28px] font-extrabold leading-[1.15]" />
68
+ <Editable.Text as="p" path="subtitulo" fallback={subtitle} label="Subtítulo" className="mt-2 text-[15px] text-[var(--store-muted)]" />
69
+ </div>
70
+ <div className="mt-9 grid items-center gap-8 md:grid-cols-[1fr_minmax(240px,340px)_1fr]">
71
+ <Coluna lado="esquerda" nome="coluna da esquerda" itens={left} />
72
+ {/* TODO: trocar por foto real do produto/marca (o agente de branding cuida disso). alt="" de propósito: é decorativa, quem fala é o texto. */}
73
+ <Editable.Slot path="imagem" type="image" fallback={{ src: image, alt: "" }} label="Foto do meio">
74
+ {(v, attrs, ref) => <Foto ref={ref} attrs={attrs} src={v.src} alt={v.alt ?? ""} width={680} height={680} sizes="340px" className="mx-auto w-full max-w-[340px] rounded-2xl object-cover" />}
75
+ </Editable.Slot>
76
+ <Coluna lado="direita" nome="coluna da direita" itens={right} />
77
+ </div>
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ // BLOCO DE HTML — a seção em que o LOJISTA cola um HTML pronto que veio de fora
5
+ // (o selo de um fornecedor, uma tabela de medidas, um trecho que alguém mandou).
6
+ //
7
+ // SIMPLES DE PROPÓSITO: a seção não desenha nada. Ela é só o container (a mesma largura e o mesmo
8
+ // respiro vertical das outras seções da home), e todo o conteúdo sai do primitivo. Título, subtítulo
9
+ // ou fundo próprio aqui competiriam com o que o lojista colou e ele não teria como desligá-los.
10
+ //
11
+ // O QUE É PERIGOSO NÃO ENTRA, E QUEM DECIDE ISSO NÃO É ESTA SEÇÃO. `Editable.Html` (lib/editable/
12
+ // primitives.tsx) roda a lista de recusa (`recusaDeHtml`) no cliente antes de injetar, e é a única das
13
+ // três camadas que existe na PRÉVIA (ali o rascunho chega por postMessage sem passar por servidor
14
+ // nenhum). As outras duas: o editor ao gravar (limpa e RELATA ao lojista o que tirou) e a loja ao ler o
15
+ // publicado (lib/editable/server.ts, que faz uma regra nova valer para quem já publicou).
16
+ //
17
+ // ── POR QUE O CONTAINER É O PRÓPRIO PRIMITIVO, E NÃO UM <div> À VOLTA DELE ──
18
+ // Bloco vazio em produção não renderiza NADA, nem invólucro (regra do primitivo). Se o padding
19
+ // morasse num <div> à volta, esse <div> continuaria na página com `py-72px` e abriria uma faixa branca
20
+ // no meio da home enquanto o lojista não colasse nada. Com as classes NO primitivo, some tudo junto.
21
+ //
22
+ // O <div className="w-full"> de fora existe por outro motivo: o fundo de seção do editor pinta o
23
+ // PRIMEIRO FILHO do invólucro da seção (a regra `[data-unbox-sec-bg="1"]>*` em provider.tsx), e o
24
+ // invólucro é `display: contents`. Sem esse filho de largura cheia, um fundo escolhido pelo lojista
25
+ // pararia na largura do container em vez de atravessar a tela. Vazio ele sai como
26
+ // `<div class="w-full"></div>`, altura 0 e padding 0: não é a faixa branca de que o parágrafo acima fala.
27
+ //
28
+ // O `sectionProps` fica de fora de propósito: um HTML cru escrito na RECEITA seria marcação à mão num
29
+ // arquivo onde o certo é chamar um componente. Este bloco nasce para o lojista colar.
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+ import { EditableHtml } from "@/lib/editable";
32
+
33
+ /**
34
+ * Não recebe props: o conteúdo é do documento, não da receita (ver o cabeçalho). A assinatura continua
35
+ * compatível com `SectionComponentProps`: o renderer passa `data`/`variant` e esta seção os ignora.
36
+ */
37
+ export function BlocoHtmlSection() {
38
+ return (
39
+ <div className="w-full">
40
+ <EditableHtml
41
+ path="conteudo"
42
+ label="Bloco de HTML"
43
+ className="mx-auto max-w-[var(--container-max,1240px)] px-4 py-[var(--section-gap,52px)] sm:px-6"
44
+ />
45
+ </div>
46
+ );
47
+ }