@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,227 @@
1
+ // A PÁGINA DE PRODUTO É UM MOLDE.
2
+ //
3
+ // Um valor para TODOS os produtos: o documento do lojista não tem dimensão de produto, de propósito.
4
+ // Nome, preço, fotos, descrição, variantes, tamanhos, estoque, selos regulados e avaliações vêm do
5
+ // catálogo da Unbox e do enriquecimento e NÃO viram primitivo: são DADO, e o gate os desconta por
6
+ // `data-editor-ignore` (README do editor, §8). O que vira primitivo é o que o lojista mudaria SEM mudar
7
+ // o produto: chapéu, links do caminho, selos de confiança, promessa de entrega, título e atributos de
8
+ // qualidade, título e perguntas MODELO do FAQ, chapéu/título/link do catálogo, newsletter.
9
+ //
10
+ // Container PRÓPRIO (`produto`, declarado em lib/rotas-editaveis.ts): a faixa de confiança e a newsletter
11
+ // daqui NÃO reaproveitam o `home`, porque "frete grátis acima de R$ 150" na home e na PDP são frases que
12
+ // o lojista pode querer diferentes (a ordem é do container; a copy é do container). Ids de seção
13
+ // descrevem o PAPEL do bloco e são chave de arquivo: não existe renomear.
14
+ //
15
+ // Esta view recebe TUDO por prop (nada de fetch aqui): é o que permite renderizá-la com um produto de
16
+ // mentira num teste, já que a loja local não fala com o catálogo. Quem busca é page.tsx.
17
+ //
18
+ // Server component: exportações NOMEADAS (`EditableSection`...), nunca o objeto `Editable` (README §4).
19
+ import { CaretRight } from "@phosphor-icons/react/dist/ssr";
20
+ import { EditableLink, EditableSection, EditableSections, EditableText } from "@/lib/editable";
21
+ import { PdpGallery } from "@/components/product/pdp/gallery";
22
+ import {
23
+ BuyBox,
24
+ type PdpBenefit,
25
+ type PdpRelatedOption,
26
+ type PdpSizeOption,
27
+ type PdpSubscription,
28
+ type PdpVariant,
29
+ } from "@/components/product/pdp/buy-box";
30
+ import { ProductTabs, FaqList, type KV } from "@/components/product/pdp/interactive";
31
+ import { TrustStrip, SecurityBar, ReviewsCard, QualidadeCard, QUALIDADE, type ReviewItem } from "@/components/product/pdp/sections";
32
+ import { Newsletter } from "@/components/product/pdp/newsletter";
33
+ import { CatalogGrid, CatalogCta, type CatalogItem } from "@/components/product/pdp/catalog-grid";
34
+ import type { PerguntaResposta } from "@/components/product/pdp/faq-modelo";
35
+
36
+ export interface PdpViewProps {
37
+ /** Dado do catálogo e do enriquecimento: nunca copy do molde. */
38
+ produto: {
39
+ id: string;
40
+ titulo: string;
41
+ imagens: string[];
42
+ videos: string[];
43
+ /** desconto REAL (compareAtPrice) para o selo da galeria; null = sem selo */
44
+ descontoPct: number | null;
45
+ descricaoCurta?: string | null;
46
+ esgotado: boolean;
47
+ minQty: number;
48
+ maxQty: number | null;
49
+ };
50
+ compra: {
51
+ variants: PdpVariant[];
52
+ benefits?: PdpBenefit[];
53
+ sizeOptions: PdpSizeOption[];
54
+ relatedTitle?: string;
55
+ relatedOptions: PdpRelatedOption[];
56
+ subscription: PdpSubscription | null;
57
+ /** nota e contagem REAIS (reviews do enriquecimento); sem elas o bloco de avaliação da buy box não aparece */
58
+ ratingAverage?: number;
59
+ ratingCount?: number;
60
+ };
61
+ detalhes: {
62
+ descHtml?: string | null;
63
+ infoHtml?: string | null;
64
+ specItems: KV[];
65
+ nutriItems: KV[];
66
+ nutriBase?: string | null;
67
+ usageSteps?: string[] | null;
68
+ };
69
+ /** null = sem avaliação real: o card não entra na página */
70
+ avaliacoes: { ratingCount: number; average: number; reviews: ReviewItem[] } | null;
71
+ /** FAQ do enriquecimento (por produto), dado do catálogo. As perguntas MODELO do molde entram sempre (faq-modelo.ts). */
72
+ faq?: readonly PerguntaResposta[];
73
+ catalogo: CatalogItem[];
74
+ }
75
+
76
+ export function PdpView({ produto, compra, detalhes, avaliacoes, faq, catalogo }: PdpViewProps) {
77
+ return (
78
+ <div className="full-bleed store-layout bg-white text-[var(--store-ink)]">
79
+ <div className="mx-auto max-w-[1240px] px-4 pb-2 sm:px-6">
80
+ <EditableSections container="produto">
81
+ {/* FIXA: a galeria e o bloco de compra SÃO a página; reordenar e ocultar valem para o resto. */}
82
+ <EditableSection id="compra" kind="produto-em-destaque" label="Bloco de compra" fixed>
83
+ {/* caminho de navegação: os dois links são copy de navegação da marca; o nome do produto é dado */}
84
+ <div className="flex items-center gap-2 pt-4 text-[13px] font-medium text-[var(--store-muted)]">
85
+ <EditableLink
86
+ path="trilha.inicio"
87
+ fallback={{ href: "/", label: "Início" }}
88
+ label="Caminho: link da página inicial"
89
+ className="no-underline hover:text-[var(--store-ink)]"
90
+ />
91
+ <CaretRight className="text-[11px]" />
92
+ <EditableLink
93
+ path="trilha.produtos"
94
+ fallback={{ href: "/produtos", label: "Produtos" }}
95
+ label="Caminho: link da lista de produtos"
96
+ className="no-underline hover:text-[var(--store-ink)]"
97
+ />
98
+ <CaretRight className="text-[11px]" />
99
+ <span data-editor-ignore className="font-semibold text-[var(--store-ink)] line-clamp-1">{produto.titulo}</span>
100
+ </div>
101
+
102
+ <div className="grid grid-cols-1 items-start gap-7 pt-4 lg:grid-cols-[1.05fr_0.95fr] lg:gap-12">
103
+ <PdpGallery images={produto.imagens} videos={produto.videos} title={produto.titulo} discountPct={produto.descontoPct} />
104
+ <BuyBox
105
+ ratingAverage={compra.ratingAverage}
106
+ ratingCount={compra.ratingCount}
107
+ productId={produto.id}
108
+ title={produto.titulo}
109
+ shortDescription={produto.descricaoCurta}
110
+ imageUrl={produto.imagens[0]}
111
+ variants={compra.variants}
112
+ benefits={compra.benefits}
113
+ sizeOptions={compra.sizeOptions}
114
+ relatedTitle={compra.relatedTitle}
115
+ relatedOptions={compra.relatedOptions}
116
+ subscription={compra.subscription}
117
+ minQty={produto.minQty}
118
+ maxQty={produto.maxQty}
119
+ isSoldOut={produto.esgotado}
120
+ />
121
+ </div>
122
+ </EditableSection>
123
+
124
+ {/* Abaixo do frame principal: confiança, conteúdo do produto, qualidade + FAQ, catálogo, newsletter. */}
125
+ <EditableSection id="confianca" kind="beneficios" label="Faixa de confiança">
126
+ <div className="mt-10"><TrustStrip /></div>
127
+ </EditableSection>
128
+
129
+ <EditableSection id="seguranca" kind="beneficios" label="Barra de compra segura e pagamento">
130
+ <div className="mt-5"><SecurityBar /></div>
131
+ </EditableSection>
132
+
133
+ {/* descrição, ficha, tabela nutricional e modo de uso: TUDO dado do produto; os rótulos das abas são interface */}
134
+ <EditableSection id="detalhes" kind="outro" label="Descrição e ficha do produto">
135
+ <div className="mt-12" data-editor-ignore>
136
+ <ProductTabs
137
+ descHtml={detalhes.descHtml}
138
+ infoHtml={detalhes.infoHtml}
139
+ specItems={detalhes.specItems}
140
+ nutriItems={detalhes.nutriItems}
141
+ nutriBase={detalhes.nutriBase}
142
+ usageSteps={detalhes.usageSteps}
143
+ />
144
+ </div>
145
+ </EditableSection>
146
+
147
+ {/* avaliações só entram quando existem de verdade */}
148
+ {avaliacoes && (
149
+ <EditableSection id="avaliacoes" kind="depoimentos" label="Avaliações">
150
+ <div id="avaliacoes" className="mt-12">
151
+ <ReviewsCard ratingCount={avaliacoes.ratingCount} average={avaliacoes.average} reviews={avaliacoes.reviews} />
152
+ </div>
153
+ </EditableSection>
154
+ )}
155
+
156
+ {/* qualidade + FAQ dividem UMA linha da grade. Os dois cartões são ITENS de uma lista aninhada: cada
157
+ um oculta e reordena por si, e cada um hospeda a própria lista (atributos de qualidade,
158
+ perguntas) num container só dele. A grade é auto-fit: com um cartão só (qualidade vazia ou
159
+ oculta pelo lojista) o FAQ ocupa a linha inteira em vez de deixar uma coluna vazia. */}
160
+ <EditableSection id="qualidade-faq" kind="perguntas-frequentes" label="Qualidade e perguntas frequentes">
161
+ <div className="mt-12 grid items-start gap-[22px] lg:grid-cols-[repeat(auto-fit,minmax(320px,1fr))]">
162
+ <EditableSections nested>
163
+ {/* o cartão de qualidade só existe com atributos declarados (sections.tsx, QUALIDADE) */}
164
+ {QUALIDADE.length > 0 && (
165
+ <EditableSection item id="qualidade" label="Qualidade do produto">
166
+ <QualidadeCard />
167
+ </EditableSection>
168
+ )}
169
+ <EditableSection item id="faq" label="Perguntas frequentes">
170
+ <div className="rounded-xl border border-[var(--store-line)] bg-white p-[28px]">
171
+ <EditableText
172
+ as="h2"
173
+ path="titulo"
174
+ fallback="Perguntas frequentes"
175
+ label="Título das perguntas frequentes"
176
+ className="font-display mb-[22px] text-[21px] font-extrabold italic leading-tight text-[var(--store-primary,#18181B)]"
177
+ />
178
+ <FaqList items={faq} />
179
+ </div>
180
+ </EditableSection>
181
+ </EditableSections>
182
+ </div>
183
+ </EditableSection>
184
+
185
+ {/* catálogo: chapéu, título, frase e link são copy do molde; os produtos da grade são dado */}
186
+ {catalogo.length > 0 && (
187
+ <EditableSection id="catalogo" kind="vitrine-de-produtos" label="Mais produtos do catálogo">
188
+ <div className="mt-[52px]">
189
+ <div className="mb-1.5 flex items-end justify-between">
190
+ <div>
191
+ <EditableText
192
+ as="div"
193
+ path="chapeu"
194
+ fallback="CATÁLOGO"
195
+ label="Chapéu do catálogo"
196
+ className="text-xs font-extrabold tracking-[1.5px] text-[var(--store-primary,#18181B)]"
197
+ />
198
+ <EditableText
199
+ as="h2"
200
+ path="titulo"
201
+ fallback="Explore mais do catálogo"
202
+ label="Título do catálogo"
203
+ className="font-display mt-1.5 text-[26px] font-extrabold"
204
+ />
205
+ </div>
206
+ <CatalogCta />
207
+ </div>
208
+ <EditableText
209
+ as="p"
210
+ path="texto"
211
+ fallback="Clientes que viram este produto também levam:"
212
+ label="Frase de apoio do catálogo"
213
+ className="mb-[22px] mt-1 text-sm text-[var(--store-muted)]"
214
+ />
215
+ <CatalogGrid items={catalogo} />
216
+ </div>
217
+ </EditableSection>
218
+ )}
219
+
220
+ <EditableSection id="newsletter" kind="newsletter" label="Newsletter">
221
+ <div className="mt-12"><Newsletter /></div>
222
+ </EditableSection>
223
+ </EditableSections>
224
+ </div>
225
+ </div>
226
+ );
227
+ }
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ // Seções de recomendação da PDP com PRODUTOS REAIS do catálogo e add-to-cart funcional.
4
+ // Substitui os antigos Bundles/Combos com preços fictícios e botões sem ação.
5
+ import * as React from "react";
6
+ import Link from "next/link";
7
+ import Image from "next/image";
8
+ import { Plus, Spinner, ShoppingCartSimple, ArrowRight } from "@phosphor-icons/react/dist/ssr";
9
+ import { useCart } from "@/components/cart/cart-provider";
10
+ import { formatBRL } from "@/lib/format";
11
+ import type { CatalogItem } from "@/components/product/pdp/catalog-grid";
12
+
13
+ const canAdd = (p: CatalogItem) => !!p.productId && !!p.variantId && p.price != null;
14
+
15
+ function useQuickAdd() {
16
+ const { add } = useCart();
17
+ const [busyId, setBusyId] = React.useState<string | null>(null);
18
+ const addOne = React.useCallback(async (p: CatalogItem) => {
19
+ if (!canAdd(p)) return;
20
+ setBusyId(p.variantId!);
21
+ await add({ productId: p.productId!, variantId: p.variantId!, price: p.price ?? 0, quantity: 1, thumbnail: p.imageUrl ?? undefined, title: p.title });
22
+ setBusyId(null);
23
+ }, [add]);
24
+ return { addOne, busyId };
25
+ }
26
+
27
+ function Thumb({ item, size = 84 }: { item: CatalogItem; size?: number }) {
28
+ return (
29
+ <span className="relative flex items-center justify-center overflow-hidden rounded-md border border-[var(--store-line)] bg-[var(--store-line)]" style={{ width: size, height: size }}>
30
+ {item.imageUrl ? (
31
+ <Image src={item.imageUrl} alt={item.title} fill sizes={`${size}px`} className="object-contain p-0.5" />
32
+ ) : (
33
+ <span className="text-[10px] text-[var(--store-faint)]">sem foto</span>
34
+ )}
35
+ </span>
36
+ );
37
+ }
38
+
39
+ /** "Compre junto" (real): N produtos reais + adicionar todos de uma vez. Sem desconto fabricado. */
40
+ export function BuyTogether({ items }: { items: CatalogItem[] }) {
41
+ const products = items.filter(canAdd).slice(0, 3);
42
+ const { addOne } = useQuickAdd();
43
+ const [busyAll, setBusyAll] = React.useState(false);
44
+ if (products.length < 2) return null;
45
+ const sum = products.reduce((s, p) => s + (p.price ?? 0), 0);
46
+
47
+ async function addAll() {
48
+ setBusyAll(true);
49
+ for (const p of products) await addOne(p);
50
+ setBusyAll(false);
51
+ }
52
+
53
+ return (
54
+ <div className="rounded-xl border border-[var(--store-line)] bg-white p-[26px]">
55
+ <h2 className="font-display mb-5 text-[22px] font-extrabold text-[var(--store-primary,#18181B)]">Compre junto</h2>
56
+ <div className="flex flex-wrap items-start gap-2 max-sm:justify-center max-sm:gap-3.5">
57
+ {products.map((p, i) => (
58
+ <div key={p.slug} className="contents">
59
+ <div className="flex w-[100px] shrink-0 flex-col items-center gap-1.5">
60
+ <Link href={`/produto/${encodeURIComponent(p.slug)}`}><Thumb item={p} /></Link>
61
+ <Link href={`/produto/${encodeURIComponent(p.slug)}`} className="line-clamp-2 text-center text-xs font-bold leading-tight text-[var(--store-ink)] no-underline">{p.title}</Link>
62
+ <div className="text-[13px] font-extrabold text-[var(--store-ink)]">{p.displayPrice}</div>
63
+ </div>
64
+ {i < products.length - 1 && (
65
+ <span className="mt-9 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--store-primary,#18181B)] text-white"><Plus weight="bold" className="text-[15px]" /></span>
66
+ )}
67
+ </div>
68
+ ))}
69
+ <span className="mt-9 shrink-0 text-[22px] font-bold text-[var(--store-faint)]">=</span>
70
+ <div className="flex flex-1 flex-col items-center gap-1.5 pt-0.5 text-center">
71
+ <div className="text-[13px] text-[var(--store-muted)]">Os {products.length} por</div>
72
+ <div className="font-display text-[26px] font-extrabold leading-none text-[var(--store-ink)]">{formatBRL(sum)}</div>
73
+ <button type="button" onClick={addAll} disabled={busyAll}
74
+ className="font-display mt-1 flex cursor-pointer items-center gap-1.5 rounded-md bg-[var(--store-primary,#18181B)] px-5 py-3 text-[13px] font-bold leading-tight text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
75
+ {busyAll ? <Spinner className="animate-spin" /> : <ShoppingCartSimple weight="bold" />}Adicionar todos
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ /** "Frequentemente comprados juntos" (real): produtos reais com quick-add individual. */
84
+ export function FrequentlyBought({ items }: { items: CatalogItem[] }) {
85
+ const products = items.filter(canAdd).slice(0, 3);
86
+ const { addOne, busyId } = useQuickAdd();
87
+ if (products.length === 0) return null;
88
+
89
+ return (
90
+ <div className="rounded-xl border border-[var(--store-line)] bg-white p-[26px]">
91
+ <h2 className="font-display mb-5 text-[22px] font-extrabold text-[var(--store-ink)]">Frequentemente comprados juntos</h2>
92
+ <div className="flex items-start gap-[18px]">
93
+ {products.map((p) => (
94
+ <div key={p.slug} className="flex flex-1 flex-col items-center gap-1.5">
95
+ <Link href={`/produto/${encodeURIComponent(p.slug)}`}><Thumb item={p} size={96} /></Link>
96
+ <Link href={`/produto/${encodeURIComponent(p.slug)}`} className="line-clamp-2 text-center text-xs font-bold leading-tight text-[var(--store-ink)] no-underline">{p.title}</Link>
97
+ <div className="text-[13px] font-extrabold text-[var(--store-ink)]">{p.displayPrice}</div>
98
+ <button type="button" onClick={() => addOne(p)} disabled={busyId === p.variantId} aria-label={`Adicionar ${p.title}`}
99
+ className="flex h-[34px] w-[34px] items-center justify-center rounded-full bg-[var(--store-primary,#18181B)] text-white shadow-[var(--store-shadow-cta-sm)] transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
100
+ {busyId === p.variantId ? <Spinner className="animate-spin text-base" /> : <Plus weight="bold" className="text-base" />}
101
+ </button>
102
+ </div>
103
+ ))}
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ /** Grade de "kits sugeridos" (real): produtos reais com quick-add. Sem % OFF fabricado. */
110
+ export function SuggestedKits({ items }: { items: CatalogItem[] }) {
111
+ const products = items.filter(canAdd).slice(0, 3);
112
+ const { addOne, busyId } = useQuickAdd();
113
+ if (products.length === 0) return null;
114
+
115
+ return (
116
+ <>
117
+ <div className="mb-[18px] flex items-end justify-between">
118
+ <div>
119
+ <h2 className="font-display text-2xl font-extrabold">Leve também</h2>
120
+ <p className="mt-1 text-sm text-[var(--store-muted)]">Você também pode gostar.</p>
121
+ </div>
122
+ <Link href="/produtos" className="flex items-center gap-1.5 text-sm font-bold text-[var(--store-primary,#18181B)] no-underline max-sm:hidden">
123
+ Ver todos os produtos <ArrowRight weight="bold" />
124
+ </Link>
125
+ </div>
126
+ <div className="grid gap-[18px] sm:grid-cols-2 lg:grid-cols-3">
127
+ {products.map((p) => (
128
+ <div key={p.slug} className="flex flex-col rounded-2xl border border-[var(--store-line)] bg-white p-[18px]">
129
+ <Link href={`/produto/${encodeURIComponent(p.slug)}`} className="flex justify-center py-2">
130
+ <Thumb item={p} size={120} />
131
+ </Link>
132
+ <Link href={`/produto/${encodeURIComponent(p.slug)}`} className="mt-2 line-clamp-2 text-base font-bold text-[var(--store-ink)] no-underline">{p.title}</Link>
133
+ {p.weight && <div className="mt-0.5 text-[12.5px] text-[var(--store-muted)]">{p.weight}</div>}
134
+ <div className="font-display mt-2 text-[22px] font-extrabold text-[var(--store-primary,#18181B)]">{p.displayPrice}</div>
135
+ <button type="button" onClick={() => addOne(p)} disabled={busyId === p.variantId}
136
+ className="font-display mt-3.5 flex cursor-pointer items-center justify-center gap-1.5 rounded-md bg-[var(--store-primary,#18181B)] py-3 text-[13.5px] font-bold text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
137
+ {busyId === p.variantId ? <Spinner className="animate-spin" /> : <Plus weight="bold" />}Adicionar
138
+ </button>
139
+ </div>
140
+ ))}
141
+ </div>
142
+ </>
143
+ );
144
+ }
@@ -0,0 +1,173 @@
1
+ import { Truck, Lightning, LockSimple, SealCheck, Star } from "@phosphor-icons/react/dist/ssr";
2
+ import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
3
+ // EDITOR: este arquivo é server component (sem "use client"): exportações NOMEADAS, nunca `Editable.*` (README §4).
4
+ import { EditableIcon, EditableSection, EditableSections, EditableText } from "@/lib/editable";
5
+ import { PaymentChips } from "@/components/product/pdp/payment-chips";
6
+ import { FREE_SHIPPING_THRESHOLD } from "@/lib/store-config";
7
+
8
+ // Os selos são uma LISTA editável (README do editor §3: `Sections nested` + `Section item`), com id por
9
+ // PAPEL e não por posição: o selo de frete só existe com FREE_SHIPPING_THRESHOLD, e um id posicional
10
+ // deslizaria (o texto que o lojista escreveu no selo 2 apareceria no selo 1) quando a regra mudasse.
11
+ // Selo de frete grátis só entra quando há regra real configurada em lib/store-config.
12
+ //
13
+ // Só o que o template consegue afirmar: SSL é fato; o prazo é calculado pela cotação real; 7 dias é o
14
+ // art. 49 do CDC, vale para toda loja online no Brasil. "Expressa em 1 a 2 dias" e "devolução em 30
15
+ // dias" eram inventados, e o segundo contradizia a própria página /devolucoes da loja.
16
+ const TRUST = [
17
+ ...(FREE_SHIPPING_THRESHOLD != null
18
+ ? [{ id: "frete", label: "Selo de frete grátis", icon: Truck, title: `Frete grátis acima de R$${FREE_SHIPPING_THRESHOLD}`, sub: "para todo o Brasil" }]
19
+ : []),
20
+ { id: "entrega", label: "Selo de entrega", icon: Lightning, title: "Entrega para todo o Brasil", sub: "prazo calculado no carrinho" },
21
+ { id: "seguranca", label: "Selo de compra segura", icon: LockSimple, title: "Compra 100% segura", sub: "criptografia SSL" },
22
+ { id: "arrependimento", label: "Selo de devolução", icon: SealCheck, title: "Troca em 7 dias", sub: "arrependimento garantido por lei (CDC)" },
23
+ ];
24
+
25
+ export function TrustStrip() {
26
+ return (
27
+ <div className={`grid grid-cols-2 rounded-2xl border border-[var(--store-line)] bg-white ${TRUST.length === 4 ? "md:grid-cols-4" : "md:grid-cols-3"}`}>
28
+ <EditableSections nested>
29
+ {TRUST.map((t, i) => (
30
+ <EditableSection key={t.id} item id={t.id} label={t.label}>
31
+ {/* o invólucro da seção é display:contents: este div continua sendo o item da grade */}
32
+ <div className="flex items-center gap-3.5 px-[22px] py-5" style={{ borderRight: i < TRUST.length - 1 ? "1px solid var(--store-surface-2)" : "none" }}>
33
+ <EditableIcon path="icone" label="Ícone do selo" size={26} className="text-[26px] text-[var(--store-primary,#18181B)]">
34
+ <t.icon />
35
+ </EditableIcon>
36
+ <div>
37
+ <EditableText as="div" path="titulo" fallback={t.title} label="Título do selo" className="text-[13.5px] font-bold text-[var(--store-ink)]" />
38
+ <EditableText as="div" path="texto" fallback={t.sub} label="Texto do selo" className="mt-0.5 text-xs text-[var(--store-muted)]" />
39
+ </div>
40
+ </div>
41
+ </EditableSection>
42
+ ))}
43
+ </EditableSections>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ export function SecurityBar() {
49
+ return (
50
+ <div className="flex flex-wrap items-center justify-between gap-6 rounded-xl bg-[var(--store-chrome-bg)] px-8 py-[22px]">
51
+ <div className="flex items-center gap-[18px]">
52
+ <span className="flex h-[52px] w-[52px] shrink-0 items-center justify-center rounded-full bg-[var(--store-primary,#18181B)]">
53
+ <EditableIcon path="icone" label="Ícone de compra segura" size={25} className="text-[25px] text-white">
54
+ <LockSimple weight="fill" />
55
+ </EditableIcon>
56
+ </span>
57
+ <div>
58
+ <EditableText as="div" path="titulo" fallback="Compra 100% Segura" label="Título" className="font-display text-lg font-bold text-white" />
59
+ <EditableText as="div" path="texto" fallback="Seus dados protegidos do início ao fim." label="Texto de apoio" className="mt-0.5 text-[13.5px] text-[var(--store-chrome-muted)]" />
60
+ </div>
61
+ </div>
62
+ <div className="flex items-center gap-[22px]">
63
+ <EditableText as="span" path="pagamento" fallback="Pague como preferir" label="Texto ao lado das bandeiras" className="font-display text-[15px] font-semibold text-white max-md:hidden" />
64
+ <span className="h-[34px] w-px bg-white/20 max-md:hidden" />
65
+ {/* bandeiras de pagamento: selo de terceiro, fora do editor (o próprio componente se marca) */}
66
+ <PaymentChips size="md" />
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ export interface ReviewItem { author: string; stars: number; comment: string; verified?: boolean }
73
+
74
+ /**
75
+ * Avaliações do produto: SÓ com dado real. Sem reviews, não renderiza (o chamador também
76
+ * esconde o card). Não existe nota default, contagem default nem depoimento default: a
77
+ * foundation publicava "4,9 · 25.347 avaliações" e "Cliente A/B/C" com selo de verificação em
78
+ * loja recém-aberta, e isso foi ao ar. A distribuição por estrela é calculada das reviews.
79
+ *
80
+ * EDITOR: só o TÍTULO é copy do molde. Nota, contagem, barras e depoimentos são dado das avaliações e
81
+ * ficam fora (`data-editor-ignore`, README §8).
82
+ */
83
+ export function ReviewsCard({ ratingCount, average, reviews }: {
84
+ ratingCount: number;
85
+ average: number;
86
+ reviews: ReviewItem[];
87
+ }) {
88
+ if (!reviews.length || !ratingCount) return null;
89
+ const avg = average.toFixed(1).replace(".", ",");
90
+ const dist = [5, 4, 3, 2, 1].map((n) => {
91
+ const q = reviews.filter((r) => Math.round(r.stars) === n).length;
92
+ return { n: String(n), pct: `${Math.round((q / reviews.length) * 100)}%` };
93
+ });
94
+ const list = reviews.slice(0, 6).map((r) => ({ initial: (r.author.trim()[0] || "?").toUpperCase(), name: r.author, quip: r.comment, stars: r.stars, verified: r.verified }));
95
+ return (
96
+ <div className="rounded-xl border border-[var(--store-line)] bg-white p-7 p-[28px]">
97
+ <EditableText as="h2" path="titulo" fallback="Quem comprou, recomenda" label="Título das avaliações" className="font-display mb-[18px] text-[21px] font-extrabold italic leading-tight text-[var(--store-primary,#18181B)]" />
98
+ <div className="flex items-center gap-3.5" data-editor-ignore>
99
+ <span className="font-display text-[44px] font-extrabold leading-none text-[var(--store-ink)]">{avg}</span>
100
+ <span className="flex text-2xl text-[var(--store-cta-dark)]" aria-label={`${avg} de 5`}>{[1, 2, 3, 4, 5].map((i) => <Star key={i} weight={average >= i - 0.25 ? "fill" : "regular"} />)}</span>
101
+ </div>
102
+ <div className="mt-2.5 mb-[18px] text-[13px] text-[var(--store-muted)]" data-editor-ignore>Baseado em {ratingCount.toLocaleString("pt-BR")} {ratingCount === 1 ? "avaliação" : "avaliações"}</div>
103
+ <div className="flex flex-col gap-2.5" data-editor-ignore>
104
+ {dist.map((r) => (
105
+ <div key={r.n} className="flex items-center gap-2.5 text-[12.5px] text-[var(--store-muted)]">
106
+ <span className="w-2.5">{r.n}</span>
107
+ <Star weight="fill" className="text-[11px] text-[var(--store-cta-dark)]" />
108
+ <div className="h-[7px] flex-1 overflow-hidden rounded-full bg-[var(--store-surface-2)]"><div className="h-full rounded-full bg-[var(--store-cta-dark)]" style={{ width: r.pct }} /></div>
109
+ <span className="w-[34px] text-right">{r.pct}</span>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ <div className="my-[22px] h-px bg-[var(--store-surface-2)]" />
114
+ <div className="flex flex-col gap-4" data-editor-ignore>
115
+ {list.map((rv, i) => (
116
+ <div key={i} className="flex items-start gap-2.5">
117
+ <div className="font-display flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-full bg-[var(--store-chrome-bg)] text-sm font-extrabold text-white">{rv.initial}</div>
118
+ <div className="min-w-0 flex-1">
119
+ <div className="flex items-center gap-1.5">
120
+ <span className="text-[13.5px] font-bold">{rv.name}</span>
121
+ <span className="flex text-[10px] text-[var(--store-cta-dark)]">{[1, 2, 3, 4, 5].map((i) => <Star key={i} weight={rv.stars >= i - 0.25 ? "fill" : "regular"} />)}</span>
122
+ {rv.verified && <span className="text-[11px] font-semibold text-[var(--store-primary,#18181B)]">· compra verificada</span>}
123
+ </div>
124
+ <div className="mt-0.5 text-[13px] leading-snug text-[var(--store-ink-2)]">{rv.quip}</div>
125
+ </div>
126
+ </div>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ /**
134
+ * OS ATRIBUTOS DE QUALIDADE DO PRODUTO: o que a marca afirma sobre o que vende (feito à mão, produção em
135
+ * pequenos lotes, garantia). Nasce VAZIO de propósito: a foundation não sabe o que o produto tem, e um
136
+ * atributo plausível inventado aqui iria ao ar como afirmação da marca (foi assim na v0.15.6, e o cartão
137
+ * saiu). Quem preenche é o briefing, só com o que a marca sustenta. Sem itens, o cartão não renderiza e
138
+ * o FAQ ocupa a linha inteira.
139
+ *
140
+ * EDITOR: a PDP é um MOLDE (um valor para todos os produtos), e estes atributos são o que o lojista muda
141
+ * quando o catálogo cresce; por isso cada um é item de lista editável (ícone, título e texto), com o id
142
+ * posicional da foundation (`item-1`, `item-2`...). Apagar um item do meio NO CÓDIGO desliza os ids
143
+ * seguintes (README do editor, §6): prefira ocultar pelo painel.
144
+ */
145
+ export const QUALIDADE: readonly { icon: PhosphorIcon; title: string; sub: string }[] = [];
146
+
147
+ export function QualidadeCard() {
148
+ if (QUALIDADE.length === 0) return null;
149
+ return (
150
+ <div className="rounded-xl border border-[var(--store-line)] bg-white p-7 p-[28px]">
151
+ <EditableText as="h2" path="titulo" fallback="Qualidade que você sente" label="Título do bloco de qualidade" className="font-display mb-[22px] text-[21px] font-extrabold italic leading-tight text-[var(--store-primary,#18181B)]" />
152
+ <div className="flex flex-col gap-5">
153
+ <EditableSections nested>
154
+ {QUALIDADE.map((q, i) => (
155
+ <EditableSection key={q.title} item id={`item-${i + 1}`} label={`Atributo ${i + 1}`}>
156
+ <div className="flex items-start gap-3.5">
157
+ <span className="flex h-[54px] w-[54px] shrink-0 items-center justify-center rounded-xl bg-[var(--store-primary-soft,#F1F1F3)]">
158
+ <EditableIcon path="icone" label="Ícone do atributo" size={26} className="text-[26px] text-[var(--store-primary,#18181B)]">
159
+ <q.icon />
160
+ </EditableIcon>
161
+ </span>
162
+ <div>
163
+ <EditableText as="div" path="titulo" fallback={q.title} label="Título do atributo" className="text-[15px] font-bold text-[var(--store-ink)]" />
164
+ <EditableText as="div" path="texto" fallback={q.sub} label="Texto do atributo" className="mt-0.5 text-[13.5px] leading-snug text-[var(--store-muted)]" />
165
+ </div>
166
+ </div>
167
+ </EditableSection>
168
+ ))}
169
+ </EditableSections>
170
+ </div>
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { Minus, Plus } from "@/lib/icons";
4
+ import { Button } from "@/components/ui/button";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ // Stepper de quantidade respeitando min/max do produto (doc 02).
8
+ export function QuantityStepper({
9
+ value,
10
+ onChange,
11
+ min = 1,
12
+ max,
13
+ disabled,
14
+ className,
15
+ }: {
16
+ value: number;
17
+ onChange: (v: number) => void;
18
+ min?: number;
19
+ max?: number | null;
20
+ disabled?: boolean;
21
+ className?: string;
22
+ }) {
23
+ const dec = () => onChange(Math.max(min, value - 1));
24
+ const inc = () => onChange(max ? Math.min(max, value + 1) : value + 1);
25
+ return (
26
+ <div className={cn("inline-flex items-center rounded-lg border", className)} role="group" aria-label="Quantidade">
27
+ <Button type="button" variant="ghost" size="icon-sm" onClick={dec} disabled={disabled || value <= min} aria-label="Diminuir">
28
+ <Minus />
29
+ </Button>
30
+ <span className="w-9 text-center text-sm tabular-nums" aria-live="polite">{value}</span>
31
+ <Button type="button" variant="ghost" size="icon-sm" onClick={inc} disabled={disabled || (max != null && value >= max)} aria-label="Aumentar">
32
+ <Plus />
33
+ </Button>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr";
6
+
7
+ export function SearchBox({ defaultValue = "", className }: { defaultValue?: string; className?: string }) {
8
+ const router = useRouter();
9
+ const [q, setQ] = React.useState(defaultValue);
10
+ return (
11
+ <form
12
+ role="search"
13
+ className={className}
14
+ onSubmit={(e) => {
15
+ e.preventDefault();
16
+ const term = q.trim();
17
+ router.push(term ? `/busca?q=${encodeURIComponent(term)}` : "/produtos");
18
+ }}
19
+ >
20
+ <div className="flex items-center gap-2 rounded-xl border-[1.5px] border-[var(--store-line-2)] bg-white px-3.5 transition-colors focus-within:border-[var(--store-primary,#18181B)]">
21
+ <MagnifyingGlass weight="bold" className="shrink-0 text-[19px] text-[var(--store-muted)]" />
22
+ <input
23
+ type="search"
24
+ name="q"
25
+ value={q}
26
+ onChange={(e) => setQ(e.target.value)}
27
+ placeholder="Buscar produtos..."
28
+ aria-label="Buscar produtos"
29
+ className="h-12 min-w-0 flex-1 bg-transparent text-[15px] font-medium text-[var(--store-ink)] outline-none placeholder:text-[var(--store-faint)]"
30
+ />
31
+ <button type="submit" className="font-display shrink-0 cursor-pointer rounded-lg bg-[var(--store-primary,#18181B)] px-4 py-2 text-[13px] font-bold text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)]">
32
+ Buscar
33
+ </button>
34
+ </div>
35
+ </form>
36
+ );
37
+ }