@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.

Potentially problematic release.


This version of @unbox-plus/cli might be problematic. Click here for more details.

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,187 @@
1
+ "use client";
2
+
3
+ // HERO DE COMPRA — o bloco de conversão principal (padrão das lojas Unbox de produção):
4
+ // galeria + título + seletor de quantidade com tiers + vantagens + garantia, como SEÇÃO
5
+ // de home (âncora #comprar) ou topo de landing. Produto e preço REAIS (data.featured).
6
+ // Tiers em QUANTITY_TIERS (lib/store-config.ts) — só prometa o que o backend cumpre.
7
+ // CTA → /carrinho/oferta (passo 2: escolher produtos/variantes + frequência).
8
+ //
9
+ // EDITOR: é copy da marca e vira caminho da seção: o chapéu (`chapeu`, quando a receita traz), o
10
+ // texto (`texto`), o botão (`cta`, um Slot: o <button> tem ícone ao lado e precisa continuar o
11
+ // mesmo elemento), as vantagens (LISTA `vantagens.vantagem-N`, com etiqueta, texto e ícone) e as
12
+ // duas linhas da garantia (`garantia.chapeu`, `garantia.titulo`). Fica fora do editor, e fora do
13
+ // gate por `data-editor-ignore`, o que é DADO ou MECÂNICA (README do editor, §8): foto, nome e preço
14
+ // do produto, os tiers de quantidade (configuração comercial), os cálculos de desconto e a frase do
15
+ // CDC (texto legal). Para o lojista o topo é BANNER ou "bloco de compra", nunca "hero".
16
+ import * as React from "react";
17
+ import Link from "next/link";
18
+ import { useRouter } from "next/navigation";
19
+ import { ShieldCheck, CreditCard, Truck, ArrowRight } from "@phosphor-icons/react/dist/ssr";
20
+ import { Editable, EditableScope } from "@/lib/editable";
21
+ import { Stars } from "./stars";
22
+ import { QUANTITY_TIERS } from "@/lib/store-config";
23
+ import { formatBRL } from "@/lib/format";
24
+ import type { SectionComponentProps } from "./registry";
25
+ import { Foto } from "@/components/ui/foto";
26
+
27
+ type Perk = { tag: string; text: string; icon?: string };
28
+ const PERK_ICONS: Record<string, React.ComponentType<any>> = { shield: ShieldCheck, card: CreditCard, truck: Truck };
29
+
30
+
31
+ export function PurchaseHeroSection({ data, sectionProps = {} }: SectionComponentProps) {
32
+ const router = useRouter();
33
+ const product = data.featured;
34
+ const {
35
+ tagline = "",
36
+ description = "",
37
+ ctaLabel = "2. Escolher os produtos",
38
+ sacEmail = "",
39
+ } = sectionProps as Record<string, string>;
40
+ // Sem perks na receita, o bloco de perks some (nada de "envio com rastreio" por default).
41
+ const perks = (sectionProps.perks as Perk[] | undefined) ?? [];
42
+ const rating = typeof sectionProps.rating === "number" ? (sectionProps.rating as number) : null;
43
+
44
+ const defaultIdx = Math.max(0, QUANTITY_TIERS.findIndex((t) => t.selected) !== -1
45
+ ? QUANTITY_TIERS.findIndex((t) => t.selected)
46
+ : Math.floor((QUANTITY_TIERS.length - 1) / 2));
47
+ const [sel, setSel] = React.useState(defaultIdx);
48
+ const [busy, setBusy] = React.useState(false);
49
+
50
+ if (!product) return null;
51
+ const tier = QUANTITY_TIERS[sel];
52
+ const unit = product.price;
53
+ const unitOff = unit * (1 - tier.offPct / 100);
54
+ const totalFull = unit * tier.quantity;
55
+ const total = unitOff * tier.quantity;
56
+ const anyOff = QUANTITY_TIERS.some((t) => t.offPct > 0);
57
+
58
+ return (
59
+ <div id="comprar" className="mx-auto max-w-[var(--container-max,1240px)] scroll-mt-24 px-4 pt-[42px] sm:px-6">
60
+ <div className="grid gap-9 lg:grid-cols-[1.02fr_0.98fr]">
61
+ {/* galeria (foto do produto: dado do catálogo) */}
62
+ <div className="lg:sticky lg:top-24 lg:self-start" data-editor-ignore="">
63
+ <div className="overflow-hidden rounded-2xl bg-[var(--store-surface-2)]">
64
+ {product.imageUrl
65
+ ? <Foto src={product.imageUrl} alt={product.title} width={1200} height={1200} sizes="(min-width: 1024px) 600px, 100vw" priority className="aspect-square w-full object-contain p-8" />
66
+ : <img src="/brand/ph/photo-a.svg" alt="" className="aspect-square w-full object-cover" />}
67
+ </div>
68
+ <Link href={`/produto/${encodeURIComponent(product.slug)}`} className="mt-3 inline-block text-[13.5px] font-semibold text-[var(--store-primary,#18181B)] underline underline-offset-2">Ver página do produto</Link>
69
+ </div>
70
+
71
+ {/* compra */}
72
+ <div>
73
+ <div className="flex items-center gap-2">
74
+ <Stars n={rating} className="text-[16px] text-[var(--store-cta-dark)]" />
75
+ {tagline && <Editable.Text path="chapeu" fallback={tagline} label="Chapéu acima do nome do produto" className="text-[12.5px] font-extrabold uppercase tracking-[0.8px] text-[var(--store-primary,#18181B)]" />}
76
+ </div>
77
+ <h2 className="font-display mt-2.5 text-[27px] font-extrabold leading-[1.15] sm:text-[31px]" data-editor-ignore="">{product.title}</h2>
78
+ {description && <Editable.Text as="p" path="texto" fallback={description} label="Texto do bloco de compra" multiline className="mt-2.5 text-[15px] leading-[1.55] text-[var(--store-muted)]" />}
79
+
80
+ <div className="mt-5 store-card rounded-2xl p-5">
81
+ {/* tiers, desconto e totais: configuração comercial e cálculo, fora do editor */}
82
+ <div data-editor-ignore="">
83
+ {anyOff && (
84
+ <>
85
+ <div className="text-[13.5px] font-extrabold text-[var(--store-ink)]">🔥 Desconto acumulado</div>
86
+ {tier.offPct > 0 && (
87
+ <div className="mt-2 rounded-lg bg-[var(--store-cta-soft,#FDF0DC)] px-3.5 py-2 text-[12.5px] font-bold text-[var(--store-cta-fg,#1C1207)]">
88
+ 📦 Você desbloqueou {tier.offPct}% OFF! Economia de {formatBRL(totalFull - total)}
89
+ </div>
90
+ )}
91
+ </>
92
+ )}
93
+ <div className={`${anyOff ? "mt-4" : ""} text-[13px] font-bold text-[var(--store-ink)]`}>1. Selecione a quantidade</div>
94
+ <div className="mt-2.5 grid gap-2.5">
95
+ {QUANTITY_TIERS.map((t, i) => {
96
+ const u = unit * (1 - t.offPct / 100);
97
+ return (
98
+ <button key={t.quantity} type="button" onClick={() => setSel(i)}
99
+ className={`flex items-center justify-between gap-3 rounded-xl border-2 px-4 py-3.5 text-left transition-colors ${i === sel ? "border-[var(--store-cta,#D97706)] bg-white shadow-sm" : "border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)]/50"}`}>
100
+ <span className="flex items-center gap-3">
101
+ <span className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full ${i === sel ? "bg-[var(--store-cta,#D97706)]" : "border-2 border-[var(--store-faint)]"}`}>
102
+ {i === sel && <span className="text-[11px] font-black text-[var(--store-cta-fg,#1C1207)]">✓</span>}
103
+ </span>
104
+ <span>
105
+ <span className="font-display flex items-center gap-2 text-[15px] font-extrabold">
106
+ {t.quantity} {t.quantity === 1 ? "unidade" : "unidades"}
107
+ {t.badge && <span className="rounded-full bg-[var(--store-surface-2)] px-2.5 py-0.5 text-[10.5px] font-bold uppercase tracking-[0.4px] text-[var(--store-muted)]">{t.badge}</span>}
108
+ </span>
109
+ <span className="mt-0.5 block text-[12.5px] font-semibold text-[var(--store-muted)]">{formatBRL(u)}/unidade{t.perks.length > 0 ? ` · ${t.perks.join(" + ")}` : ""}</span>
110
+ </span>
111
+ </span>
112
+ <span className="text-right">
113
+ {t.offPct > 0 && <span className="mr-2 rounded-full bg-[var(--store-cta-soft,#FDF0DC)] px-2 py-0.5 text-[11px] font-extrabold text-[var(--store-cta-fg,#1C1207)]">{t.offPct}%OFF</span>}
114
+ {t.offPct > 0 && <span className="block text-[11.5px] text-[var(--store-faint)] line-through">{formatBRL(unit * t.quantity)}</span>}
115
+ <span className="font-display text-[16px] font-extrabold text-[var(--store-ink)]">{formatBRL(u * t.quantity)}</span>
116
+ </span>
117
+ </button>
118
+ );
119
+ })}
120
+ </div>
121
+ </div>
122
+
123
+ {/* vantagens: lista editável (o invólucro do item é display:contents; a célula do grid é o card) */}
124
+ {perks.length > 0 && (
125
+ <div className="mt-5 grid grid-cols-3 gap-2.5">
126
+ <EditableScope path="vantagens">
127
+ <Editable.Sections nested>
128
+ {perks.map((p, i) => {
129
+ const Icon = PERK_ICONS[p.icon ?? ""] ?? ShieldCheck;
130
+ return (
131
+ <Editable.Section key={i} item id={`vantagem-${i + 1}`} label={`Vantagem ${i + 1}`}>
132
+ <div className="overflow-hidden rounded-xl border border-[var(--store-line-2)] text-center">
133
+ <Editable.Text as="div" path="etiqueta" fallback={p.tag} label="Etiqueta da vantagem" className="bg-[var(--store-chrome-bg,#18181B)] px-1 py-1.5 text-[9.5px] font-extrabold uppercase tracking-[0.5px] text-[var(--store-chrome-text,#ffffff)]" />
134
+ <div className="flex flex-col items-center gap-1.5 px-2 py-3">
135
+ <Editable.Icon path="icone" label="Ícone da vantagem" size={22} className="text-[22px] text-[var(--store-primary,#18181B)]">
136
+ <Icon weight="fill" />
137
+ </Editable.Icon>
138
+ <Editable.Text path="texto" fallback={p.text} label="Texto da vantagem" className="text-[11px] font-semibold leading-[1.3] text-[var(--store-ink-2)]" />
139
+ </div>
140
+ </div>
141
+ </Editable.Section>
142
+ );
143
+ })}
144
+ </Editable.Sections>
145
+ </EditableScope>
146
+ </div>
147
+ )}
148
+
149
+ {/* resumo + CTA */}
150
+ <div className="mt-4 flex flex-wrap items-center justify-between gap-2" data-editor-ignore="">
151
+ <span className="flex items-center gap-2">
152
+ <span className="rounded-full bg-[var(--store-primary-soft,#F1F1F3)] px-3 py-1 text-[12px] font-bold text-[var(--store-primary,#18181B)]">{formatBRL(unitOff)}/unidade</span>
153
+ {tier.offPct > 0 && <span className="rounded-full bg-[var(--store-cta-soft,#FDF0DC)] px-3 py-1 text-[12px] font-extrabold text-[var(--store-cta-fg,#1C1207)]">{tier.offPct}%OFF</span>}
154
+ </span>
155
+ <span className="text-right">
156
+ {tier.offPct > 0 && <span className="mr-2 text-[12.5px] text-[var(--store-faint)] line-through">De {formatBRL(totalFull)}</span>}
157
+ <span className="font-display text-[19px] font-extrabold">{formatBRL(total)}</span>
158
+ </span>
159
+ </div>
160
+ <Editable.Slot path="cta" type="text" fallback={ctaLabel} label="Botão de compra">
161
+ {(v, attrs, ref, estilo) => (
162
+ <button ref={ref} {...attrs} type="button" disabled={busy} onClick={() => { setBusy(true); router.push(`/carrinho/oferta?quantity=${tier.quantity}`); }}
163
+ className="font-display mt-3.5 flex w-full items-center justify-center gap-2 rounded-full bg-[var(--store-cta,#D97706)] py-4 text-[15.5px] font-extrabold uppercase tracking-[0.5px] text-[var(--store-cta-fg,#1C1207)] transition-colors hover:bg-[var(--store-cta-dark,#B45309)] disabled:opacity-60" style={estilo}>
164
+ {busy ? "Preparando..." : v} <ArrowRight weight="bold" />
165
+ </button>
166
+ )}
167
+ </Editable.Slot>
168
+ </div>
169
+
170
+ <div className="mt-4 flex items-start gap-3.5 rounded-2xl border-2 border-dashed border-[var(--store-cta,#D97706)]/60 bg-[var(--store-surface)] px-5 py-4">
171
+ <span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--store-primary-soft,#F1F1F3)] text-[var(--store-primary,#18181B)]">
172
+ <Editable.Icon path="garantia.icone" label="Ícone da garantia" size={22} className="text-[22px]">
173
+ <ShieldCheck weight="fill" />
174
+ </Editable.Icon>
175
+ </span>
176
+ <div className="text-[13px] leading-[1.5] text-[var(--store-muted)]">
177
+ <Editable.Text as="span" path="garantia.chapeu" fallback="Seu pedido está protegido por" label="Linha acima da garantia" className="block text-[11.5px]" />
178
+ <Editable.Text as="span" path="garantia.titulo" fallback="Compra segura e troca fácil" label="Título da garantia" className="font-display block text-[15px] font-extrabold text-[var(--store-ink)]" />
179
+ {/* texto legal (CDC): fora do editor por regra (§8) */}
180
+ <span data-editor-ignore="">Arrependimento em até 7 dias após o recebimento (CDC).{sacEmail ? ` Fale com ${sacEmail}.` : ""}</span>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ );
187
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ // Faixa full-width com foto + citação grande (na loja de referência: depoimento da fundadora).
4
+ // Conteúdo via receita; default neutro assinado pela própria loja (nunca invente
5
+ // depoimento de terceiro).
6
+ //
7
+ // EDITOR: foto, citação, autor e cargo (quando a receita traz) são caminhos da seção. A citação é
8
+ // um `Editable.Slot`: as aspas curvas ficam no render, fora do valor, para o lojista não ter de
9
+ // digitá-las (e não perdê-las) ao trocar a frase. A foto continua passando pelo `Foto` (otimizador
10
+ // do Next): o `Editable.Slot` só lhe entrega o src/alt do lojista e os atributos de seleção.
11
+ import { Editable } from "@/lib/editable";
12
+ import type { SectionComponentProps } from "./registry";
13
+ import { Foto } from "@/components/ui/foto";
14
+
15
+ export function QuoteBannerSection({ sectionProps = {} }: SectionComponentProps) {
16
+ const {
17
+ quote = "A gente montou essa loja do jeito que gostaria de comprar: produto honesto, entrega cuidada e zero enrolação.",
18
+ author = "Time da loja",
19
+ role = "",
20
+ image = "/brand/ph/photo-b.svg",
21
+ } = sectionProps as Record<string, string>;
22
+ return (
23
+ <div className="reveal mt-[52px] bg-[var(--store-chrome-bg,#18181B)]">
24
+ <div className="mx-auto grid max-w-[var(--container-max,1240px)] items-stretch md:grid-cols-[44fr_56fr]">
25
+ {/* TODO: trocar por foto real (fundador/bastidor/produto) */}
26
+ <Editable.Slot path="imagem" type="image" fallback={{ src: image, alt: "" }} label="Foto">
27
+ {(v, attrs, ref) => <Foto ref={ref} attrs={attrs} src={v.src} alt={v.alt ?? ""} width={1240} height={840} sizes="(min-width: 768px) 50vw, 100vw" className="h-full max-h-[420px] w-full object-cover md:max-h-none" />}
28
+ </Editable.Slot>
29
+ <div className="flex flex-col justify-center gap-5 px-6 py-10 sm:px-10 sm:py-14">
30
+ <Editable.Slot path="citacao" type="text" fallback={quote} label="Citação">
31
+ {(v, attrs, ref, estilo) => (
32
+ <p ref={ref} className="font-display text-[22px] font-bold leading-[1.35] text-[var(--store-chrome-text,#ffffff)] sm:text-[26px]" style={estilo} {...attrs}>
33
+ “{v}”
34
+ </p>
35
+ )}
36
+ </Editable.Slot>
37
+ <div className="text-[14px] text-[var(--store-chrome-muted)]">
38
+ <Editable.Text path="autor" fallback={author} label="Quem assina a citação" className="font-bold text-[var(--store-chrome-text,#ffffff)]" />
39
+ {role ? (
40
+ <>
41
+ {" · "}
42
+ <Editable.Text path="cargo" fallback={role} label="Cargo de quem assina" />
43
+ </>
44
+ ) : null}
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,186 @@
1
+ // Registry das seções da home — o mapa nome→componente que a receita
2
+ // (components/home/home-recipe.ts) referencia.
3
+ //
4
+ // ESTE ARQUIVO NÃO DESCREVE AS SEÇÕES, só declara os nomes válidos (a union tipada que faz o
5
+ // tsc reprovar nome inventado). Pra saber O QUE cada seção é, quais variantes tem e quando
6
+ // cabe usar: agents/PADROES.md §2 — é o catálogo, e é mais barato que abrir os 24 componentes.
7
+ //
8
+ // Pra criar uma seção ou variante nova: arquivo em components/home/sections/ + entrada aqui
9
+ // + IDENTIDADE_PADRAO (id, tipo e nome que o lojista vê) + registro no PADROES.md §2.
10
+ import type { CatalogProductItem, CatalogCategory } from "@/components/catalog/catalog-client";
11
+ import type { ResolvedCombo } from "@/lib/enrichment/combos";
12
+ import type { SectionKind } from "@/lib/editable";
13
+ // só o TIPO: lib/vitrine.ts é `server-only`, e `import type` é apagado na compilação. Nenhum
14
+ // módulo de servidor entra no bundle das seções por causa desta linha.
15
+ import type { VitrinesResolvidas } from "@/lib/vitrine";
16
+ import { HeroSection } from "./hero";
17
+ import { TrustBarSection } from "./trust-bar";
18
+ import { CategoryPillsSection } from "./category-pills";
19
+ import { CombosCarouselSection } from "./combos-carousel";
20
+ import { SavingsSection } from "./savings";
21
+ import { ReviewsSection } from "./reviews";
22
+ import { KitsSection } from "./kits";
23
+ import { TrustStripSection } from "./trust-strip";
24
+ import { NewsletterSection } from "./newsletter";
25
+ import { AttributesMarqueeSection } from "./attributes-marquee";
26
+ import { BenefitsSection } from "./benefits";
27
+ import { StatsGridSection } from "./stats-grid";
28
+ import { QuoteBannerSection } from "./quote-banner";
29
+ import { MediaCardsSection } from "./media-cards";
30
+ import { SpecTableSection } from "./spec-table";
31
+ import { RitualSection } from "./ritual";
32
+ import { FounderStorySection } from "./founder-story";
33
+ import { VideoWallSection } from "./video-wall";
34
+ import { SocialRowSection } from "./social-row";
35
+ import { ReviewsCarouselSection } from "./reviews-carousel";
36
+ import { ComparisonSection } from "./comparison";
37
+ import { ProductShowcaseSection } from "./product-showcase";
38
+ import { PurchaseHeroSection } from "./purchase-hero";
39
+ import { BlocoHtmlSection } from "./bloco-html";
40
+
41
+ /** Dados buscados pelo servidor (app/page.tsx) e entregues a todas as seções. */
42
+ export interface HomeData {
43
+ combos: CatalogProductItem[];
44
+ featured: CatalogProductItem | null;
45
+ bundles: ResolvedCombo[];
46
+ categories: CatalogCategory[];
47
+ combosTitle: string;
48
+ /** Rótulo "R$X" do frete grátis; null = loja sem regra de frete grátis (seções escondem o selo). */
49
+ freeShipLabel: string | null;
50
+ /**
51
+ * EDITOR: as VITRINES já resolvidas, indexadas pelo CAMINHO (`<container>.<id da seção>.vitrine`).
52
+ *
53
+ * O documento do lojista guarda só a ESCOLHA (categoria da Unbox, lista de produtos ou busca);
54
+ * quem busca os produtos é o SERVIDOR (lib/vitrine.ts, `resolverVitrinesDoDocumento`) e os entrega
55
+ * prontos aqui. Cada seção acha os seus DERIVANDO o caminho do próprio escopo (`joinPath(ctx.scope,
56
+ * CAMPO_VITRINE)`), nunca por uma constante escrita à mão, que descolaria da seção no primeiro `id`
57
+ * renomeado.
58
+ *
59
+ * Caminho ausente = o lojista não escolheu, ou a escolha não resolveu em nada (Unbox fora,
60
+ * categoria esvaziada): a seção mostra os produtos do CÓDIGO. Omitido de todo (página que não
61
+ * leu o documento, como /oferta) = idem.
62
+ */
63
+ vitrines?: VitrinesResolvidas;
64
+ }
65
+
66
+ /** Props que toda seção recebe do renderer (CombosHome). */
67
+ export interface SectionComponentProps {
68
+ data: HomeData;
69
+ variant?: string;
70
+ sectionProps?: Record<string, unknown>;
71
+ }
72
+
73
+ export const SECTIONS = {
74
+ hero: HeroSection,
75
+ "trust-bar": TrustBarSection,
76
+ "category-pills": CategoryPillsSection,
77
+ "combos-carousel": CombosCarouselSection,
78
+ savings: SavingsSection,
79
+ reviews: ReviewsSection,
80
+ kits: KitsSection,
81
+ "trust-strip": TrustStripSection,
82
+ newsletter: NewsletterSection,
83
+ // Biblioteca de conversão (portada de uma loja Unbox real em produção, genericizadas):
84
+ "attributes-marquee": AttributesMarqueeSection,
85
+ benefits: BenefitsSection,
86
+ "stats-grid": StatsGridSection,
87
+ "quote-banner": QuoteBannerSection,
88
+ "media-cards": MediaCardsSection,
89
+ "spec-table": SpecTableSection,
90
+ ritual: RitualSection,
91
+ "founder-story": FounderStorySection,
92
+ "video-wall": VideoWallSection,
93
+ "social-row": SocialRowSection,
94
+ "reviews-carousel": ReviewsCarouselSection,
95
+ comparison: ComparisonSection,
96
+ "product-showcase": ProductShowcaseSection,
97
+ "purchase-hero": PurchaseHeroSection,
98
+ // Seção do EDITOR (não vem da receita): o lojista a adiciona pelo "+" e cola o HTML dele dentro.
99
+ // Fica no registry como as outras porque o catálogo (sections/catalogo.ts) só sabe instanciar o que
100
+ // está aqui, e porque o dia em que a receita quiser um bloco de HTML fixo, ele já existe.
101
+ "bloco-html": BlocoHtmlSection,
102
+ } as const;
103
+
104
+ export type SectionName = keyof typeof SECTIONS;
105
+
106
+ export interface RecipeEntry {
107
+ section: SectionName;
108
+ /**
109
+ * EDITOR: identidade ESTÁVEL da seção (ordem, ocultar, e o prefixo de todo caminho de copy dentro
110
+ * dela). Omitido = o id de IDENTIDADE_PADRAO da seção (e "-2", "-3" nas repetidas).
111
+ *
112
+ * CHAVE PRIMÁRIA depois que a loja publica: o documento do lojista aponta para este nome. Renomear,
113
+ * ou reordenar seções repetidas (o que renumera as derivadas), apaga a edição dele em silêncio.
114
+ * Escreva o id à mão nas seções que se repetem na receita.
115
+ */
116
+ id?: string;
117
+ /** Nome que o LOJISTA vê no painel ("Banner principal"). Omitido = o de IDENTIDADE_PADRAO. */
118
+ label?: string;
119
+ /** Tipo da seção no vocabulário fechado da foundation (`SECTION_KINDS`). Omitido = o de IDENTIDADE_PADRAO. */
120
+ kind?: SectionKind;
121
+ /** Variante visual da seção (ver cada componente / PADROES.md). Omitida = default. */
122
+ variant?: string;
123
+ /** Props específicas da seção (ex.: imagens do hero, copy). */
124
+ props?: Record<string, unknown>;
125
+ }
126
+
127
+ /** A receita da home: ordem + variante + props de cada seção. */
128
+ export type HomeRecipe = RecipeEntry[];
129
+
130
+ /**
131
+ * A IDENTIDADE de cada seção PARA O EDITOR: o id (prefixo de todo caminho de copy, chave primária
132
+ * depois de publicado), o tipo do vocabulário fechado e o nome que o lojista lê no painel.
133
+ *
134
+ * Mora AQUI, e não em cada entrada da receita, porque `components/home/home-recipe.ts` é REESCRITO
135
+ * pelo create-unbox-store a partir de src/presets.js: uma receita que exigisse id/kind/label em cada
136
+ * linha nasceria incompleta em toda loja gerada. A receita pode sobrescrever qualquer um dos três
137
+ * (é o que fazer numa seção repetida ou num bloco com nome próprio, "Tradicional", "Chocolate").
138
+ *
139
+ * Ids em português, do ponto de vista do lojista, nunca o nome de código: o id vaza no tooltip do
140
+ * painel e no caminho de todo texto da seção ("banner", nunca "hero"; ver README do editor,
141
+ * "Vocabulário: nunca hero"). NÃO RENOMEIE depois que alguma loja publicou.
142
+ */
143
+ export const IDENTIDADE_PADRAO: Record<SectionName, { id: string; kind: SectionKind; label: string }> = {
144
+ hero: { id: "banner", kind: "banner", label: "Banner principal" },
145
+ "trust-bar": { id: "selos", kind: "beneficios", label: "Selos de confiança" },
146
+ "category-pills": { id: "categorias", kind: "outro", label: "Categorias" },
147
+ "combos-carousel": { id: "vitrine", kind: "vitrine-de-produtos", label: "Vitrine de destaques" },
148
+ savings: { id: "economia", kind: "produto-em-destaque", label: "Economia do combo" },
149
+ reviews: { id: "depoimentos", kind: "depoimentos", label: "Depoimentos" },
150
+ kits: { id: "kits", kind: "vitrine-de-produtos", label: "Kits e combos" },
151
+ "trust-strip": { id: "diferenciais", kind: "beneficios", label: "Diferenciais" },
152
+ newsletter: { id: "newsletter", kind: "newsletter", label: "Newsletter" },
153
+ "attributes-marquee": { id: "faixa-rolante", kind: "texto-rolante", label: "Faixa rolante" },
154
+ benefits: { id: "beneficios", kind: "beneficios", label: "Benefícios" },
155
+ "stats-grid": { id: "numeros", kind: "beneficios", label: "Números e destaques" },
156
+ "quote-banner": { id: "citacao", kind: "sobre-a-marca", label: "Citação" },
157
+ "media-cards": { id: "cards", kind: "galeria", label: "Cards de imagem" },
158
+ "spec-table": { id: "ficha", kind: "outro", label: "Ficha técnica" },
159
+ ritual: { id: "como-funciona", kind: "como-funciona", label: "Como funciona" },
160
+ "founder-story": { id: "historia", kind: "sobre-a-marca", label: "Nossa história" },
161
+ "video-wall": { id: "videos", kind: "video", label: "Vídeos" },
162
+ "social-row": { id: "comunidade", kind: "galeria", label: "Comunidade" },
163
+ "reviews-carousel": { id: "avaliacoes", kind: "depoimentos", label: "Avaliações" },
164
+ comparison: { id: "comparativo", kind: "comparacao", label: "Comparativo" },
165
+ "product-showcase": { id: "grade", kind: "vitrine-de-produtos", label: "Grade de produtos" },
166
+ "purchase-hero": { id: "compra", kind: "produto-em-destaque", label: "Bloco de compra" },
167
+ "bloco-html": { id: "bloco-html", kind: "outro", label: "Bloco de HTML" },
168
+ };
169
+
170
+ /**
171
+ * Cada entrada da receita com a identidade completa (id, kind, label) que o editor usa: o que a
172
+ * receita declara vence; o que ela omite sai de IDENTIDADE_PADRAO. Id derivado ganha "-2", "-3"
173
+ * quando a mesma seção se repete, na ordem em que aparece.
174
+ */
175
+ export function withIds(recipe: HomeRecipe): (RecipeEntry & { id: string; kind: SectionKind; label: string })[] {
176
+ const vistos = new Map<string, number>();
177
+ return recipe.map((e) => {
178
+ const padrao = IDENTIDADE_PADRAO[e.section];
179
+ const kind = e.kind ?? padrao.kind;
180
+ const label = e.label ?? padrao.label;
181
+ if (e.id) return { ...e, id: e.id, kind, label };
182
+ const n = (vistos.get(padrao.id) ?? 0) + 1;
183
+ vistos.set(padrao.id, n);
184
+ return { ...e, id: n === 1 ? padrao.id : `${padrao.id}-${n}`, kind, label };
185
+ });
186
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ // Carrossel de reviews em card bordado com pílulas de tema (portado do carrossel de clube de uma loja real em produção).
4
+ // Reviews REAIS via receita (props.reviews). Sem elas a seção não renderiza — não existe
5
+ // default: "Cliente A/B/C" fabricado foi ao ar em loja real.
6
+ //
7
+ // EDITOR: só o TÍTULO é caminho da seção. As avaliações são dado de terceiros (quem fala é o
8
+ // cliente, não a marca), e a regra §8 do README do editor as deixa fora dos primitivos; os filtros
9
+ // e as setas são interface. O carrossel mostra uma avaliação por vez, então nem uma lista editável
10
+ // caberia aqui sem trocar o desenho da seção. O miolo sai do gate por `data-editor-ignore`.
11
+ import * as React from "react";
12
+ import { CaretLeft, CaretRight } from "@phosphor-icons/react/dist/ssr";
13
+ import { Editable } from "@/lib/editable";
14
+ import { Stars } from "./stars";
15
+ import type { SectionComponentProps } from "./registry";
16
+
17
+ type Review = { name: string; quote: string; tag?: string; stars?: number };
18
+
19
+ export function ReviewsCarouselSection({ sectionProps = {} }: SectionComponentProps) {
20
+ const { title = "O que dizem nossos clientes" } = sectionProps as Record<string, string>;
21
+ const reviews = (sectionProps.reviews as Review[] | undefined) ?? [];
22
+ const tags = [...new Set(reviews.map((r) => r.tag).filter(Boolean))] as string[];
23
+ const [active, setActive] = React.useState<string | null>(null);
24
+ const [idx, setIdx] = React.useState(0);
25
+ const visible = active ? reviews.filter((r) => r.tag === active) : reviews;
26
+ const current = visible[Math.min(idx, visible.length - 1)] ?? reviews[0];
27
+
28
+ const step = (d: number) => setIdx((i) => (i + d + visible.length) % visible.length);
29
+
30
+ if (!reviews.length) return null;
31
+ const comNota = reviews.filter((r) => r.stars != null);
32
+ const media = comNota.length ? comNota.reduce((s, r) => s + (r.stars as number), 0) / comNota.length : null;
33
+
34
+ return (
35
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[var(--section-gap,52px)] sm:px-6">
36
+ <div className="rounded-2xl border-2 border-[var(--store-primary,#18181B)]/25 bg-[var(--store-surface)] p-6 sm:p-9">
37
+ <div className="flex flex-wrap items-center justify-between gap-3">
38
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[24px] font-extrabold" />
39
+ <Stars n={media} className="text-[16px] text-[var(--store-cta-dark)]" />
40
+ </div>
41
+ <div data-editor-ignore="">
42
+ {tags.length > 1 && (
43
+ <div className="mt-5 flex flex-wrap gap-2">
44
+ {[null, ...tags].map((t) => (
45
+ <button key={t ?? "todos"} type="button" onClick={() => { setActive(t); setIdx(0); }} className={`rounded-full border px-4 py-1.5 text-[13px] font-semibold transition-colors ${active === t ? "border-[var(--store-primary,#18181B)] bg-[var(--store-primary,#18181B)] text-white" : "border-[var(--store-line-2)] bg-white text-[var(--store-ink-2)] hover:border-[var(--store-primary,#18181B)]"}`}>{t ?? "todos"}</button>
46
+ ))}
47
+ </div>
48
+ )}
49
+ <div className="mt-6 flex items-center gap-4">
50
+ <button type="button" onClick={() => step(-1)} aria-label="Anterior" className="hidden h-[42px] w-[42px] shrink-0 items-center justify-center rounded-full border-[1.5px] border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)] sm:flex"><CaretLeft weight="bold" /></button>
51
+ <div className="min-h-[110px] flex-1 rounded-xl bg-white p-6">
52
+ <p className="text-[15.5px] leading-[1.6] text-[var(--store-ink-2)]">“{current.quote}”</p>
53
+ <div className="mt-3 text-[13.5px] font-bold text-[var(--store-ink)]">{current.name}</div>
54
+ </div>
55
+ <button type="button" onClick={() => step(1)} aria-label="Próximo" className="hidden h-[42px] w-[42px] shrink-0 items-center justify-center rounded-full border-[1.5px] border-[var(--store-line-2)] bg-white hover:border-[var(--store-primary,#18181B)] sm:flex"><CaretRight weight="bold" /></button>
56
+ </div>
57
+ <div className="mt-4 flex justify-center gap-1.5">
58
+ {visible.map((_, i) => (
59
+ <button key={i} type="button" onClick={() => setIdx(i)} aria-label={`Review ${i + 1}`} className={`h-2 w-2 rounded-full ${i === idx ? "bg-[var(--store-primary,#18181B)]" : "bg-[var(--store-line-2)]"}`} />
60
+ ))}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ // REVIEWS — depoimentos. Variantes: "grid" (3 colunas, default) e "faixa" (linha única
4
+ // horizontal, mais discreta — combina com estilos minimalistas).
5
+ // SÓ com depoimentos reais, vindos da receita (props.reviews, preenchidos no briefing a partir
6
+ // de avaliações que existem). Sem eles, a seção não renderiza. Não há default: a foundation
7
+ // publicava "Cliente A/B/C" com selo de compra verificada, e isso foi ao ar em loja real.
8
+ //
9
+ // EDITOR: só o TÍTULO é caminho da seção. As avaliações são dado de terceiros (quem fala é o
10
+ // cliente, não a marca) e a nota média é número vindo de dados: a regra §8 do README do editor
11
+ // os deixa fora dos primitivos, e a lista sai do gate por `data-editor-ignore`.
12
+ import { SealCheck } from "@phosphor-icons/react/dist/ssr";
13
+ import { Star } from "@phosphor-icons/react/dist/ssr";
14
+ import { Editable } from "@/lib/editable";
15
+ import { Stars } from "./stars";
16
+ import type { SectionComponentProps } from "./registry";
17
+
18
+ export interface HomeReview { name: string; quote: string; stars?: number; verified?: boolean }
19
+
20
+ export function ReviewsSection({ variant = "grid", sectionProps = {} }: SectionComponentProps) {
21
+ const REVIEWS = (sectionProps.reviews as HomeReview[] | undefined) ?? [];
22
+ const { title = "O que dizem nossos clientes" } = sectionProps as Record<string, string>;
23
+ if (!REVIEWS.length) return null;
24
+ const comNota = REVIEWS.filter((r) => r.stars != null);
25
+ const media = comNota.length ? (comNota.reduce((s, r) => s + (r.stars as number), 0) / comNota.length).toFixed(1).replace(".", ",") : null;
26
+ if (variant === "faixa") {
27
+ return (
28
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[42px] sm:px-6">
29
+ <div className="flex gap-[18px] overflow-x-auto rounded-xl border border-[var(--store-line)] bg-[var(--store-surface)] px-6 py-5 [scrollbar-width:thin]" data-editor-ignore="">
30
+ {REVIEWS.map((r) => (
31
+ <div key={r.name} className="flex min-w-[300px] flex-1 items-start gap-3.5">
32
+ <Stars n={r.stars} className="mt-0.5 flex text-[13px] text-[var(--store-cta-dark)]" />
33
+ <div>
34
+ <p className="text-[14px] leading-[1.5] text-[var(--store-ink-2)]">“{r.quote}”</p>
35
+ <div className="mt-1.5 text-[12.5px] font-bold text-[var(--store-muted)]">{r.name}</div>
36
+ </div>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ // grid (default)
45
+ return (
46
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[42px] sm:px-6">
47
+ <div className="mb-[18px] flex items-end justify-between">
48
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[26px] font-extrabold" />
49
+ {media && <div className="flex items-center gap-2 text-sm font-semibold text-[var(--store-muted)]" data-editor-ignore=""><Star weight="fill" className="text-[17px] text-[var(--store-cta-dark)]" /><b className="text-[var(--store-ink)]">{media}</b></div>}
50
+ </div>
51
+ <div className="grid gap-[18px] md:grid-cols-3" data-editor-ignore="">
52
+ {REVIEWS.map((r) => (
53
+ <div key={r.name} className="flex flex-col gap-3.5 store-card rounded-xl p-6">
54
+ <Stars n={r.stars} className="flex text-[15px] text-[var(--store-cta-dark)]" />
55
+ <p className="flex-1 text-[15px] leading-[1.55] text-[var(--store-ink-2)]">“{r.quote}”</p>
56
+ <div className="flex items-center gap-3">
57
+ <span className="flex h-11 w-11 items-center justify-center rounded-full border-2 border-[var(--store-primary-soft,#F1F1F3)] bg-[var(--store-chrome-bg,#18181B)] text-sm font-extrabold text-white">{r.name[0]}</span>
58
+ <div>
59
+ <div className="text-sm font-bold text-[var(--store-ink)]">{r.name}</div>
60
+ {r.verified && <div className="mt-0.5 flex items-center gap-1.5 text-xs font-semibold text-[var(--store-primary,#18181B)]"><SealCheck weight="fill" />Compra verificada</div>}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ );
68
+ }