@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,96 @@
1
+ "use client";
2
+
3
+ // "Como usar / o ritual": crossfade automático de fotos + checklist com ícones + CTA.
4
+ // Portado de loja real em produção e genericizado. Conteúdo via receita; defaults neutros.
5
+ //
6
+ // EDITOR: selo, título e cada foto do crossfade são caminhos da seção (`foto-1`, `foto-2`…: a
7
+ // quantidade de fotos é da receita, não é lista); os passos são uma LISTA editável
8
+ // (`passos.passo-N`, com ícone e texto). No crossfade, as fotos que não estão na vez ficam
9
+ // `pointer-events-none`: assim o clique do lojista no editor cai na foto que ele está vendo, e não
10
+ // na última do empilhamento. O botão é um `Editable.Slot` de texto (o elemento já existe e não
11
+ // pode ganhar invólucro dentro do fluxo do botão).
12
+ import * as React from "react";
13
+ import { CheckCircle } from "@phosphor-icons/react/dist/ssr";
14
+ import { Editable, EditableScope } from "@/lib/editable";
15
+ import type { SectionComponentProps } from "./registry";
16
+
17
+ const DEFAULT_PHOTOS = ["/brand/ph/photo-a.svg", "/brand/ph/photo-b.svg", "/brand/ph/photo-c.svg"];
18
+ const DEFAULT_ITEMS = [
19
+ "Escolha o que combina com a sua rotina",
20
+ "Receba em casa, do nosso cuidado pro seu",
21
+ "Use do seu jeito, no seu tempo",
22
+ "Volte quando quiser: recompra em 2 cliques",
23
+ ];
24
+
25
+ export function RitualSection({ sectionProps = {} }: SectionComponentProps) {
26
+ const {
27
+ badge = "simples assim",
28
+ title = "Do pedido ao seu dia a dia",
29
+ ctaLabel = "Ver destaques",
30
+ } = sectionProps as Record<string, string>;
31
+ const photos = (sectionProps.photos as string[] | undefined) ?? DEFAULT_PHOTOS;
32
+ const items = (sectionProps.items as string[] | undefined) ?? DEFAULT_ITEMS;
33
+ const [idx, setIdx] = React.useState(0);
34
+
35
+ React.useEffect(() => {
36
+ if (photos.length < 2) return;
37
+ const t = setInterval(() => setIdx((i) => (i + 1) % photos.length), 3200);
38
+ return () => clearInterval(t);
39
+ }, [photos.length]);
40
+
41
+ return (
42
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[var(--section-gap,52px)] sm:px-6">
43
+ <div className="grid items-center gap-9 md:grid-cols-2">
44
+ <div className="relative aspect-[4/5] max-h-[520px] w-full overflow-hidden rounded-2xl">
45
+ {photos.map((p, i) => (
46
+ /* TODO: fotos reais de uso/rotina */
47
+ <Editable.Img
48
+ key={p + i}
49
+ path={`foto-${i + 1}`}
50
+ fallback={{ src: p, alt: "" }}
51
+ label={`Foto ${i + 1}`}
52
+ className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-700 ${i === idx ? "opacity-100" : "pointer-events-none opacity-0"}`}
53
+ loading="lazy"
54
+ decoding="async"
55
+ />
56
+ ))}
57
+ </div>
58
+ <div>
59
+ <Editable.Text path="selo" fallback={badge} label="Selo acima do título" className="rounded-full bg-[var(--store-primary-soft,#F1F1F3)] px-4 py-1.5 text-[12.5px] font-bold uppercase tracking-[0.6px] text-[var(--store-primary,#18181B)]" />
60
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display mt-4 text-[28px] font-extrabold leading-[1.15]" />
61
+ <ul className="mt-6 flex flex-col gap-4">
62
+ <EditableScope path="passos">
63
+ <Editable.Sections nested>
64
+ {items.map((t, i) => (
65
+ <Editable.Section key={t} item id={`passo-${i + 1}`} label={`Passo ${i + 1}`}>
66
+ <li className="flex items-start gap-3">
67
+ {/* a className era do ícone; vai pro invólucro do Icon pra não descolar da linha */}
68
+ <Editable.Icon path="icone" label="Ícone do passo" size={22} className="mt-0.5 shrink-0 text-[22px] text-[var(--store-primary,#18181B)]">
69
+ <CheckCircle weight="fill" />
70
+ </Editable.Icon>
71
+ <Editable.Text path="texto" fallback={t} label="Texto do passo" className="text-[15px] leading-[1.5] text-[var(--store-ink-2)]" />
72
+ </li>
73
+ </Editable.Section>
74
+ ))}
75
+ </Editable.Sections>
76
+ </EditableScope>
77
+ </ul>
78
+ <Editable.Slot path="cta" type="text" fallback={ctaLabel} label="Botão">
79
+ {(v, attrs, ref, estilo) => (
80
+ <button
81
+ ref={ref}
82
+ type="button"
83
+ onClick={() => document.getElementById("destaques")?.scrollIntoView({ behavior: "smooth" })}
84
+ className="font-display mt-7 rounded-full bg-[var(--store-cta,#D97706)] px-7 py-3 text-[15px] font-extrabold text-[var(--store-cta-fg,#1C1207)] transition-colors hover:bg-[var(--store-cta-dark,#B45309)]"
85
+ style={estilo}
86
+ {...attrs}
87
+ >
88
+ {v}
89
+ </button>
90
+ )}
91
+ </Editable.Slot>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ // Card de ECONOMIA — comparativo "preço normal − combo = você economiza".
4
+ // Só aparece quando o produto em destaque tem desconto real (preço antigo do catálogo).
5
+ //
6
+ // EDITOR: título, texto e os rótulos das colunas são copy (caminhos da seção). Preço, economia,
7
+ // porcentagem e o nome do produto são DADO do catálogo: fora dos primitivos (README do editor,
8
+ // §8); a célula com o nome do produto sai do gate por `data-editor-ignore`.
9
+ import { Editable } from "@/lib/editable";
10
+ import { formatBRL } from "@/lib/format";
11
+ import type { SectionComponentProps } from "./registry";
12
+
13
+ export function SavingsSection({ data }: SectionComponentProps) {
14
+ const { featured } = data;
15
+ const featSave = featured?.oldPrice != null ? featured.oldPrice - featured.price : null;
16
+ if (!featured || featSave == null || featSave <= 0) return null;
17
+
18
+ return (
19
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-10 sm:px-6">
20
+ <div className="store-card rounded-xl p-7">
21
+ <div className="grid items-center gap-6 lg:grid-cols-[1.1fr_auto_auto_auto]">
22
+ <div>
23
+ <Editable.Text as="h2" path="titulo" fallback="Economia que faz diferença" label="Título" className="font-display text-[22px] font-extrabold" />
24
+ <Editable.Text as="p" path="texto" fallback="Compare e veja como compensa levar o nosso combo." label="Texto" className="mt-1.5 max-w-[280px] text-sm text-[var(--store-muted)]" />
25
+ </div>
26
+ <div className="text-center">
27
+ <Editable.Text as="div" path="rotulo-normal" fallback="Preço normal" label="Rótulo do preço normal" className="text-[12.5px] font-semibold leading-[1.3] text-[var(--store-muted)]" />
28
+ <div className="font-display mt-1.5 text-[22px] font-bold text-[var(--store-muted)] line-through">{featured.displayOld}</div>
29
+ </div>
30
+ <div className="flex items-center gap-6">
31
+ <span className="text-xl font-bold text-[var(--store-faint)]">−</span>
32
+ <div className="text-center">
33
+ <div className="text-[12.5px] font-semibold text-[var(--store-muted)]" data-editor-ignore="">No {featured.title}</div>
34
+ <div className="font-display mt-1.5 text-[22px] font-extrabold text-[var(--store-primary,#18181B)]">{featured.displayPrice}</div>
35
+ </div>
36
+ <span className="text-xl font-bold text-[var(--store-faint)]">=</span>
37
+ </div>
38
+ <div className="rounded-lg bg-[var(--store-cta,#D97706)] px-6 py-4 text-center lg:justify-self-end">
39
+ <Editable.Text as="div" path="rotulo-economia" fallback="Você economiza" label="Rótulo da economia" className="text-[12.5px] font-bold text-[var(--store-cta-fg,#1C1207)]" />
40
+ <div className="font-display mt-1 text-[26px] font-extrabold leading-[1.1] text-[var(--store-cta-fg,#1C1207)]">{formatBRL(featSave)}</div>
41
+ {featured.offPct != null && <div className="mt-0.5 text-xs font-extrabold text-[var(--store-cta-fg,#1C1207)]">{featured.offPct}% OFF</div>}
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ // Fileira de comunidade/creators: cards de foto com badge de @handle (portado de loja real em produção).
4
+ // ⚠️ GENTE REAL: os defaults são placeholders neutros sem nome/seguidores. O agente troca
5
+ // por pessoas reais (com autorização de uso de imagem) via receita — números de seguidores
6
+ // só se forem verdadeiros.
7
+ //
8
+ // EDITOR: título e subtítulo são caminhos da seção; as pessoas são uma LISTA editável
9
+ // (`pessoas.pessoa-N`, com foto, @ e, quando a receita traz, nome e detalhe). O invólucro do item é
10
+ // display:contents, então quem continua sendo a célula do grid é o próprio card. A foto continua
11
+ // passando pelo `Foto` (otimizador do Next): o `Editable.Slot` só lhe entrega o src/alt do lojista
12
+ // e os atributos de seleção.
13
+ import { Editable, EditableScope } from "@/lib/editable";
14
+ import type { SectionComponentProps } from "./registry";
15
+ import { Foto } from "@/components/ui/foto";
16
+
17
+ type Person = { image: string; handle?: string; name?: string; meta?: string };
18
+
19
+ const DEFAULT_PEOPLE: Person[] = [
20
+ { image: "/brand/ph/poster-a.svg", handle: "@suamarca" },
21
+ { image: "/brand/ph/poster-b.svg", handle: "@suamarca" },
22
+ { image: "/brand/ph/poster-c.svg", handle: "@suamarca" },
23
+ { image: "/brand/ph/poster-a.svg", handle: "@suamarca" },
24
+ ];
25
+
26
+ export function SocialRowSection({ sectionProps = {} }: SectionComponentProps) {
27
+ const {
28
+ title = "A comunidade da marca",
29
+ subtitle = "Quem cria e compartilha com a gente.",
30
+ } = sectionProps as Record<string, string>;
31
+ const people = (sectionProps.people as Person[] | undefined) ?? DEFAULT_PEOPLE;
32
+ return (
33
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[var(--section-gap,52px)] sm:px-6">
34
+ <div className="mb-[18px]">
35
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[28px] font-extrabold leading-[1.15]" />
36
+ <Editable.Text as="p" path="subtitulo" fallback={subtitle} label="Subtítulo" className="mt-1 text-sm text-[var(--store-muted)]" />
37
+ </div>
38
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
39
+ <EditableScope path="pessoas">
40
+ <Editable.Sections nested>
41
+ {people.map((p, i) => (
42
+ <Editable.Section key={i} item id={`pessoa-${i + 1}`} label={`Pessoa ${i + 1}`}>
43
+ <div className="overflow-hidden store-card rounded-2xl">
44
+ <div className="relative">
45
+ {/* TODO: fotos reais, com autorização de uso */}
46
+ <Editable.Slot path="imagem" type="image" fallback={{ src: p.image, alt: "" }} label="Foto">
47
+ {(v, attrs, ref) => <Foto ref={ref} attrs={attrs} src={v.src} alt={v.alt ?? ""} width={600} height={750} sizes="(min-width: 1024px) 300px, 50vw" className="aspect-[4/5] w-full object-cover" />}
48
+ </Editable.Slot>
49
+ {p.handle && <Editable.Text path="arroba" fallback={p.handle} label="Perfil (@)" className="absolute bottom-2.5 left-2.5 rounded-full bg-black/60 px-3 py-1 text-[12px] font-semibold text-white" />}
50
+ </div>
51
+ {(p.name || p.meta) && (
52
+ <div className="px-4 py-3">
53
+ {p.name && <Editable.Text as="div" path="nome" fallback={p.name} label="Nome" className="text-[14px] font-bold text-[var(--store-ink)]" />}
54
+ {p.meta && <Editable.Text as="div" path="detalhe" fallback={p.meta} label="Detalhe" className="text-[12px] text-[var(--store-muted)]" />}
55
+ </div>
56
+ )}
57
+ </div>
58
+ </Editable.Section>
59
+ ))}
60
+ </Editable.Sections>
61
+ </EditableScope>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ // Faixa de especificações: texto+CTA / tabela de linhas / foto — com modal de tabela
4
+ // completa. Serve pra ficha técnica, composição, medidas — e tabela nutricional, se for alimento.
5
+ // Conteúdo SÓ via receita (props.rows): sem linhas, a seção não renderiza. "Origem: Brasil" e
6
+ // "garantia" não são dados da foundation, são do produto.
7
+ //
8
+ // EDITOR: título, texto, botão e foto são caminhos da seção; as linhas da tabela são uma LISTA
9
+ // editável (`linhas.linha-N`, com rótulo e valor). O divisor entre linhas é uma regra do CONTAINER
10
+ // (`[&>*+*>div]:border-t`): o invólucro de cada item é display:contents e é ele o irmão no DOM, então
11
+ // a regra "toda linha depois da primeira" continua certa depois que o lojista reordena ou oculta.
12
+ // O modal é interface transitória (só existe aberto): as linhas dele LEEM o documento (mesma
13
+ // ordem, mesmas ocultas, mesmos valores da lista) sem registrar um segundo ponto, como o eco da
14
+ // faixa rolante. Se a receita passar `modalRows` separadas, elas são dado da receita e ficam literais.
15
+ import * as React from "react";
16
+ import { X } from "@phosphor-icons/react/dist/ssr";
17
+ import { Editable, EditableScope, joinPath, orderSections, resolveValue, useEditableContext } from "@/lib/editable";
18
+ import type { SectionComponentProps } from "./registry";
19
+
20
+ type Row = { label: string; value: string };
21
+
22
+ const ESCOPO_LINHAS = "linhas";
23
+ const idDaLinha = (i: number) => `linha-${i + 1}`;
24
+ const CLASSE_LINHA = "flex items-center justify-between gap-4 px-4 py-2.5 text-[13.5px]";
25
+ const CLASSE_ROTULO = "font-semibold text-[var(--store-ink-2)]";
26
+ const CLASSE_VALOR = "text-right text-[var(--store-muted)]";
27
+ const CLASSE_DIVISOR = "[&>*+*>div]:border-t [&>*+*>div]:border-[var(--store-line)]";
28
+
29
+ /**
30
+ * As linhas como o lojista as publicou (ordem, ocultas, cópias e valores), lidas do documento sem
31
+ * registrar ponto editável: é o que o modal mostra.
32
+ */
33
+ function useLinhasPublicadas(rows: Row[]): Row[] {
34
+ const ctx = useEditableContext();
35
+ const escopo = joinPath(ctx.scope, ESCOPO_LINHAS);
36
+ const ids = rows.map((_, i) => idDaLinha(i));
37
+ const estado = ctx.doc.sections[escopo];
38
+ const { visible } = orderSections(ids, estado);
39
+ const copias = estado?.clones ?? {};
40
+ return visible.flatMap((id) => {
41
+ const base = rows[ids.indexOf(copias[id] ?? id)];
42
+ if (!base) return [];
43
+ return [{
44
+ label: resolveValue(ctx.doc, `${escopo}.${id}.rotulo`, base.label),
45
+ value: resolveValue(ctx.doc, `${escopo}.${id}.valor`, base.value),
46
+ }];
47
+ });
48
+ }
49
+
50
+ export function SpecTableSection({ sectionProps = {} }: SectionComponentProps) {
51
+ const {
52
+ title = "Ficha completa, sem letra miúda",
53
+ text = "",
54
+ ctaLabel = "Ver ficha completa",
55
+ image = "/brand/ph/photo-b.svg",
56
+ modalTitle = "Ficha completa",
57
+ } = sectionProps as Record<string, string>;
58
+ const rows = ((sectionProps.rows as Row[] | undefined) ?? []).filter((r) => r?.label && r?.value);
59
+ const modalRows = sectionProps.modalRows as Row[] | undefined;
60
+ const linhasPublicadas = useLinhasPublicadas(rows);
61
+ const linhasDoModal = modalRows ?? linhasPublicadas;
62
+ const [open, setOpen] = React.useState(false);
63
+ if (rows.length === 0) return null;
64
+
65
+ return (
66
+ <div className="mt-[52px] bg-[var(--store-surface-2)]">
67
+ <div className="mx-auto grid max-w-[var(--container-max,1240px)] items-center gap-8 px-4 py-12 sm:px-6 md:grid-cols-[1fr_1.1fr_0.9fr]">
68
+ <div>
69
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[26px] font-extrabold leading-[1.2]" />
70
+ {text && <Editable.Text as="p" path="texto" fallback={text} label="Texto" multiline className="mt-2.5 text-[14.5px] leading-[1.55] text-[var(--store-muted)]" />}
71
+ <Editable.Slot path="cta" type="text" fallback={ctaLabel} label="Botão que abre a ficha">
72
+ {(v, attrs, ref, estilo) => (
73
+ <button ref={ref} type="button" onClick={() => setOpen(true)} className="font-display mt-5 rounded-full bg-[var(--store-primary,#18181B)] px-6 py-2.5 text-[14px] font-bold text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)]" style={estilo} {...attrs}>
74
+ {v}
75
+ </button>
76
+ )}
77
+ </Editable.Slot>
78
+ </div>
79
+ <div className={`rounded-2xl border border-[var(--store-line-2)] bg-white p-1.5 ${CLASSE_DIVISOR}`}>
80
+ <EditableScope path={ESCOPO_LINHAS}>
81
+ <Editable.Sections nested>
82
+ {rows.map((r, i) => (
83
+ <Editable.Section key={i} item id={idDaLinha(i)} label={`Linha ${i + 1}`}>
84
+ <div className={CLASSE_LINHA}>
85
+ <Editable.Text path="rotulo" fallback={r.label} label="Nome da linha" className={CLASSE_ROTULO} />
86
+ <Editable.Text path="valor" fallback={r.value} label="Valor da linha" className={CLASSE_VALOR} />
87
+ </div>
88
+ </Editable.Section>
89
+ ))}
90
+ </Editable.Sections>
91
+ </EditableScope>
92
+ </div>
93
+ {/* TODO: foto real do produto */}
94
+ <Editable.Img path="imagem" fallback={{ src: image, alt: "" }} label="Foto" className="hidden h-full max-h-[320px] w-full rounded-2xl object-cover md:block" loading="lazy" decoding="async" />
95
+ </div>
96
+
97
+ {open && (
98
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onClick={() => setOpen(false)}>
99
+ <div className="max-h-[85vh] w-full max-w-[560px] overflow-y-auto rounded-2xl bg-white p-6" onClick={(e) => e.stopPropagation()}>
100
+ <div className="mb-4 flex items-center justify-between">
101
+ <h3 className="font-display text-[20px] font-extrabold">{modalTitle}</h3>
102
+ <button type="button" onClick={() => setOpen(false)} aria-label="Fechar" className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--store-line-2)] hover:bg-[var(--store-surface)]"><X weight="bold" /></button>
103
+ </div>
104
+ <div className="rounded-xl border border-[var(--store-line-2)]">
105
+ {linhasDoModal.map((r, i) => (
106
+ <div key={i} className={`${CLASSE_LINHA} ${i > 0 ? "border-t border-[var(--store-line)]" : ""}`}>
107
+ <span className={CLASSE_ROTULO}>{r.label}</span>
108
+ <span className={CLASSE_VALOR}>{r.value}</span>
109
+ </div>
110
+ ))}
111
+ </div>
112
+ </div>
113
+ </div>
114
+ )}
115
+ </div>
116
+ );
117
+ }
@@ -0,0 +1,12 @@
1
+ import { Star } from "@phosphor-icons/react/dist/ssr";
2
+
3
+ /** Estrelas de avaliação. `n` ausente → não renderiza: nota que a loja não tem não aparece. */
4
+ export function Stars({ n, className = "" }: { n?: number | null; className?: string }) {
5
+ if (n == null || Number.isNaN(n)) return null;
6
+ const k = Math.max(0, Math.min(5, Math.round(n)));
7
+ return (
8
+ <span className={`flex ${className}`} aria-label={`${k} de 5 estrelas`}>
9
+ {Array.from({ length: 5 }, (_, i) => <Star key={i} weight={i < k ? "fill" : "regular"} />)}
10
+ </span>
11
+ );
12
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ // Grid de destaques com imagem circular + texto (na loja de referência: estatísticas de eficácia).
4
+ // ⚠️ NÚMERO SÓ COM DADO REAL: os defaults são qualitativos de propósito. Se a marca tiver
5
+ // pesquisa/estatística de verdade, a receita passa `stats` com os valores reais e a fonte.
6
+ //
7
+ // EDITOR: o título é caminho da seção; os cards são uma LISTA editável (`destaques.destaque-N`) com
8
+ // imagem, número (quando a receita traz), texto e fonte. Número e fonte só registram caminho quando
9
+ // a receita os passa: não abrimos ponto de "número" numa seção que por regra não inventa número.
10
+ // A imagem continua passando pelo `Foto` (otimizador do Next): o `Editable.Slot` só lhe entrega o
11
+ // src/alt do lojista e os atributos de seleção.
12
+ import { Editable, EditableScope } from "@/lib/editable";
13
+ import type { SectionComponentProps } from "./registry";
14
+ import { Foto } from "@/components/ui/foto";
15
+
16
+ type Stat = { value?: string; text: string; image?: string; source?: string };
17
+
18
+ const DEFAULT_STATS: Stat[] = [
19
+ { text: "Clientes que voltam pra segunda compra", image: "/brand/ph/avatar-a.svg" },
20
+ { text: "Entrega acompanhada do pedido à porta", image: "/brand/ph/avatar-b.svg" },
21
+ { text: "Troca simples se algo não ficar certo", image: "/brand/ph/avatar-c.svg" },
22
+ { text: "Atendimento que responde de verdade", image: "/brand/ph/avatar-d.svg" },
23
+ ];
24
+
25
+ export function StatsGridSection({ sectionProps = {} }: SectionComponentProps) {
26
+ const { title = "O que sustenta a nossa promessa" } = sectionProps as Record<string, string>;
27
+ const stats = (sectionProps.stats as Stat[] | undefined) ?? DEFAULT_STATS;
28
+ return (
29
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[var(--section-gap,52px)] sm:px-6">
30
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display mx-auto max-w-[560px] text-center text-[28px] font-extrabold leading-[1.15]" />
31
+ <div className="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
32
+ {/* o invólucro do item é display:contents: quem continua sendo a célula do grid é o card */}
33
+ <EditableScope path="destaques">
34
+ <Editable.Sections nested>
35
+ {stats.map((s, i) => (
36
+ <Editable.Section key={i} item id={`destaque-${i + 1}`} label={`Destaque ${i + 1}`}>
37
+ <div className="flex flex-col items-center gap-4 store-card rounded-2xl px-5 py-7 text-center">
38
+ {s.image && (
39
+ <Editable.Slot path="imagem" type="image" fallback={{ src: s.image, alt: "" }} label="Imagem do destaque">
40
+ {(v, attrs, ref) => <Foto ref={ref} attrs={attrs} src={v.src} alt={v.alt ?? ""} width={160} height={160} sizes="80px" className="h-20 w-20 rounded-full object-cover" />}
41
+ </Editable.Slot>
42
+ )}
43
+ {s.value && <Editable.Text as="div" path="valor" fallback={s.value} label="Número do destaque" className="font-display text-[34px] font-extrabold leading-none text-[var(--store-primary,#18181B)]" />}
44
+ <Editable.Text as="p" path="texto" fallback={s.text} label="Texto do destaque" multiline className="text-[14px] font-medium leading-[1.5] text-[var(--store-ink-2)]" />
45
+ {s.source && <Editable.Text as="div" path="fonte" fallback={s.source} label="Fonte do número" className="text-[11px] text-[var(--store-faint)]" />}
46
+ </div>
47
+ </Editable.Section>
48
+ ))}
49
+ </Editable.Sections>
50
+ </EditableScope>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ // TRUST BAR clara — selos de confiança logo abaixo do hero. Os itens vêm da RECEITA
4
+ // (props.items: [{ icon?, text }]); o selo de frete grátis entra sozinho quando a loja tem a
5
+ // regra real (store-config). Sem itens e sem frete grátis, a seção não renderiza: "Entrega
6
+ // rápida para todo o Brasil" e "Satisfação garantida" são promessas do lojista, não da foundation.
7
+ //
8
+ // EDITOR: os selos são uma LISTA editável (ícone e texto por item; o lojista reordena, oculta e
9
+ // duplica). O selo de frete tem id de PAPEL (`frete`) porque só existe quando a configuração
10
+ // existe; os da receita são posicionais (`selo-N`). O VALOR do frete (R$X) fica fora do primitivo:
11
+ // é configuração da loja, não copy (README do editor, §8). O invólucro do item é display:contents,
12
+ // então quem continua sendo a célula do grid é o próprio <div> do selo.
13
+ import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
14
+ import { Truck, Lightning, LockSimple, SealCheck, Package, Heart, Clock, Shield, CreditCard, ChatCircle } from "@phosphor-icons/react/dist/ssr";
15
+ import { Editable } from "@/lib/editable";
16
+ import type { SectionComponentProps } from "./registry";
17
+
18
+ export const TRUST_ICONS: Record<string, PhosphorIcon> = {
19
+ truck: Truck, lightning: Lightning, lock: LockSimple, seal: SealCheck, package: Package,
20
+ heart: Heart, clock: Clock, shield: Shield, card: CreditCard, chat: ChatCircle,
21
+ };
22
+ export type TrustItem = { icon?: string; text: string };
23
+
24
+ type Selo = { id: string; label: string; icon: PhosphorIcon; text: string; /** o selo de frete: o valor vem da configuração e fica fora do texto editável */ frete?: boolean };
25
+
26
+ export function TrustBarSection({ data, sectionProps = {} }: SectionComponentProps) {
27
+ const custom = (sectionProps.items as TrustItem[] | undefined) ?? [];
28
+ const items: Selo[] = [
29
+ ...(data.freeShipLabel ? [{ id: "frete", label: "Selo de frete grátis", icon: Truck, text: "Frete Grátis acima de", frete: true }] : []),
30
+ ...custom.filter((t) => t?.text).map((t, i) => ({ id: `selo-${i + 1}`, label: `Selo ${i + 1}`, icon: (t.icon && TRUST_ICONS[t.icon]) || SealCheck, text: t.text })),
31
+ ];
32
+ if (items.length === 0) return null;
33
+ const cols = items.length >= 4 ? "md:grid-cols-4" : items.length === 3 ? "md:grid-cols-3" : "md:grid-cols-2";
34
+ return (
35
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[18px] sm:px-6">
36
+ <div className={`grid grid-cols-2 store-card rounded-2xl ${cols}`}>
37
+ <Editable.Sections nested>
38
+ {items.map((t, i) => (
39
+ <Editable.Section key={t.id} item id={t.id} label={t.label}>
40
+ <div className="flex items-center gap-3 px-[22px] py-[18px]" style={{ borderRight: i < items.length - 1 ? "1px solid var(--store-surface-2)" : "none" }}>
41
+ {/* a className era do ícone; vai para o invólucro do Icon para não descolar da linha */}
42
+ <Editable.Icon path="icone" label="Ícone do selo" size={25} className="shrink-0 text-[25px] text-[var(--store-primary,#18181B)]">
43
+ <t.icon weight="fill" />
44
+ </Editable.Icon>
45
+ <div className="text-[13px] font-semibold leading-[1.35] text-[var(--store-ink-2)]">
46
+ <Editable.Text path="texto" fallback={t.text} label="Texto do selo" />
47
+ {t.frete ? <> {data.freeShipLabel}</> : null}
48
+ </div>
49
+ </div>
50
+ </Editable.Section>
51
+ ))}
52
+ </Editable.Sections>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ // TRUST STRIP escura — faixa de diferenciais sobre o chrome da marca. Itens SÓ via receita
4
+ // (props.items: [{ icon?, text }], ícones em TRUST_ICONS). Sem itens, não renderiza.
5
+ //
6
+ // EDITOR: os diferenciais são uma LISTA editável (`item-N`, com ícone e texto). O invólucro do
7
+ // item é display:contents, então quem continua sendo a célula do grid é o próprio <div>.
8
+ import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
9
+ import { SealCheck } from "@phosphor-icons/react/dist/ssr";
10
+ import { Editable } from "@/lib/editable";
11
+ import type { SectionComponentProps } from "./registry";
12
+ import { TRUST_ICONS, type TrustItem } from "./trust-bar";
13
+
14
+ export function TrustStripSection({ sectionProps = {} }: SectionComponentProps) {
15
+ const items = ((sectionProps.items as TrustItem[] | undefined) ?? []).filter((t) => t?.text);
16
+ if (items.length === 0) return null;
17
+ const cols = items.length >= 4 ? "lg:grid-cols-4" : items.length === 3 ? "lg:grid-cols-3" : "lg:grid-cols-2";
18
+ return (
19
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-10 sm:px-6">
20
+ <div className={`grid grid-cols-1 rounded-xl bg-[var(--store-chrome-bg,#18181B)] sm:grid-cols-2 ${cols}`}>
21
+ <Editable.Sections nested>
22
+ {items.map((t, i) => {
23
+ const Icon: PhosphorIcon = (t.icon && TRUST_ICONS[t.icon]) || SealCheck;
24
+ return (
25
+ <Editable.Section key={i} item id={`item-${i + 1}`} label={`Diferencial ${i + 1}`}>
26
+ <div className="flex items-center gap-3.5 px-[26px] py-6">
27
+ {/* a className era do ícone; vai para o invólucro do Icon para não descolar da linha */}
28
+ <Editable.Icon path="icone" label="Ícone do diferencial" size={30} className="shrink-0 text-[30px] text-[var(--store-cta,#D97706)]">
29
+ <Icon weight="fill" />
30
+ </Editable.Icon>
31
+ <Editable.Text as="div" path="texto" fallback={t.text} label="Texto do diferencial" className="text-[13px] font-medium leading-[1.4] text-[var(--store-chrome-muted)]" />
32
+ </div>
33
+ </Editable.Section>
34
+ );
35
+ })}
36
+ </Editable.Sections>
37
+ </div>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ // Parede de vídeo-depoimentos 9:16 com play inline (portado de loja real em produção).
4
+ // ⚠️ SÓ VÍDEO REAL: itens via receita (props.items: [{ poster, videoUrl, label? }]). Sem itens
5
+ // com videoUrl, a seção não renderiza — moldura vazia nunca vai ao ar.
6
+ //
7
+ // EDITOR: título e subtítulo são caminhos da seção; cada moldura é item de uma LISTA editável
8
+ // (`videos.video-N`) com a capa trocável. O arquivo de vídeo em si continua vindo da receita: o
9
+ // `<video>` só existe enquanto toca, e um ponto que aparece e some não é um ponto do painel.
10
+ import * as React from "react";
11
+ import { Play } from "@phosphor-icons/react/dist/ssr";
12
+ import { Editable, EditableScope } from "@/lib/editable";
13
+ import type { SectionComponentProps } from "./registry";
14
+
15
+ type VideoItem = { poster: string; videoUrl?: string; label?: string };
16
+
17
+
18
+ export function VideoWallSection({ sectionProps = {} }: SectionComponentProps) {
19
+ const {
20
+ title = "Quem usa, mostra",
21
+ subtitle = "Depoimentos em vídeo de quem já comprou.",
22
+ } = sectionProps as Record<string, string>;
23
+ const items = ((sectionProps.items as VideoItem[] | undefined) ?? []).filter((v) => v?.poster && v?.videoUrl);
24
+ const [playing, setPlaying] = React.useState<number | null>(null);
25
+ if (items.length === 0) return null;
26
+
27
+ return (
28
+ <div className="mx-auto max-w-[var(--container-max,1240px)] px-4 pt-[var(--section-gap,52px)] sm:px-6">
29
+ <div className="mb-[18px]">
30
+ <Editable.Text as="h2" path="titulo" fallback={title} label="Título" className="font-display text-[28px] font-extrabold leading-[1.15]" />
31
+ <Editable.Text as="p" path="subtitulo" fallback={subtitle} label="Subtítulo" className="mt-1 text-sm text-[var(--store-muted)]" />
32
+ </div>
33
+ <div className="grid grid-cols-3 gap-3 sm:grid-cols-6">
34
+ <EditableScope path="videos">
35
+ <Editable.Sections nested>
36
+ {items.map((v, i) => (
37
+ <Editable.Section key={i} item id={`video-${i + 1}`} label={`Vídeo ${i + 1}`}>
38
+ <div className="relative aspect-[9/16] overflow-hidden rounded-xl bg-[var(--store-surface-2)]">
39
+ {playing === i && v.videoUrl ? (
40
+ <video src={v.videoUrl} controls autoPlay playsInline className="h-full w-full object-cover" />
41
+ ) : (
42
+ <button type="button" onClick={() => v.videoUrl && setPlaying(i)} aria-label={v.label ?? "Assistir depoimento"} className="group h-full w-full cursor-pointer">
43
+ <Editable.Img path="capa" fallback={{ src: v.poster, alt: "" }} label="Capa do vídeo" className="h-full w-full object-cover" loading="lazy" decoding="async" />
44
+ {v.videoUrl && (
45
+ <span className="absolute inset-0 flex items-center justify-center">
46
+ <span className="flex h-11 w-11 items-center justify-center rounded-full bg-white/90 text-[var(--store-ink)] transition-transform group-hover:scale-110"><Play weight="fill" className="text-[18px]" /></span>
47
+ </span>
48
+ )}
49
+ </button>
50
+ )}
51
+ </div>
52
+ </Editable.Section>
53
+ ))}
54
+ </Editable.Sections>
55
+ </EditableScope>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,20 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // RECEITA DA LANDING DE OFERTA (/oferta) — as seções ABAIXO do hero de compra.
3
+ // Mesma mecânica da home: registry + entradas editáveis pelo agente de branding
4
+ // (catálogo de seções em agents/PADROES.md). O hero de compra (galeria + tiers)
5
+ // é fixo da página; o que muda por marca é a pilha de convencimento abaixo dele.
6
+ // ═══════════════════════════════════════════════════════════════════════════
7
+ import type { HomeRecipe } from "@/components/home/sections/registry";
8
+
9
+ export const landingRecipe: HomeRecipe = [
10
+ { section: "attributes-marquee" },
11
+ { section: "benefits" },
12
+ { section: "stats-grid" },
13
+ { section: "ritual" },
14
+ { section: "media-cards" },
15
+ { section: "spec-table" },
16
+ { section: "reviews-carousel" },
17
+ { section: "video-wall" },
18
+ { section: "comparison" },
19
+ { section: "quote-banner" },
20
+ ];
@@ -0,0 +1,40 @@
1
+ // EDITOR: a pilha de seções da landing /oferta dentro do container PRÓPRIO dela ("oferta").
2
+ //
3
+ // São os mesmos componentes da home, com copy desta página de propósito (é uma campanha). Renderizadas
4
+ // SOLTAS, como app/(loja)/oferta/page.tsx fazia, as seções aparecem na tela mas nada dentro delas é
5
+ // editável: os caminhos cairiam na raiz do documento (foundation 11: a raiz não se grava), e o gate
6
+ // reprova a página por `semContainer`. `withIds` dá a cada entrada da receita a mesma identidade
7
+ // estável que a home usa (IDENTIDADE_PADRAO do registry).
8
+ //
9
+ // Uso na página (server component; este arquivo também é, por isso as exportações NOMEADAS):
10
+ //
11
+ // <div className="store-layout full-bleed bg-white text-[var(--store-ink)]">
12
+ // <h1 className="sr-only">Oferta especial</h1>
13
+ // <OfertaSections data={data} />
14
+ // <div className="pb-14" />
15
+ // </div>
16
+ //
17
+ // A rota tem de estar em CONTAINERS_POR_ROTA (lib/rotas-editaveis.ts) como `"/oferta": ["oferta"]`:
18
+ // é o que o editor lê para saber em que página cada seção aparece, e o que o gate confere.
19
+ import { landingRecipe } from "@/components/landing/landing-recipe";
20
+ import { SECTIONS, withIds, type HomeData } from "@/components/home/sections/registry";
21
+ import { PurchaseHeroSection } from "@/components/home/sections/purchase-hero";
22
+ import { EditableSection, EditableSections } from "@/lib/editable";
23
+
24
+ export function OfertaSections({ data }: { data: HomeData }) {
25
+ return (
26
+ <EditableSections container="oferta">
27
+ <EditableSection id="compra" kind="produto-em-destaque" label="Bloco de compra">
28
+ <PurchaseHeroSection data={data} />
29
+ </EditableSection>
30
+ {withIds(landingRecipe).map((entry) => {
31
+ const Section = SECTIONS[entry.section];
32
+ return (
33
+ <EditableSection key={entry.id} id={entry.id} kind={entry.kind} label={entry.label}>
34
+ <Section data={data} variant={entry.variant} sectionProps={entry.props} />
35
+ </EditableSection>
36
+ );
37
+ })}
38
+ </EditableSections>
39
+ );
40
+ }