@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,116 @@
1
+ # Agente 05 — Cart (Carrinho)
2
+
3
+ > ⚠️ **Doc de plano (histórico).** A implementação entregue pela foundation difere em nomes e
4
+ > caminhos de arquivo citados abaixo. O mapa REAL de arquivos por área é a tabela "Agentes
5
+ > 00-11" do `agents/MANAGER.md` — em conflito, valem o MANAGER e o código.
6
+
7
+ ## Escopo
8
+ Contexto global do carrinho, drawer lateral, página `/carrinho`, API route server-side e input de cupom.
9
+
10
+ ## Dependências
11
+ - Agente 00 (Scaffold) — `lib/unbox.ts`
12
+
13
+ ## Arquivos proprietários
14
+
15
+ | Arquivo | Descrição |
16
+ |---------|-----------|
17
+ | `app/api/cart/route.ts` | Route Handler: create, add, update, remove, get, discount |
18
+ | `components/cart/CartContext.tsx` | Context + Provider + useCart hook (client) |
19
+ | `components/cart/CartDrawer.tsx` | Drawer lateral Base UI Dialog |
20
+ | `components/cart/CartItem.tsx` | Item com imagem, qtd, preço, remover |
21
+ | `components/cart/CartSummary.tsx` | Subtotal, desconto, frete, botão checkout |
22
+ | `components/cart/EmptyCart.tsx` | Estado vazio com CTA para catálogo |
23
+ | `components/cart/DiscountCodeInput.tsx` | Input + aplicar cupom |
24
+ | `app/(loja)/carrinho/page.tsx` | Versão full-page do carrinho |
25
+
26
+ ## Interface do CartContext
27
+
28
+ ```ts
29
+ interface CartContextValue {
30
+ cartId: string | null
31
+ cartToken: string | null
32
+ items: CartLineItem[]
33
+ totalCount: number // soma de quantities
34
+ subtotal: number
35
+ total: number
36
+ appliedDiscount?: { code: string; amount: number }
37
+ isOpen: boolean
38
+ isLoading: boolean
39
+ openCart(): void
40
+ closeCart(): void
41
+ addItem(params: AddParams): Promise<void>
42
+ updateQuantity(itemId: string, qty: number): Promise<void>
43
+ removeItem(itemId: string): Promise<void>
44
+ applyDiscount(code: string): Promise<{ success: boolean; error?: string }>
45
+ refreshCart(): Promise<void>
46
+ }
47
+ ```
48
+
49
+ ## Persistência de cookies
50
+
51
+ ```ts
52
+ // Browser-side (client component) — cartId e cartToken NÃO são sensíveis
53
+ function saveCartCookies(cartId: string, cartToken: string) {
54
+ const exp = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString()
55
+ document.cookie = `unbox_cart_id=${cartId}; expires=${exp}; path=/; SameSite=Lax`
56
+ document.cookie = `unbox_cart_token=${cartToken}; expires=${exp}; path=/; SameSite=Lax`
57
+ }
58
+ function readCartCookies() {
59
+ const get = (n: string) => document.cookie.match(new RegExp(`(?:^|; )${n}=([^;]*)`))?.[1]
60
+ return { cartId: get("unbox_cart_id") ?? null, cartToken: get("unbox_cart_token") ?? null }
61
+ }
62
+ ```
63
+
64
+ ## API Route /api/cart
65
+
66
+ ```ts
67
+ // POST /api/cart — actions: create | add | update | remove | get | discount | clear
68
+ import { getUnboxClient } from "@/lib/unbox"
69
+
70
+ export async function POST(req: Request) {
71
+ const client = await getUnboxClient()
72
+ const body = await req.json()
73
+
74
+ switch (body.action) {
75
+ case "create":
76
+ return Response.json(await client.createCart({ shopId: client.shopId }))
77
+ case "add":
78
+ return Response.json(await client.addCartItems(body.cartId, body.cartToken, [body.item]))
79
+ case "update":
80
+ return Response.json(await client.updateCartItem(body.cartId, body.cartToken, body.itemId, body.qty))
81
+ case "remove":
82
+ return Response.json(await client.removeCartItem(body.cartId, body.cartToken, body.itemId))
83
+ case "get":
84
+ return Response.json(await client.getCart(body.cartId, body.cartToken))
85
+ case "discount":
86
+ return Response.json(await client.applyDiscountCode(body.cartId, body.cartToken, body.code))
87
+ default:
88
+ return Response.json({ error: "Ação inválida" }, { status: 400 })
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## CartDrawer (Base UI)
94
+
95
+ ```tsx
96
+ import * as Dialog from "@base-ui-components/react/dialog"
97
+
98
+ <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
99
+ <Dialog.Backdrop className="fixed inset-0 bg-black/40" />
100
+ <Dialog.Popup render={
101
+ <aside className="fixed inset-y-0 right-0 w-full max-w-md bg-white shadow-2xl flex flex-col" />
102
+ }>
103
+ <Dialog.Title>Meu Carrinho ({totalCount})</Dialog.Title>
104
+ {/* items */}
105
+ <CartSummary />
106
+ </Dialog.Popup>
107
+ </Dialog.Root>
108
+ ```
109
+
110
+ ## Cupom — regra crítica
111
+
112
+ ```ts
113
+ // discountCodes(first:) recebe ConnectionLimitInt — máximo 20
114
+ // Ao aplicar: mostrar erro via friendlyError() se falhar
115
+ // Erro de cupom inválido: cartEventLabel("COUPON_NOT_APPLICABLE") → "Cupom não aplicável"
116
+ ```
@@ -0,0 +1,146 @@
1
+ # Agente 06 — Checkout (Pagamento)
2
+
3
+ > ⚠️ **Doc de plano (histórico).** A implementação entregue pela foundation difere em nomes e
4
+ > caminhos de arquivo citados abaixo. O mapa REAL de arquivos por área é a tabela "Agentes
5
+ > 00-11" do `agents/MANAGER.md` — em conflito, valem o MANAGER e o código.
6
+
7
+ ## Escopo
8
+ Fluxo completo de checkout: endereço → cotação de frete → seleção → pagamento (Pix/Cartão) → pedido.
9
+
10
+ ## Dependências
11
+ - Agente 00 (Scaffold)
12
+ - Agente 05 (Cart) — `cartId` e `cartToken` vêm do CartContext
13
+
14
+ ## Arquivos proprietários
15
+
16
+ | Arquivo | Descrição |
17
+ |---------|-----------|
18
+ | `app/api/checkout/route.ts` | Route Handler com lock de idempotência por cartId |
19
+ | `app/(loja)/checkout/page.tsx` | Multi-step checkout (client-heavy) |
20
+ | `app/(loja)/pedido/[referenceId]/page.tsx` | Confirmação de pedido |
21
+ | `components/checkout/AddressForm.tsx` | Formulário de endereço |
22
+ | `components/checkout/ShippingSelector.tsx` | Lista de opções de frete |
23
+ | `components/checkout/PaymentSelector.tsx` | Pix ou Cartão |
24
+ | `components/checkout/PixPayment.tsx` | QR code + copia-e-cola + polling |
25
+ | `components/checkout/CreditCardForm.tsx` | Dados do cartão |
26
+ | `components/checkout/OrderSummary.tsx` | Resumo dos itens (sidebar) |
27
+ | `components/checkout/CheckoutProgress.tsx` | Steps: Endereço → Frete → Pagamento |
28
+
29
+ ## Fluxo da API (ordem obrigatória)
30
+
31
+ ```
32
+ 1. setShippingAddressOnCart → fulfillmentGroupId
33
+ 2. updateFulfillmentOptionsForGroup → availableFulfillmentOptions[]
34
+ 3. (opcional) applyDiscountCodeToCart
35
+ 4. selectFulfillmentOptionForGroup → total com frete
36
+ 5. placeOrder → order (Pix QR | resultado cartão)
37
+ ```
38
+
39
+ ## device (antifraude/3DS) — OBRIGATÓRIO no placeOrder
40
+
41
+ Vai no **nível raiz** do `PlaceOrderInput` (irmão de `order` e `payments`), campo único
42
+ com enum `type`:
43
+
44
+ - `type: "BROWSER"` — 8 campos reais do navegador (colorDepth, javaEnabled, userAgent,
45
+ language, screenHeight, screenWidth, timezoneOffset), coletados em
46
+ `components/checkout/checkout-client.tsx` e repassados pelo BFF (`/api/checkout`).
47
+ - `type: "API"` — fallback para pedidos server-side (scripts) — o SDK aplica
48
+ automaticamente quando `device` é omitido.
49
+
50
+ `timezoneOffset` em **minutos** (`getTimezoneOffset()`, BRT → 180) — ponto aberto com a
51
+ Unbox se seria em horas; ver `lib/unbox/types.ts`.
52
+
53
+ ## AddressInput — campos obrigatórios
54
+
55
+ ```ts
56
+ // NON_NULL no schema: fullName, phone, postal, address1, city, region, country
57
+ // Validados ao vivo (obrigatórios na prática): taxPayerId, number, neighborhood
58
+ interface AddressInput {
59
+ fullName: string; taxPayerId: string; postal: string
60
+ address1: string; number: string; neighborhood: string
61
+ city: string; region: string; phone: string
62
+ country?: string // default "BR"
63
+ address2?: string
64
+ isCommercial?: boolean
65
+ }
66
+ ```
67
+
68
+ ## Idempotência (CRÍTICO)
69
+
70
+ ```ts
71
+ // Em memória (dev/single-instance) — em prod usar KV/Redis
72
+ const inFlight = new Map<string, true>()
73
+
74
+ export async function POST(req: Request) {
75
+ const { action, cartId, cartToken, ...rest } = await req.json()
76
+
77
+ if (action === "place") {
78
+ if (inFlight.has(cartId)) {
79
+ return Response.json({ error: "pedido_em_andamento" }, { status: 409 })
80
+ }
81
+ inFlight.set(cartId, true)
82
+ try {
83
+ const result = await doPlaceOrder(cartId, cartToken, rest)
84
+ return Response.json(result)
85
+ } finally {
86
+ inFlight.delete(cartId)
87
+ }
88
+ }
89
+ // ... outros actions
90
+ }
91
+ ```
92
+
93
+ ## Pix — polling de status
94
+
95
+ ```tsx
96
+ // Polling a cada 5s até status !== "new" e !== "processing"
97
+ useEffect(() => {
98
+ if (!orderId || status === "paid") return
99
+ const id = setInterval(async () => {
100
+ const res = await fetch(`/api/checkout?orderId=${orderId}`)
101
+ const { order } = await res.json()
102
+ if (order.status !== "new" && order.status !== "processing") {
103
+ setStatus(order.status)
104
+ clearInterval(id)
105
+ if (order.status === "paid") router.push(`/pedido/${order.referenceId}`)
106
+ }
107
+ }, 5000)
108
+ return () => clearInterval(id)
109
+ }, [orderId, status])
110
+ ```
111
+
112
+ ## Múltiplos grupos de frete
113
+
114
+ ```ts
115
+ // NUNCA assuma fulfillmentGroups[0] apenas
116
+ // Itere todos os grupos:
117
+ for (const group of cart.checkout.fulfillmentGroups) {
118
+ await updateFulfillmentOptionsForGroup({ cartId, cartToken, fulfillmentGroupId: group._id })
119
+ }
120
+ // Ao selecionar frete, selecionar para CADA grupo
121
+ // No placeOrder, incluir TODOS os grupos
122
+ ```
123
+
124
+ ## Página de confirmação (/pedido/[referenceId])
125
+
126
+ ```ts
127
+ // Buscar pedido por referenceId
128
+ // Status: use orderStatusLabel(order.status) — NUNCA displayStatus(language)
129
+ // Rastreio: order.fulfillmentGroups.data[0].tracking.trackingCode (não trackingUrl)
130
+ ```
131
+
132
+ ## Submit do formulário — botão único
133
+
134
+ ```tsx
135
+ // Prevenir duplo clique:
136
+ const [submitting, setSubmitting] = useState(false)
137
+ async function handleSubmit() {
138
+ if (submitting) return
139
+ setSubmitting(true)
140
+ try { await placeOrder() }
141
+ finally { setSubmitting(false) }
142
+ }
143
+ <button disabled={submitting} aria-busy={submitting}>
144
+ {submitting ? "Processando..." : "Finalizar Pedido"}
145
+ </button>
146
+ ```
@@ -0,0 +1,130 @@
1
+ # Agente 07 — Auth (OTP Login)
2
+
3
+ > ⚠️ **Doc de plano (histórico).** A implementação entregue pela foundation difere em nomes e
4
+ > caminhos de arquivo citados abaixo. O mapa REAL de arquivos por área é a tabela "Agentes
5
+ > 00-11" do `agents/MANAGER.md` — em conflito, valem o MANAGER e o código.
6
+
7
+ ## Escopo
8
+ Login passwordless (OTP por e-mail), gestão de sessão via cookie httpOnly, e proteção de rotas.
9
+
10
+ ## Dependências
11
+ - Agente 00 (Scaffold) — `lib/auth.ts` base
12
+
13
+ ## Arquivos proprietários
14
+
15
+ | Arquivo | Descrição |
16
+ |---------|-----------|
17
+ | `app/(loja)/conta/entrar/page.tsx` | Página de login (2 steps: email → código) |
18
+ | `app/api/account/otp/route.ts` | Dispara OTP por e-mail |
19
+ | `app/api/account/signin/route.ts` | Troca código OTP pelo token do cliente |
20
+ | `app/api/account/signout/route.ts` | Limpa cookie e redireciona |
21
+ | `lib/auth.ts` | getCustomerToken / setCustomerToken / clearCustomerToken |
22
+ | `middleware.ts` | Protege /conta/pedidos e /conta/assinaturas |
23
+ | `components/auth/OTPForm.tsx` | Formulário em dois steps |
24
+
25
+ ## lib/auth.ts (completo)
26
+
27
+ ```ts
28
+ import "server-only"
29
+ import { cookies } from "next/headers"
30
+
31
+ const COOKIE_NAME = "unbox_customer_token"
32
+ const COOKIE_OPTS = {
33
+ httpOnly: true,
34
+ secure: process.env.NODE_ENV === "production",
35
+ sameSite: "lax" as const,
36
+ path: "/",
37
+ }
38
+
39
+ export async function getCustomerToken(): Promise<string | undefined> {
40
+ return (await cookies()).get(COOKIE_NAME)?.value
41
+ }
42
+
43
+ export async function setCustomerToken(token: string, maxAge = 7 * 24 * 60 * 60) {
44
+ (await cookies()).set(COOKIE_NAME, token, { ...COOKIE_OPTS, maxAge })
45
+ }
46
+
47
+ export async function clearCustomerToken() {
48
+ (await cookies()).delete(COOKIE_NAME)
49
+ }
50
+ ```
51
+
52
+ ## /api/auth/otp (POST) — CRÍTICO
53
+
54
+ ```ts
55
+ // ⚠️ Manda e-mail REAL — rate-limit obrigatório
56
+ // Rate-limit simples: Map<email, lastSentAt>
57
+ const rateLimitMap = new Map<string, number>()
58
+
59
+ export async function POST(req: Request) {
60
+ const { email } = await req.json()
61
+
62
+ // Rate-limit: 1 por minuto por email
63
+ const last = rateLimitMap.get(email) ?? 0
64
+ if (Date.now() - last < 60_000) {
65
+ return Response.json({ error: "Aguarde antes de reenviar o código" }, { status: 429 })
66
+ }
67
+ rateLimitMap.set(email, Date.now())
68
+
69
+ const client = await getUnboxClient()
70
+ await client.gql(
71
+ `mutation($i:CustomerOTPRequestInput!){ customerOTPRequest(input:$i){success} }`,
72
+ { i: { email, shopId: process.env.UNBOX_SHOP_ID } },
73
+ { captcha: true } // ⚠️ obrigatório — SDK envia x-captcha-verification
74
+ )
75
+ return Response.json({ success: true })
76
+ }
77
+ ```
78
+
79
+ ## /api/auth/signin (POST)
80
+
81
+ ```ts
82
+ export async function POST(req: Request) {
83
+ const { email, code } = await req.json()
84
+ const client = await getUnboxClient()
85
+ const data = await client.gql(
86
+ `mutation($i:CustomerPasswordlessSignInInput){
87
+ customerPasswordlessSignIn(input:$i){
88
+ accessToken firstAccess newShopSignIn
89
+ }
90
+ }`,
91
+ { i: { email, otpCode: code, shopId: process.env.UNBOX_SHOP_ID } },
92
+ { captcha: true }
93
+ )
94
+ await setCustomerToken(data.customerPasswordlessSignIn.accessToken)
95
+ return Response.json({ success: true, firstAccess: data.customerPasswordlessSignIn.firstAccess })
96
+ }
97
+ ```
98
+
99
+ ## middleware.ts
100
+
101
+ ```ts
102
+ import { NextResponse } from "next/server"
103
+ import type { NextRequest } from "next/server"
104
+
105
+ export function middleware(req: NextRequest) {
106
+ const token = req.cookies.get("unbox_customer_token")?.value
107
+ if (!token) {
108
+ const url = req.nextUrl.clone()
109
+ url.pathname = "/conta/entrar"
110
+ url.searchParams.set("next", req.nextUrl.pathname)
111
+ return NextResponse.redirect(url)
112
+ }
113
+ return NextResponse.next()
114
+ }
115
+
116
+ export const config = {
117
+ matcher: ["/conta/pedidos/:path*", "/conta/assinaturas/:path*"],
118
+ }
119
+ ```
120
+
121
+ ## OTPForm — UX
122
+
123
+ ```
124
+ Step 1: Campo de email + botão "Enviar código"
125
+ Step 2: Campo de 6 dígitos (autofoco) + botão "Entrar"
126
+ + botão "Reenviar código" (habilitado após 60s)
127
+
128
+ Erros: via friendlyError() do lib/feedback.ts
129
+ Após sucesso: redirect para ?next ou /conta/pedidos
130
+ ```
@@ -0,0 +1,55 @@
1
+ # Agente 08 — Customer (Área da Conta)
2
+
3
+ ## Escopo
4
+ Área logada do cliente: visão geral, pedidos, assinaturas, endereços e preferências. Tudo
5
+ protegido por sessão via cookie httpOnly (token do **cliente**, distinto do token de loja).
6
+
7
+ ## Dependências
8
+ - Agente 07 (Auth) — `lib/session.ts` (`getCustomerToken`), fluxo de login OTP
9
+ - Agente 00 (Scaffold) — `lib/unbox/customer.ts` (`UnboxCustomerClient`)
10
+
11
+ ## Arquivos proprietários
12
+
13
+ | Arquivo | Descrição |
14
+ |---------|-----------|
15
+ | `app/(loja)/conta/page.tsx` | Visão geral (resumo de pedidos/assinaturas recentes) |
16
+ | `app/(loja)/conta/pedidos/page.tsx` + `[referenceId]/page.tsx` | Lista e detalhe de pedidos |
17
+ | `app/(loja)/conta/assinaturas/page.tsx` + `[referenceId]/page.tsx` | Lista e detalhe de assinaturas (pausar/cancelar/pular ciclo) |
18
+ | `app/(loja)/conta/enderecos/page.tsx` | CRUD de endereços salvos |
19
+ | `app/(loja)/conta/preferencias/page.tsx` | Preferências de marketing/notificação |
20
+ | `components/account/account-shell.tsx` | Shell com sidebar de navegação + breadcrumb, usado por todas as páginas acima |
21
+ | `components/account/address-book.tsx` | Lista + formulário de endereços |
22
+ | `components/account/preferences-form.tsx` | Formulário de preferências |
23
+ | `components/account/subscription-actions.tsx` | Ações de assinatura (pausar/cancelar/pular) |
24
+ | `components/account/reorder-button.tsx` | "Comprar novamente" a partir de um pedido |
25
+ | `components/account/signout-button.tsx` | Logout |
26
+ | `lib/customer-session.ts` | `getCustomerClient()` / `requireCustomerClient()` — client autenticado com o token do cliente |
27
+ | `app/api/account/{me,addresses,preferences,signout}/route.ts` | Route Handlers que usam `requireCustomerClient()` |
28
+
29
+ ## Regra de ouro — dois tokens
30
+ `lib/customer-session.ts` usa o token do **cliente** (cookie httpOnly, setado pelo Agente 07),
31
+ nunca o `serverEnv.apiKey`/token de loja do Agente 00 (`lib/unbox/store.ts`). Misturar os dois
32
+ client factories é o erro mais comum nessa área — sempre confirme qual client está sendo importado.
33
+
34
+ ```ts
35
+ // Server Component de página protegida:
36
+ const client = await getCustomerClient();
37
+ if (!client) redirect("/conta/entrar?next=/conta/pedidos");
38
+ ```
39
+
40
+ ## Proteção de rotas
41
+ Páginas sob `/conta/*` (exceto `/conta/entrar`) checam `getCustomerClient()` e redirecionam para
42
+ `/conta/entrar?next=<rota>` se não houver sessão — sem `middleware.ts` global, a checagem é feita
43
+ em cada `page.tsx` (Server Component) para poder usar `AccountShell` mesmo no estado deslogado de
44
+ algumas páginas públicas (ex.: rastreio de pedido pelo `/pedido/[referenceId]` não exige login).
45
+
46
+ ## Assinaturas — ações sensíveis
47
+ `subscription-actions.tsx` chama mutations que alteram cobrança recorrente (pausar/cancelar/pular
48
+ ciclo) — sempre exigir confirmação explícita (dialog) antes de disparar, e mostrar o novo estado
49
+ otimisticamente só após resposta 200 da API.
50
+
51
+ ## Link de recuperação e frequência de assinatura
52
+ `recurringItemsFrequencyId` é um campo do pedido (`placeOrder`), não do carrinho, e por isso
53
+ vive em cookie no meio do caminho — ao restaurar um carrinho de assinatura por link (`?id=&token=`, doc 05-cart), é
54
+ obrigatório também repassar `&freq=` (ver `app/api/cart/link/route.ts`), senão o cliente reabre o
55
+ checkout sem frequência selecionada e não consegue finalizar.
@@ -0,0 +1,62 @@
1
+ # Agente 09 — Promotions (Kits & Combos)
2
+
3
+ ## Escopo
4
+ Sistema de kits/combos promocionais — bundles curados de produtos com desconto, resolvidos
5
+ dinamicamente contra o catálogo real da loja (preço e estoque nunca são hardcoded).
6
+
7
+ ## Dependências
8
+ - Agente 03 (Catalog) — usa o mesmo catálogo (`getCatalog`) e `lib/enrichment` para indexar produtos
9
+ - Agente 02 (Homepage) — consome `<CombosSection>`/`<CombosHome>` na home
10
+
11
+ ## Arquivos proprietários
12
+
13
+ | Arquivo | Descrição |
14
+ |---------|-----------|
15
+ | `lib/enrichment/combos.ts` | Define `COMBOS` (specs do kit) e `resolveCombos()` (resolve contra o catálogo) |
16
+ | `components/home/combos-section.tsx` | Seção completa da home: carrossel + CTA "Adicionar combo ao carrinho" |
17
+ | `components/home/combos-home.tsx` | Wrapper que busca o catálogo e chama `resolveCombos()` |
18
+ | `components/home/combo-card-compact.tsx` | Card compacto reusado em outros pontos (ex.: cross-sell) |
19
+
20
+ ## ⚠️ Estado no template base: vazio por padrão
21
+ `COMBOS: ComboDef[] = []` — a infraestrutura está pronta e testada, mas **sem nenhum kit
22
+ configurado**. Sem isso, `<CombosSection>` não renderiza nada (degrada graciosamente, não é erro),
23
+ mas a loja não tem essa alavanca de AOV até alguém preencher `COMBOS` com kits reais.
24
+
25
+ ## Modelo de dados
26
+ Um `ComboDef` referencia **famílias de produto** (`familyCode` do `lib/enrichment/products.json`),
27
+ não SKUs fixos — a resolução escolhe o produto comprável mais barato (ou mais caro, via
28
+ `size: "max"`) daquela família no catálogo atual:
29
+
30
+ ```ts
31
+ // lib/enrichment/combos.ts — popule com os kits reais da loja
32
+ export const COMBOS: ComboDef[] = [
33
+ {
34
+ id: "kit-essencial",
35
+ name: "Kit Essencial",
36
+ description: "Tudo o que você precisa para começar.",
37
+ imageUrl: "/brand/combos/kit-essencial.webp",
38
+ badge: "MAIS VENDIDO",
39
+ discountPct: 15,
40
+ highlights: [{ icon: "sparkle", label: "3 itens" }],
41
+ items: [{ family: "FAMILIA_A" }, { family: "FAMILIA_B" }, { family: "FAMILIA_C", size: "min" }],
42
+ },
43
+ ];
44
+ ```
45
+
46
+ ## Regra de ouro — nunca hardcode preço
47
+ `resolveCombos()` calcula `subtotal`/`total`/`save` a partir do preço real do catálogo
48
+ (`resolveProductPrice`) multiplicado por `(1 - discountPct/100)` — **o componente nunca recebe
49
+ preço fixo**. Um combo cujos itens não resolvem no catálogo atual (produto fora de linha, sem
50
+ estoque) é descartado silenciosamente (`items.length < 2` → `continue`), nunca quebra a home.
51
+
52
+ ## Ao configurar pra uma loja nova
53
+ 1. Popular `lib/enrichment/products.json` à mão, no formato de `lib/enrichment/index.ts` (não há script gerador)
54
+ 2. Definir `familyCode` para os grupos de produto que formarão kits
55
+ 3. Preencher `COMBOS` em `combos.ts` com os kits desejados (`items`, `discountPct`, `highlights`)
56
+ 4. Adicionar as imagens temáticas em `public/brand/combos/`
57
+
58
+ ## Fora de escopo aqui — não confundir com CRO
59
+ Banners de oferta genéricos ("shopSales" da loja), contadores de urgência e prova social **não**
60
+ são deste agente — isso é escopo do Agente 13 (CRO Package), que hoje só tem o módulo de frete
61
+ grátis (`FREE_SHIPPING_THRESHOLD`) implementado; os outros 9 módulos ainda não existem no template
62
+ base.
@@ -0,0 +1,128 @@
1
+ # Agente 10 — Feedback / UX
2
+
3
+ > ⚠️ **Doc de plano (histórico).** A implementação entregue pela foundation difere em nomes e
4
+ > caminhos de arquivo citados abaixo. O mapa REAL de arquivos por área é a tabela "Agentes
5
+ > 00-11" do `agents/MANAGER.md` — em conflito, valem o MANAGER e o código.
6
+
7
+ ## Escopo
8
+ Padrões de UX compartilhados: toasts, skeletons, estados de loading, error boundaries e páginas
9
+ especiais (error.tsx, not-found.tsx).
10
+
11
+ ## Dependências
12
+ - Agente 00 (Scaffold) — `lib/feedback.ts` base
13
+ - Reutilizado por todos os outros agentes
14
+
15
+ ## Arquivos proprietários
16
+
17
+ | Arquivo | Descrição |
18
+ |---------|-----------|
19
+ | `components/ui/Toast.tsx` | Componente de toast |
20
+ | `components/ui/ToastProvider.tsx` | Provider global com useToast hook |
21
+ | `components/ui/skeleton.tsx` | Skeleton polimórfico com animate-pulse |
22
+ | `components/ui/LoadingSpinner.tsx` | Spinner SVG com aria-label |
23
+ | `components/ui/ErrorBoundary.tsx` | Client error boundary |
24
+ | `components/ui/EmptyState.tsx` | Estado vazio genérico |
25
+ | `app/error.tsx` | Erro global (client component obrigatório) |
26
+ | `app/not-found.tsx` | 404 |
27
+ | `app/loading.tsx` | Fallback de Suspense global |
28
+ | `lib/feedback.ts` | Re-exports + mapa de erros de pagamento |
29
+
30
+ ## lib/feedback.ts (completo)
31
+
32
+ ```ts
33
+ export { friendlyError, cartEventLabel, ERROR_MESSAGES, CART_EVENT_LABELS } from "@payflows/unbox-sdk"
34
+
35
+ export const CHECKOUT_STEPS = {
36
+ address: "Endereço",
37
+ shipping: "Frete",
38
+ payment: "Pagamento",
39
+ confirmation: "Confirmação",
40
+ } as const
41
+
42
+ export function getPaymentErrorMessage(code: string): string {
43
+ const map: Record<string, string> = {
44
+ INSUFFICIENT_FUNDS_ERROR: "Cartão sem limite disponível",
45
+ CARD_DECLINED: "Cartão recusado pela operadora",
46
+ INVALID_CARD: "Dados do cartão inválidos",
47
+ EXPIRED_CARD: "Cartão expirado",
48
+ PAYMENT_PROCESSING_ERROR: "Erro ao processar pagamento. Tente novamente.",
49
+ }
50
+ return map[code] ?? "Erro no pagamento. Tente outro cartão ou método de pagamento."
51
+ }
52
+ ```
53
+
54
+ ## ToastProvider + useToast
55
+
56
+ ```tsx
57
+ // Não depender de Radix/shadcn toast — usar Base UI ou implementação própria
58
+ type ToastType = "success" | "error" | "info" | "warning"
59
+ interface ToastItem { id: string; title: string; description?: string; type: ToastType }
60
+
61
+ const ToastContext = createContext<{
62
+ toast(params: Omit<ToastItem, "id">): void
63
+ dismiss(id: string): void
64
+ }>()
65
+
66
+ export function useToast() { return useContext(ToastContext) }
67
+
68
+ // Toast com auto-dismiss em 5s, acessível com role="status" aria-live="polite"
69
+ ```
70
+
71
+ ## Skeleton — uso
72
+
73
+ ```tsx
74
+ // Genérico (polimórfico):
75
+ <Skeleton className="h-4 w-32" /> // linha de texto
76
+ <Skeleton className="aspect-square w-full" /> // imagem
77
+
78
+ // Compostos:
79
+ export function ProductCardSkeleton() {
80
+ return (
81
+ <div>
82
+ <Skeleton className="aspect-square w-full rounded-lg" />
83
+ <Skeleton className="mt-2 h-4 w-3/4" />
84
+ <Skeleton className="mt-1 h-4 w-1/2" />
85
+ </div>
86
+ )
87
+ }
88
+ export function ProductGridSkeleton({ count = 8 }: { count?: number }) {
89
+ return (
90
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
91
+ {Array.from({ length: count }).map((_, i) => <ProductCardSkeleton key={i} />)}
92
+ </div>
93
+ )
94
+ }
95
+ ```
96
+
97
+ ## app/error.tsx (CRÍTICO)
98
+
99
+ ```tsx
100
+ "use client" // ← obrigatório pelo Next.js
101
+ import { useEffect } from "react"
102
+
103
+ export default function Error({ error, reset }: { error: Error & { digest?: string }; reset(): void }) {
104
+ useEffect(() => {
105
+ // Log interno — nunca exibir ao usuário
106
+ console.error("[Error Boundary]", error.message)
107
+ }, [error])
108
+
109
+ return (
110
+ <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
111
+ <h2>Algo deu errado</h2>
112
+ <p>Ocorreu um erro inesperado. Tente novamente.</p>
113
+ <button onClick={reset}>Tentar novamente</button>
114
+ </div>
115
+ )
116
+ }
117
+ ```
118
+
119
+ ## Acessibilidade — checklist obrigatório
120
+
121
+ ```
122
+ - aria-live="polite" nos toasts (não "assertive" para não interromper)
123
+ - aria-busy="true" em botões durante loading
124
+ - role="status" em mensagens de feedback
125
+ - Não remover outline/foco visível (apenas customizar cor)
126
+ - Focus trap em drawers e dialogs (Base UI já cuida)
127
+ - Skip link: <a href="#main-content" className="sr-only focus:not-sr-only">Pular para conteúdo</a>
128
+ ```