@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
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@unbox-plus/cli",
3
+ "version": "0.20.4",
4
+ "author": {
5
+ "name": "Unbox Development Team",
6
+ "email": "devs@unbox.com.br",
7
+ "url": "https://unbox.com.br"
8
+ },
9
+ "description": "Scaffold a new Unbox storefront (Next.js) from the official template",
10
+ "keywords": [
11
+ "unbox",
12
+ "storefront",
13
+ "ecommerce"
14
+ ],
15
+ "license": "SEE LICENSE IN LICENSE.md",
16
+ "type": "module",
17
+ "bin": {
18
+ "create-unbox-store": "./bin/cli.js"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "src",
23
+ "template",
24
+ "tools"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20.11"
28
+ },
29
+ "dependencies": {
30
+ "prompts": "^2.4.2"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/unbox-plus/unbox-cli.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/unbox-plus/unbox-cli/issues"
38
+ },
39
+ "scripts": {
40
+ "check:neutro": "node tools/check-template-neutro.mjs",
41
+ "prepack": "node tools/check-template-neutro.mjs"
42
+ }
43
+ }
package/src/colors.js ADDED
@@ -0,0 +1,100 @@
1
+ // Matemática de cor mínima pra derivar a paleta da loja a partir de 2-3 cores escolhidas pelo
2
+ // usuário (primária + CTA, opcionalmente chrome). Sem dependências externas.
3
+
4
+ const HEX_RE = /^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/;
5
+
6
+ export function normalizeHex(input) {
7
+ const m = HEX_RE.exec(input.trim());
8
+ if (!m) return null;
9
+ let hex = m[1];
10
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
11
+ return `#${hex.toUpperCase()}`;
12
+ }
13
+
14
+ function hexToRgb(hex) {
15
+ const n = parseInt(hex.slice(1), 16);
16
+ return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
17
+ }
18
+
19
+ function rgbToHex({ r, g, b }) {
20
+ const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
21
+ return `#${c(r)}${c(g)}${c(b)}`.toUpperCase();
22
+ }
23
+
24
+ /** Mistura `hex` com `target` (preto/branco) por `amount` (0-1). amount=0.2 → 20% em direção ao target. */
25
+ function mix(hex, target, amount) {
26
+ const a = hexToRgb(hex);
27
+ const b = hexToRgb(target);
28
+ return rgbToHex({
29
+ r: a.r + (b.r - a.r) * amount,
30
+ g: a.g + (b.g - a.g) * amount,
31
+ b: a.b + (b.b - a.b) * amount,
32
+ });
33
+ }
34
+
35
+ export const darken = (hex, amount = 0.15) => mix(hex, "#000000", amount);
36
+ export const lighten = (hex, amount = 0.15) => mix(hex, "#ffffff", amount);
37
+
38
+ /** Luminância relativa (WCAG simplificado) — decide se o texto sobre `hex` deve ser claro ou escuro. */
39
+ function relativeLuminance(hex) {
40
+ const { r, g, b } = hexToRgb(hex);
41
+ const lin = (v) => {
42
+ const c = v / 255;
43
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
44
+ };
45
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
46
+ }
47
+
48
+ /** Texto de contraste (preto ou branco) pra ficar legível sobre `bgHex`. */
49
+ export function contrastText(bgHex) {
50
+ return relativeLuminance(bgHex) > 0.45 ? "#1A1B18" : "#FFFFFF";
51
+ }
52
+
53
+ /**
54
+ * Deriva os tokens de marca a partir de `primary` + `cta` (e opcionalmente `chromeBg`),
55
+ * mais o MODO de chrome do preset:
56
+ * - "brand-dark" (default): chrome = primária escurecida (comportamento histórico)
57
+ * - "ink-dark": chrome = tinta do estilo escurecida (neutro, não "cor da marca")
58
+ * - "light": chrome claro (superfície do estilo, texto na tinta)
59
+ * Mesmo conjunto de variáveis do bloco "CONFIGURE" em app/globals.css.
60
+ */
61
+ export function deriveBrandTokens({ primary, cta, chromeBg }, preset) {
62
+ const mode = preset?.chrome ?? "brand-dark";
63
+ const neutrals = preset?.neutrals ?? {};
64
+ const ink = neutrals["--store-ink"] ?? "#20211E";
65
+
66
+ let resolvedChromeBg, chromeText, chromeMuted;
67
+ if (chromeBg) {
68
+ resolvedChromeBg = chromeBg;
69
+ chromeText = contrastText(resolvedChromeBg);
70
+ chromeMuted = mix(resolvedChromeBg, chromeText, 0.55);
71
+ } else if (mode === "light") {
72
+ resolvedChromeBg = neutrals["--store-surface"] ?? "#FAF8F3";
73
+ chromeText = ink;
74
+ chromeMuted = neutrals["--store-muted"] ?? "#85816F";
75
+ } else if (mode === "ink-dark") {
76
+ resolvedChromeBg = darken(ink, 0.35);
77
+ chromeText = contrastText(resolvedChromeBg);
78
+ chromeMuted = mix(resolvedChromeBg, "#FFFFFF", 0.55);
79
+ } else {
80
+ resolvedChromeBg = darken(primary, 0.55);
81
+ chromeText = contrastText(resolvedChromeBg);
82
+ chromeMuted = mix(resolvedChromeBg, "#FFFFFF", 0.55);
83
+ }
84
+
85
+ return {
86
+ "--store-primary": primary,
87
+ "--store-primary-dark": darken(primary, 0.15),
88
+ "--store-primary-soft": lighten(primary, 0.88),
89
+ "--store-cta": cta,
90
+ "--store-cta-fg": contrastText(cta),
91
+ "--store-cta-dark": darken(cta, 0.15),
92
+ "--store-cta-soft": lighten(cta, 0.8),
93
+ "--store-chrome-bg": resolvedChromeBg,
94
+ "--store-chrome-text": chromeText,
95
+ "--store-chrome-muted": chromeMuted,
96
+ // divisores DENTRO do chrome (rodapé). Derivado do texto pra funcionar também em
97
+ // chrome claro — um branco fixo a 10% sumiria por completo lá.
98
+ "--store-chrome-line": mix(resolvedChromeBg, chromeText, 0.14),
99
+ };
100
+ }
package/src/presets.js ADDED
@@ -0,0 +1,241 @@
1
+ // Presets de ESTILO visual da loja: receita da home + tipografia + neutros + chrome +
2
+ // radius + hero. O CLI sugere um (suggestPreset), o usuário confirma no select, e
3
+ // applyPreset (theme.js) escreve tudo no projeto gerado. O agente de branding refina
4
+ // depois no briefing (agents/PADROES.md documenta o repertório).
5
+ //
6
+ // Regras dos presets:
7
+ // - "essencial" é o default equilibrado (não mais "paridade com o visual histórico":
8
+ // header sem barra de busca e hero full-bleed, que é o que tira a cara de template).
9
+ // - Fontes: só famílias do Google Fonts com pesos 500-800 (o design usa font-extrabold).
10
+ // - Neutros: hex literais por preset (o CLI escreve nos tokens --store-* de globals.css).
11
+
12
+ export const PRESETS = {
13
+ essencial: {
14
+ // dials do briefing (1-10) — variância de composição, intensidade de motion,
15
+ // densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
16
+ dials: { variancia: 5, motion: 3, densidade: 6 },
17
+ label: "Essencial",
18
+ tagline: "layout completo e equilibrado, funciona pra qualquer loja",
19
+ fonts: {
20
+ sans: { fn: "Plus_Jakarta_Sans", weights: ["400", "500", "600", "700"] },
21
+ display: { fn: "Poppins", weights: ["500", "600", "700", "800"] },
22
+ },
23
+ neutrals: {
24
+ "--store-ink": "#18181B",
25
+ "--store-ink-2": "#3F3F46",
26
+ "--store-muted": "#71717A",
27
+ "--store-faint": "#A1A1AA",
28
+ "--store-bg": "#FAFAFA",
29
+ "--store-surface": "#FFFFFF",
30
+ "--store-surface-2": "#F4F4F5",
31
+ "--store-line": "#E4E4E7",
32
+ "--store-line-2": "#D4D4D8",
33
+ "--store-sale": "#DC2626",
34
+ "--store-sale-soft": "#FEE9E9",
35
+ },
36
+ chrome: "brand-dark",
37
+ chromeRecipe: { header: "equilibrado", footer: "colunas" },
38
+ radius: "0.875rem",
39
+ axes: {
40
+ "--section-gap": "52px", "--container-max": "1240px",
41
+ "--card-bg": "#ffffff", "--card-border": "1px solid var(--store-line)", "--card-shadow": "none",
42
+ },
43
+ // hero full-bleed: o banner em card arredondado é o maior "cara de template" que existe.
44
+ hero: "imagem-imersiva",
45
+ home: [
46
+ { section: "hero", variant: "imagem-imersiva", props: { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", title: "Bem-vindo à nossa loja", subtitle: "Produtos escolhidos com cuidado, do nosso catálogo pra sua casa.", ctaLabel: "Ver destaques" } },
47
+ { section: "trust-bar" },
48
+ { section: "attributes-marquee" },
49
+ { section: "category-pills" },
50
+ { section: "combos-carousel" },
51
+ { section: "savings" },
52
+ { section: "benefits" },
53
+ { section: "media-cards" },
54
+ { section: "reviews", variant: "grid" },
55
+ { section: "ritual" },
56
+ { section: "founder-story" },
57
+ { section: "kits" },
58
+ { section: "spec-table" },
59
+ { section: "video-wall" },
60
+ { section: "trust-strip" },
61
+ { section: "newsletter", variant: "bloco" },
62
+ ],
63
+ },
64
+
65
+ promocional: {
66
+ // dials do briefing (1-10) — variância de composição, intensidade de motion,
67
+ // densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
68
+ dials: { variancia: 6, motion: 4, densidade: 9 },
69
+ label: "Promocional",
70
+ tagline: "oferta em primeiro plano: kits, economia e urgência pra subir o ticket",
71
+ fonts: {
72
+ sans: { fn: "Barlow", weights: ["400", "500", "600", "700"] },
73
+ display: { fn: "Barlow_Condensed", weights: ["500", "600", "700", "800"] },
74
+ },
75
+ neutrals: {
76
+ "--store-ink": "#09090B",
77
+ "--store-ink-2": "#27272A",
78
+ "--store-muted": "#71717A",
79
+ "--store-faint": "#A1A1AA",
80
+ "--store-bg": "#FFFFFF",
81
+ "--store-surface": "#FAFAFA",
82
+ "--store-surface-2": "#F4F4F5",
83
+ "--store-line": "#E4E4E7",
84
+ "--store-line-2": "#D4D4D8",
85
+ "--store-sale": "#E11D48",
86
+ "--store-sale-soft": "#FFE4E9",
87
+ },
88
+ chrome: "brand-dark",
89
+ chromeRecipe: { header: "compacto", footer: "conversao" },
90
+ radius: "1.25rem",
91
+ axes: {
92
+ "--section-gap": "44px", "--container-max": "1240px",
93
+ "--card-bg": "#ffffff", "--card-border": "1px solid transparent", "--card-shadow": "0 8px 28px rgba(0,0,0,0.07)",
94
+ },
95
+ hero: "carousel",
96
+ home: [
97
+ { section: "hero", variant: "carousel", props: { banners: [
98
+ { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", href: "#comprar" },
99
+ { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", href: "#comprar" },
100
+ { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg" },
101
+ ] } },
102
+ { section: "attributes-marquee" },
103
+ { section: "purchase-hero" },
104
+ { section: "product-showcase", props: { ctaHref: "#comprar" } },
105
+ { section: "savings" },
106
+ { section: "combos-carousel" },
107
+ { section: "benefits" },
108
+ { section: "stats-grid" },
109
+ { section: "reviews-carousel" },
110
+ { section: "media-cards" },
111
+ { section: "comparison" },
112
+ { section: "kits" },
113
+ { section: "ritual", props: { ctaLabel: "Montar meu pedido" } },
114
+ { section: "social-row" },
115
+ { section: "trust-strip" },
116
+ { section: "newsletter", variant: "bloco" },
117
+ ],
118
+ },
119
+
120
+ editorial: {
121
+ // dials do briefing (1-10) — variância de composição, intensidade de motion,
122
+ // densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
123
+ dials: { variancia: 7, motion: 5, densidade: 4 },
124
+ label: "Editorial",
125
+ tagline: "clima de clube e assinatura, com respiro e tipografia de revista",
126
+ fonts: {
127
+ sans: { fn: "Inter", weights: ["400", "500", "600", "700"] },
128
+ display: { fn: "Fraunces", weights: ["500", "600", "700", "800"] },
129
+ },
130
+ neutrals: {
131
+ "--store-ink": "#1C1917",
132
+ "--store-ink-2": "#44403C",
133
+ "--store-muted": "#78716C",
134
+ "--store-faint": "#A8A29E",
135
+ "--store-bg": "#FAFAF9",
136
+ "--store-surface": "#FFFFFF",
137
+ "--store-surface-2": "#F5F5F4",
138
+ "--store-line": "#E7E5E4",
139
+ "--store-line-2": "#D6D3D1",
140
+ "--store-sale": "#B91C1C",
141
+ "--store-sale-soft": "#FBE8E8",
142
+ },
143
+ chrome: "ink-dark",
144
+ chromeRecipe: { header: "centralizado", footer: "editorial" },
145
+ radius: "0.5rem",
146
+ axes: {
147
+ "--section-gap": "72px", "--container-max": "1080px",
148
+ "--card-bg": "var(--store-surface)", "--card-border": "1px solid transparent", "--card-shadow": "none",
149
+ },
150
+ hero: "split-editorial",
151
+ home: [
152
+ { section: "hero", variant: "split-editorial", props: { imageDesktop: "/brand/hero-desktop.svg", imageMobile: "/brand/hero-mobile.svg", title: "Feito com calma, entregue todo mês", subtitle: "Curadoria da nossa loja pra sua rotina. Conheça os destaques e, se fizer sentido, assine.", ctaLabel: "Ver destaques" } },
153
+ { section: "attributes-marquee" },
154
+ { section: "benefits" },
155
+ { section: "combos-carousel" },
156
+ { section: "founder-story" },
157
+ { section: "ritual" },
158
+ { section: "media-cards" },
159
+ { section: "reviews", variant: "faixa" },
160
+ { section: "quote-banner" },
161
+ { section: "kits" },
162
+ { section: "spec-table" },
163
+ { section: "social-row" },
164
+ { section: "newsletter", variant: "inline" },
165
+ ],
166
+ },
167
+
168
+ boutique: {
169
+ // dials do briefing (1-10) — variância de composição, intensidade de motion,
170
+ // densidade visual. O agente 15 ajusta conforme a marca; ver agents/PADROES.md.
171
+ dials: { variancia: 8, motion: 6, densidade: 2 },
172
+ label: "Boutique",
173
+ tagline: "minimal e premium: poucas seções, muito respiro, detalhes finos",
174
+ fonts: {
175
+ sans: { fn: "DM_Sans", weights: ["400", "500", "600", "700"] },
176
+ display: { fn: "Playfair_Display", weights: ["500", "600", "700", "800"] },
177
+ },
178
+ neutrals: {
179
+ "--store-ink": "#1A1A1F",
180
+ "--store-ink-2": "#3F3F49",
181
+ "--store-muted": "#71717D",
182
+ "--store-faint": "#A5A5B0",
183
+ "--store-bg": "#FBFBFC",
184
+ "--store-surface": "#FFFFFF",
185
+ "--store-surface-2": "#F4F4F6",
186
+ "--store-line": "#E8E8EC",
187
+ "--store-line-2": "#DCDCE2",
188
+ "--store-sale": "#A63D2F",
189
+ "--store-sale-soft": "#F6E7E3",
190
+ },
191
+ chrome: "ink-dark",
192
+ chromeRecipe: { header: "imersivo", footer: "minimal" },
193
+ radius: "0.25rem",
194
+ axes: {
195
+ "--section-gap": "88px", "--container-max": "1000px",
196
+ "--card-bg": "#ffffff", "--card-border": "1px solid var(--store-line-2)", "--card-shadow": "none",
197
+ },
198
+ // hero imersivo: é o par do header "imersivo" (a foto sobe por baixo dele e o header
199
+ // fica transparente). Sem foto boa da marca, troque na receita por "minimal-texto" —
200
+ // o header degrada pra sólido sozinho.
201
+ hero: "imagem-imersiva",
202
+ home: [
203
+ { section: "hero", variant: "imagem-imersiva", props: { title: "Peças escolhidas a dedo", subtitle: "Um catálogo enxuto, pensado pra durar. Conheça os destaques da casa.", ctaLabel: "Ver destaques" } },
204
+ { section: "combos-carousel" },
205
+ { section: "benefits" },
206
+ { section: "media-cards" },
207
+ { section: "quote-banner" },
208
+ { section: "reviews", variant: "faixa" },
209
+ { section: "founder-story" },
210
+ { section: "kits" },
211
+ { section: "spec-table" },
212
+ { section: "newsletter", variant: "inline" },
213
+ ],
214
+ },
215
+ };
216
+
217
+ export const PRESET_NAMES = Object.keys(PRESETS);
218
+
219
+ // Heurística de sugestão: soma pontos por palavra-chave (sem acento, lowercase) no
220
+ // objetivo/site/instagram. Empate: promocional > editorial > boutique. Zero: essencial.
221
+ const KEYWORDS = {
222
+ promocional: ["ticket", "upsell", "kit", "combo", "oferta", "promoc", "desconto", "atacado", "black", "volume", "venda", "conversao", "barato"],
223
+ editorial: ["assinatura", "recorren", "clube", "curadoria", "conteudo", "comunidade", "blog", "receita", "ritual", "mensal"],
224
+ boutique: ["premium", "minimal", "luxo", "sofistic", "estetic", "design", "autoral", "exclusiv", "boutique", "atelie", "artesanal", "fino"],
225
+ };
226
+
227
+ function normalize(s) {
228
+ return (s ?? "").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
229
+ }
230
+
231
+ /** Sugere um preset a partir do que o usuário contou no formulário. Nunca lança. */
232
+ export function suggestPreset({ objetivo, site, instagram } = {}) {
233
+ const haystack = [objetivo, site, instagram].map(normalize).join(" ");
234
+ const scores = {};
235
+ for (const [preset, words] of Object.entries(KEYWORDS)) {
236
+ scores[preset] = words.reduce((acc, w) => acc + (haystack.includes(w) ? 1 : 0), 0);
237
+ }
238
+ const order = ["promocional", "editorial", "boutique"]; // desempate
239
+ const best = order.reduce((a, b) => (scores[b] > scores[a] ? b : a), "promocional");
240
+ return scores[best] > 0 ? best : "essencial";
241
+ }
package/src/theme.js ADDED
@@ -0,0 +1,253 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { PRESETS } from "./presets.js";
4
+
5
+ /**
6
+ * Substitui os valores dos tokens `--store-*` (e `--radius`) no bloco "CONFIGURE" de
7
+ * app/globals.css. Só troca o valor depois de `:`, nunca a chave nem o resto do arquivo.
8
+ * Aceita hex e valores rem (radius).
9
+ *
10
+ * A regex é ANCORADA no início da linha (`^\s*`) e proibida de atravessar quebra de linha
11
+ * (`[^;\n]`) por um motivo concreto: sem isso, uma MENÇÃO ao token dentro de um comentário
12
+ * (ex.: "o token --motion: 0 desliga tudo") casava primeiro, e o `[^;]+` engolia tudo até o
13
+ * `;` seguinte — várias linhas adiante, levando junto o fechamento do comentário e o
14
+ * `@import` do Tailwind. O CSS saía quebrado e só o build acusava, com erro que não apontava
15
+ * pra causa. Declaração de token vive sempre sozinha na linha; menção em prosa, não.
16
+ */
17
+ export function applyBrandTokens(targetDir, tokens) {
18
+ const cssPath = path.join(targetDir, "app", "globals.css");
19
+ let css = fs.readFileSync(cssPath, "utf8");
20
+
21
+ for (const [name, value] of Object.entries(tokens)) {
22
+ const re = new RegExp(`^(\\s*${name}:\\s*)[^;\\n]+;`, "m");
23
+ if (!re.test(css)) {
24
+ throw new Error(`[create-unbox-store] token ${name} não encontrado em app/globals.css: a foundation pode ter mudado.`);
25
+ }
26
+ css = css.replace(re, `$1${value};`);
27
+ }
28
+
29
+ fs.writeFileSync(cssPath, css);
30
+ }
31
+
32
+ /**
33
+ * Reescreve o bloco UNBOX-FONTS de app/layout.tsx com o par tipográfico do preset.
34
+ * next/font exige declaração estática top-level — por isso replace de bloco marcado.
35
+ */
36
+ export function applyFonts(targetDir, preset) {
37
+ const layoutPath = path.join(targetDir, "app", "layout.tsx");
38
+ let src = fs.readFileSync(layoutPath, "utf8");
39
+ const re = /\/\/ UNBOX-FONTS-BEGIN[\s\S]*?\/\/ UNBOX-FONTS-END/;
40
+ if (!re.test(src)) {
41
+ throw new Error("[create-unbox-store] markers UNBOX-FONTS não encontrados em app/layout.tsx: a foundation pode ter mudado.");
42
+ }
43
+ const { sans, display } = preset.fonts;
44
+ const families = [...new Set(["Geist_Mono", sans.fn, display.fn])].sort();
45
+ const block = [
46
+ "// UNBOX-FONTS-BEGIN (bloco reescrito pelo create-unbox-store conforme o estilo escolhido; não renomear os markers)",
47
+ `import { ${families.join(", ")} } from "next/font/google";`,
48
+ `const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });`,
49
+ `const sans = ${sans.fn}({ variable: "--font-sans", subsets: ["latin"], weight: [${sans.weights.map((w) => `"${w}"`).join(", ")}], display: "swap" });`,
50
+ `const displayFont = ${display.fn}({ variable: "--font-display", subsets: ["latin"], weight: [${display.weights.map((w) => `"${w}"`).join(", ")}], display: "swap" });`,
51
+ "// UNBOX-FONTS-END",
52
+ ].join("\n");
53
+ fs.writeFileSync(layoutPath, src.replace(re, block));
54
+ }
55
+
56
+ /** Escreve a receita da home (components/home/home-recipe.ts) conforme o preset. */
57
+ /** Escreve components/chrome/chrome-recipe.ts — qual header e qual rodapé a loja usa.
58
+ * Mesma mecânica do applyHomeRecipe: reescreve o arquivo inteiro e falha alto se ele sumiu. */
59
+ export function applyChromeRecipe(targetDir, preset) {
60
+ const recipePath = path.join(targetDir, "components", "chrome", "chrome-recipe.ts");
61
+ if (!fs.existsSync(recipePath)) {
62
+ throw new Error("[create-unbox-store] components/chrome/chrome-recipe.ts não encontrado: a foundation pode ter mudado.");
63
+ }
64
+ const recipe = preset.chromeRecipe ?? { header: "classico", footer: "colunas" };
65
+ const content = `// ═══════════════════════════════════════════════════════════════════════════
66
+ // RECEITA DO CHROME: qual header e qual rodapé esta loja usa.
67
+ // Escrita pelo create-unbox-store conforme o ESTILO escolhido no setup; editada
68
+ // pelo agente de branding no briefing (é AQUI que o chrome muda, não reescreva
69
+ // o JSX de uma variante; catálogo em agents/PADROES.md).
70
+ // \`npm run typecheck\` acusa header/rodapé inexistente.
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+ import type { ChromeRecipe } from "@/components/chrome/registry";
73
+
74
+ export const chromeRecipe: ChromeRecipe = ${JSON.stringify(recipe, null, 2)};
75
+ `;
76
+ fs.writeFileSync(recipePath, content);
77
+ }
78
+
79
+ export function applyHomeRecipe(targetDir, preset) {
80
+ const recipePath = path.join(targetDir, "components", "home", "home-recipe.ts");
81
+ if (!fs.existsSync(recipePath)) {
82
+ throw new Error("[create-unbox-store] components/home/home-recipe.ts não encontrado: a foundation pode ter mudado.");
83
+ }
84
+ const body = JSON.stringify(preset.home, null, 2);
85
+ const content = `// ═══════════════════════════════════════════════════════════════════════════
86
+ // RECEITA DA HOME: a ordem, as variantes e as props das seções.
87
+ // Escrita pelo create-unbox-store conforme o ESTILO escolhido no setup; editada
88
+ // pelo agente de branding no briefing (é AQUI que o layout muda, não reescreva
89
+ // o JSX das seções; catálogo de seções/variantes em agents/PADROES.md).
90
+ // \`npm run typecheck\` acusa seção/variante inexistente.
91
+ // ═══════════════════════════════════════════════════════════════════════════
92
+ import type { HomeRecipe } from "@/components/home/sections/registry";
93
+
94
+ export const homeRecipe: HomeRecipe = ${body};
95
+ `;
96
+ fs.writeFileSync(recipePath, content);
97
+ }
98
+
99
+ // Cores DEFAULT usadas nos SVGs de hero placeholder — trocadas pelas da marca no scaffold.
100
+ // Sentinelas NEUTRAS (= os valores default da paleta), únicas entre si: o template cru já
101
+ // renderiza limpo e o recolorizador segue mapeando hex→token no scaffold.
102
+ const HERO_SVG_COLOR_SLOTS = {
103
+ "#18181B": "--store-chrome-bg",
104
+ "#3F3F46": "--store-ink-2",
105
+ "#27272A": "--store-primary",
106
+ "#D97706": "--store-cta",
107
+ "#F1F1F3": "--store-primary-soft",
108
+ "#FCFCFD": "--store-surface",
109
+ "#F4F4F5": "--store-surface-2",
110
+ };
111
+
112
+ /** Recolore um SVG trocando as cores default pelos valores da marca. */
113
+ function recolorSvg(svg, palette) {
114
+ for (const [hex, token] of Object.entries(HERO_SVG_COLOR_SLOTS)) {
115
+ const value = palette[token];
116
+ if (value) svg = svg.replaceAll(hex, value);
117
+ }
118
+ return svg;
119
+ }
120
+
121
+ /**
122
+ * Escolhe o par de heros placeholder do preset, recolore com a marca e grava como
123
+ * public/brand/hero-{desktop,mobile}.svg. Remove a pasta heros/ do projeto gerado.
124
+ * Também recolore TODOS os placeholders de mídia em public/brand/ph/ (fotos, posters,
125
+ * avatares usados pelas seções da biblioteca) — a loja nasce com a paleta da marca.
126
+ */
127
+ export function applyHeroAssets(targetDir, presetName, tokens, neutrals) {
128
+ const herosDir = path.join(targetDir, "public", "brand", "heros");
129
+ const palette = { ...neutrals, ...tokens };
130
+ for (const kind of ["desktop", "mobile"]) {
131
+ const src = path.join(herosDir, `${presetName}-${kind}.svg`);
132
+ if (!fs.existsSync(src)) {
133
+ throw new Error(`[create-unbox-store] hero placeholder ${presetName}-${kind}.svg não encontrado: a foundation pode ter mudado.`);
134
+ }
135
+ const svg = recolorSvg(fs.readFileSync(src, "utf8"), palette);
136
+ fs.writeFileSync(path.join(targetDir, "public", "brand", `hero-${kind}.svg`), svg);
137
+ }
138
+ fs.rmSync(herosDir, { recursive: true, force: true });
139
+
140
+ const phDir = path.join(targetDir, "public", "brand", "ph");
141
+ if (fs.existsSync(phDir)) {
142
+ for (const f of fs.readdirSync(phDir)) {
143
+ if (!f.endsWith(".svg")) continue;
144
+ const p = path.join(phDir, f);
145
+ fs.writeFileSync(p, recolorSvg(fs.readFileSync(p, "utf8"), palette));
146
+ }
147
+ }
148
+ }
149
+
150
+ /** Aplica o preset inteiro: tokens (cores+neutros+radius), fontes, receita e heros. */
151
+ export function applyPreset(targetDir, presetName, brandTokens) {
152
+ const preset = PRESETS[presetName];
153
+ if (!preset) throw new Error(`[create-unbox-store] estilo desconhecido: ${presetName}`);
154
+ // --motion vem do dial: 0 desliga as animações, 1 é o padrão, acima disso amplifica.
155
+ const motion = preset.dials?.motion != null ? (preset.dials.motion / 5).toFixed(2) : "1";
156
+ applyBrandTokens(targetDir, {
157
+ ...brandTokens, ...preset.neutrals, ...(preset.axes ?? {}),
158
+ "--radius": preset.radius, "--motion": motion,
159
+ });
160
+ applyFonts(targetDir, preset);
161
+ applyHomeRecipe(targetDir, preset);
162
+ applyChromeRecipe(targetDir, preset);
163
+ applyHeroAssets(targetDir, presetName, brandTokens, preset.neutrals);
164
+ // manifest.ts e opengraph-image.tsx: os DOIS lugares onde a cor precisa ser hex literal, porque
165
+ // nem o manifest do PWA nem o satori (ImageResponse) leem variavel de CSS. Sem reescrever aqui,
166
+ // a loja aplica a paleta da marca em tudo e mesmo assim instala no celular e compartilha no
167
+ // WhatsApp com a cor default da foundation.
168
+ const tokens = { ...preset.neutrals, ...brandTokens };
169
+ const chromeBg = tokens["--store-chrome-bg"];
170
+ const manifestPath = path.join(targetDir, "app", "manifest.ts");
171
+ if (chromeBg && fs.existsSync(manifestPath)) {
172
+ const content = fs.readFileSync(manifestPath, "utf8");
173
+ fs.writeFileSync(manifestPath, content.replace(/theme_color:\s*"[^"]*"/, `theme_color: "${chromeBg}"`));
174
+ }
175
+ const ogPath = path.join(targetDir, "app", "opengraph-image.tsx");
176
+ const ogBg = tokens["--store-primary"];
177
+ const ogFg = tokens["--store-chrome-text"] || "#FFFFFF";
178
+ if (ogBg && fs.existsSync(ogPath)) {
179
+ let og = fs.readFileSync(ogPath, "utf8");
180
+ og = og.replace(/const PRIMARY = "[^"]*";/, `const PRIMARY = "${ogBg}";`);
181
+ og = og.replace(/const PRIMARY_FG = "[^"]*";/, `const PRIMARY_FG = "${ogFg}";`);
182
+ fs.writeFileSync(ogPath, og);
183
+ }
184
+ if (preset.chrome === "light") {
185
+ fs.copyFileSync(
186
+ path.join(targetDir, "public", "brand", "logo.svg"),
187
+ path.join(targetDir, "public", "brand", "logo-chrome.svg"),
188
+ );
189
+ // Selo "Powered by Unbox": em rodapé CLARO a variante ativa é a com fundo preto
190
+ // (regra da Unbox). O default do template (transparente neon) serve os chromes escuros.
191
+ fs.copyFileSync(
192
+ path.join(targetDir, "public", "unbox", "powered-by-fundo-preto.png"),
193
+ path.join(targetDir, "public", "unbox", "powered-by.png"),
194
+ );
195
+ }
196
+ }
197
+
198
+ /** Troca o fallback "Minha Loja" pelo nome de exibição escolhido (header, footer e metadata). */
199
+ export function applyStoreName(targetDir, displayName) {
200
+ if (!displayName) return;
201
+ // Literal de string TS: escapa barra e aspas. E TODA substituição usa função (não string)
202
+ // — com string, um nome com `$&` ou `$'` vira padrão de replace e corrompe o TSX.
203
+ const safe = displayName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
204
+ const lit = () => `?? "${safe}"`;
205
+ const nome = () => safe;
206
+ const files = [
207
+ path.join(targetDir, "components", "site-header.tsx"),
208
+ path.join(targetDir, "components", "site-footer.tsx"),
209
+ ];
210
+ for (const f of files) {
211
+ if (!fs.existsSync(f)) continue;
212
+ const content = fs.readFileSync(f, "utf8");
213
+ fs.writeFileSync(f, content.replaceAll('?? "Minha Loja"', lit));
214
+ }
215
+ // metadata do app/layout.tsx (título da aba, template e OpenGraph siteName) e da PDP,
216
+ // que repete siteName no seu próprio openGraph — o App Router substitui o objeto
217
+ // openGraph inteiro do layout pai, não faz merge, então o campo precisa existir lá também.
218
+ for (const rel of [["app", "layout.tsx"], ["app", "(loja)", "produto", "[productSlug]", "page.tsx"], ["app", "(loja)", "page.tsx"], ["app", "opengraph-image.tsx"]]) {
219
+ const p = path.join(targetDir, ...rel);
220
+ if (!fs.existsSync(p)) continue;
221
+ fs.writeFileSync(p, fs.readFileSync(p, "utf8").replaceAll("Minha Loja", nome));
222
+ }
223
+ // app/manifest.ts (nome do PWA na tela inicial — some com o "Minha Loja"/"Loja" placeholder)
224
+ const manifestPath = path.join(targetDir, "app", "manifest.ts");
225
+ if (fs.existsSync(manifestPath)) {
226
+ const content = fs.readFileSync(manifestPath, "utf8");
227
+ fs.writeFileSync(manifestPath, content.replaceAll('?? "Minha Loja"', lit).replaceAll('?? "Loja"', lit));
228
+ }
229
+ // Porta de preview: o slug vira nome do cookie e título do negócio no Pipedrive
230
+ // ("<slug> - Nome do Lead"). Sem acento/espaço pra viver em nome de cookie.
231
+ const slug = displayName
232
+ .normalize("NFD").replace(/[̀-ͯ]/g, "")
233
+ .toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 30) || "loja";
234
+ for (const rel of [["middleware.ts"], ["app", "api", "acesso", "route.ts"]]) {
235
+ const p = path.join(targetDir, ...rel);
236
+ if (!fs.existsSync(p)) continue;
237
+ const content = fs.readFileSync(p, "utf8");
238
+ fs.writeFileSync(p, content.replaceAll('"minhaloja"', `"${slug}"`));
239
+ }
240
+ // Editor da Unbox: o MESMO slug vira o STORE_SLUG de lib/editable/config.ts (a foundation traz o
241
+ // marcador __STORE_SLUG__; é por ele que a loja pede o conteúdo publicado ao editor). Tem de ser
242
+ // IGUAL ao `slug` da entrada da loja no shops.json do editor, que segue esta mesma derivação
243
+ // (minúsculas, sem acento, só letras e números). Marcador ausente é erro, não silêncio: uma loja
244
+ // com "__STORE_SLUG__" no ar pede o publicado de uma loja que não existe e nunca muda.
245
+ const configEditavel = path.join(targetDir, "lib", "editable", "config.ts");
246
+ if (fs.existsSync(configEditavel)) {
247
+ const content = fs.readFileSync(configEditavel, "utf8");
248
+ if (!content.includes("__STORE_SLUG__")) {
249
+ throw new Error("[create-unbox-store] marcador __STORE_SLUG__ não encontrado em lib/editable/config.ts: a foundation do editor pode ter mudado.");
250
+ }
251
+ fs.writeFileSync(configEditavel, content.replaceAll("__STORE_SLUG__", () => slug));
252
+ }
253
+ }