create-snapify-app 0.0.0-stage → 1.1.0

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 (614) hide show
  1. package/LICENSE.md +24 -0
  2. package/README.md +55 -2
  3. package/bin/create-snapify-app.mjs +174 -0
  4. package/lib/args.mjs +59 -0
  5. package/lib/copy.mjs +44 -0
  6. package/lib/env.mjs +84 -0
  7. package/lib/prompt.mjs +63 -0
  8. package/package.json +33 -4
  9. package/template/.env.example +79 -0
  10. package/template/.github/workflows/ci.yml +23 -0
  11. package/template/.github/workflows/deploy.yml +26 -0
  12. package/template/.github/workflows/ping.yml +8 -0
  13. package/template/.github/workflows/release.yml +67 -0
  14. package/template/.snapify-install.json +5 -0
  15. package/template/CHANGELOG.md +28 -0
  16. package/template/LICENSE +14 -0
  17. package/template/README.md +287 -0
  18. package/template/administrator.html +23 -0
  19. package/template/apps/client/src/App.tsx +765 -0
  20. package/template/apps/client/src/auth.css +400 -0
  21. package/template/apps/client/src/lib/api.ts +22 -0
  22. package/template/apps/client/src/main.tsx +20 -0
  23. package/template/apps/community/src/App.tsx +592 -0
  24. package/template/apps/community/src/main.tsx +20 -0
  25. package/template/apps/community/src/styles.css +24 -0
  26. package/template/apps/developers/src/App.tsx +643 -0
  27. package/template/apps/developers/src/main.tsx +17 -0
  28. package/template/apps/developers/src/pages.tsx +1587 -0
  29. package/template/apps/developers/src/styles.css +98 -0
  30. package/template/apps/developers/src/ui.tsx +179 -0
  31. package/template/apps/landing/src/App.tsx +598 -0
  32. package/template/apps/landing/src/LegalPages.tsx +158 -0
  33. package/template/apps/landing/src/OrderPage.tsx +381 -0
  34. package/template/apps/landing/src/SeoPages.tsx +923 -0
  35. package/template/apps/landing/src/copy.ts +195 -0
  36. package/template/apps/landing/src/main.tsx +20 -0
  37. package/template/apps/landing/src/styles.css +803 -0
  38. package/template/apps/landing/src/types.ts +19 -0
  39. package/template/apps/marketplace/src/App.tsx +1229 -0
  40. package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
  41. package/template/apps/marketplace/src/main.tsx +20 -0
  42. package/template/apps/marketplace/src/styles.css +1001 -0
  43. package/template/apps/shared/AuthContext.tsx +65 -0
  44. package/template/apps/shared/Logo.tsx +33 -0
  45. package/template/apps/shared/api.ts +47 -0
  46. package/template/apps/shared/ui.tsx +128 -0
  47. package/template/apps/superadmin/src/App.tsx +1264 -0
  48. package/template/apps/superadmin/src/lib/api.ts +20 -0
  49. package/template/apps/superadmin/src/lib/hooks.ts +20 -0
  50. package/template/apps/superadmin/src/main.tsx +16 -0
  51. package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
  52. package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
  53. package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
  54. package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
  55. package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
  56. package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
  57. package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
  58. package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
  59. package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
  60. package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
  61. package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
  62. package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
  63. package/template/client.html +21 -0
  64. package/template/community/hamdiayari/akhbar/README.md +23 -0
  65. package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
  66. package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
  67. package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
  68. package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
  69. package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
  70. package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
  71. package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
  72. package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
  73. package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
  74. package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
  75. package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
  76. package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
  77. package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
  78. package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
  79. package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
  80. package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
  81. package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
  82. package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
  83. package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
  84. package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
  85. package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
  86. package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
  87. package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
  88. package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
  89. package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
  90. package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
  91. package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
  92. package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
  93. package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
  94. package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
  95. package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
  96. package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
  97. package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
  98. package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
  99. package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
  100. package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
  101. package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
  102. package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
  103. package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
  104. package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
  105. package/template/community.html +21 -0
  106. package/template/developers.html +24 -0
  107. package/template/docker-compose.db.yml +24 -0
  108. package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
  109. package/template/docs/CI_CD.md +66 -0
  110. package/template/docs/MODULES.md +346 -0
  111. package/template/docs/PLATFORM_DEPLOY.md +54 -0
  112. package/template/docs/logo.svg +10 -0
  113. package/template/drizzle/0000_init.sql +147 -0
  114. package/template/drizzle/0001_page_project.sql +5 -0
  115. package/template/drizzle/0002_modules.sql +23 -0
  116. package/template/drizzle/0003_themes.sql +4 -0
  117. package/template/drizzle/0004_security.sql +82 -0
  118. package/template/drizzle/0005_module_records.sql +16 -0
  119. package/template/drizzle/0006_avatars.sql +4 -0
  120. package/template/drizzle/0007_platform.sql +264 -0
  121. package/template/drizzle/0008_mp_validation.sql +3 -0
  122. package/template/drizzle/0009_saas_ops.sql +77 -0
  123. package/template/drizzle/0010_cms_chrome.sql +13 -0
  124. package/template/drizzle/0011_client_portal.sql +26 -0
  125. package/template/drizzle/meta/0000_snapshot.json +998 -0
  126. package/template/drizzle/meta/0001_snapshot.json +1004 -0
  127. package/template/drizzle/meta/_journal.json +90 -0
  128. package/template/drizzle.config.ts +16 -0
  129. package/template/ecosystem.snapify.cjs +24 -0
  130. package/template/for.html +1 -0
  131. package/template/index.html +29 -0
  132. package/template/landing.html +37 -0
  133. package/template/marketplace/catalog.json +373 -0
  134. package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
  135. package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
  136. package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
  137. package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
  138. package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
  139. package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
  140. package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
  141. package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
  142. package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
  143. package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
  144. package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
  145. package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
  146. package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
  147. package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
  148. package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
  149. package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
  150. package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
  151. package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
  152. package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
  153. package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
  154. package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
  155. package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
  156. package/template/marketplace.html +21 -0
  157. package/template/metadata.json +7 -0
  158. package/template/modules/ai-articles/screenshot.jpg +0 -0
  159. package/template/modules/ai-articles/snapify.module.json +74 -0
  160. package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
  161. package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
  162. package/template/modules/ai-articles/src/admin/index.tsx +12 -0
  163. package/template/modules/ai-articles/src/public/index.tsx +119 -0
  164. package/template/modules/ai-articles/src/server.ts +287 -0
  165. package/template/modules/ai-articles/src/types.ts +30 -0
  166. package/template/modules/ai-chat/screenshot.jpg +0 -0
  167. package/template/modules/ai-chat/snapify.module.json +217 -0
  168. package/template/modules/ai-chat/src/public.tsx +198 -0
  169. package/template/modules/ai-chat/src/server.ts +142 -0
  170. package/template/modules/ai-theme-studio/README.md +15 -0
  171. package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
  172. package/template/modules/ai-theme-studio/snapify.module.json +175 -0
  173. package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
  174. package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
  175. package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
  176. package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
  177. package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
  178. package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
  179. package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
  180. package/template/modules/ai-theme-studio/src/server.ts +137 -0
  181. package/template/modules/ai-theme-studio/src/types.ts +112 -0
  182. package/template/modules/calendar/screenshot.jpg +0 -0
  183. package/template/modules/calendar/snapify.module.json +45 -0
  184. package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
  185. package/template/modules/calendar/src/admin/index.tsx +11 -0
  186. package/template/modules/calendar/src/public/index.tsx +231 -0
  187. package/template/modules/calendar/src/server.ts +143 -0
  188. package/template/modules/calendar/src/types.ts +25 -0
  189. package/template/modules/cart/screenshot.jpg +0 -0
  190. package/template/modules/cart/snapify.module.json +90 -0
  191. package/template/modules/cart/src/admin/index.tsx +265 -0
  192. package/template/modules/cart/src/public/CartPage.tsx +198 -0
  193. package/template/modules/cart/src/public/index.tsx +261 -0
  194. package/template/modules/cart/src/public/useQuote.ts +67 -0
  195. package/template/modules/cart/src/server.ts +148 -0
  196. package/template/modules/cart/src/types.ts +31 -0
  197. package/template/modules/checkout/screenshot.jpg +0 -0
  198. package/template/modules/checkout/snapify.module.json +264 -0
  199. package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
  200. package/template/modules/checkout/src/admin/badges.tsx +28 -0
  201. package/template/modules/checkout/src/admin/index.tsx +236 -0
  202. package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
  203. package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
  204. package/template/modules/checkout/src/public/index.tsx +23 -0
  205. package/template/modules/checkout/src/server.ts +383 -0
  206. package/template/modules/checkout/src/types.ts +70 -0
  207. package/template/modules/customers/screenshot.jpg +0 -0
  208. package/template/modules/customers/snapify.module.json +115 -0
  209. package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
  210. package/template/modules/customers/src/admin/index.tsx +194 -0
  211. package/template/modules/customers/src/public/AccountPages.tsx +470 -0
  212. package/template/modules/customers/src/public/AddressForm.tsx +115 -0
  213. package/template/modules/customers/src/public/AuthPages.tsx +349 -0
  214. package/template/modules/customers/src/public/Avatar.tsx +29 -0
  215. package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
  216. package/template/modules/customers/src/public/index.tsx +137 -0
  217. package/template/modules/customers/src/public/session.ts +51 -0
  218. package/template/modules/customers/src/server.ts +421 -0
  219. package/template/modules/customers/src/types.ts +53 -0
  220. package/template/modules/recaptcha/logo.jpg +0 -0
  221. package/template/modules/recaptcha/screenshot.jpg +0 -0
  222. package/template/modules/recaptcha/snapify.module.json +99 -0
  223. package/template/modules/recaptcha/src/public/index.tsx +94 -0
  224. package/template/modules/recaptcha/src/server.ts +65 -0
  225. package/template/modules/recaptcha/src/types.ts +18 -0
  226. package/template/modules/shop/screenshot.jpg +0 -0
  227. package/template/modules/shop/snapify.module.json +340 -0
  228. package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
  229. package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
  230. package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
  231. package/template/modules/shop/src/admin/index.tsx +14 -0
  232. package/template/modules/shop/src/public/Catalog.tsx +244 -0
  233. package/template/modules/shop/src/public/NotFound.tsx +22 -0
  234. package/template/modules/shop/src/public/ProductPage.tsx +195 -0
  235. package/template/modules/shop/src/public/index.tsx +134 -0
  236. package/template/modules/shop/src/public/useFetch.ts +26 -0
  237. package/template/modules/shop/src/server.ts +493 -0
  238. package/template/modules/shop/src/shared.ts +41 -0
  239. package/template/modules/shop/src/types.ts +60 -0
  240. package/template/modules/whatsapp/screenshot.jpg +0 -0
  241. package/template/modules/whatsapp/snapify.module.json +121 -0
  242. package/template/modules/whatsapp/src/public.tsx +119 -0
  243. package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
  244. package/template/modules/wp-seo-migrator/README.md +21 -0
  245. package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
  246. package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
  247. package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
  248. package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
  249. package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
  250. package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
  251. package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
  252. package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
  253. package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
  254. package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
  255. package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
  256. package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
  257. package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
  258. package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
  259. package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
  260. package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
  261. package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
  262. package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
  263. package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
  264. package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
  265. package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
  266. package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
  267. package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
  268. package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
  269. package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
  270. package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
  271. package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
  272. package/template/modules/wp-seo-migrator/src/server.ts +492 -0
  273. package/template/modules/wp-seo-migrator/src/types.ts +151 -0
  274. package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
  275. package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
  276. package/template/package-lock.json +6016 -0
  277. package/template/package.json +74 -0
  278. package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
  279. package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
  280. package/template/public/images/photos/maison-campagne-collines.webp +0 -0
  281. package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
  282. package/template/public/images/photos/panorama-ville.jpg +0 -0
  283. package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
  284. package/template/public/images/photos/preveris-collectivites.jpg +0 -0
  285. package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
  286. package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
  287. package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
  288. package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
  289. package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
  290. package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
  291. package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
  292. package/template/public/landing/hero-ai-cms.jpg +0 -0
  293. package/template/public/landing/shot-dogga.jpg +0 -0
  294. package/template/public/landing/shot-mahdia.jpg +0 -0
  295. package/template/public/landing/shot-sidibou.jpg +0 -0
  296. package/template/public/llms.txt +35 -0
  297. package/template/public/logo.svg +10 -0
  298. package/template/scripts/app-pack.ts +155 -0
  299. package/template/scripts/app-publish.ts +100 -0
  300. package/template/scripts/apply-ecommerce-home.ts +196 -0
  301. package/template/scripts/auto-deploy-poll.sh +13 -0
  302. package/template/scripts/db-backup.sh +16 -0
  303. package/template/scripts/db-restore.sh +11 -0
  304. package/template/scripts/deploy.sh +34 -0
  305. package/template/scripts/github-webhook-deploy.sh +7 -0
  306. package/template/scripts/install-new-modules.ts +96 -0
  307. package/template/scripts/refresh-demo-baseline.ts +9 -0
  308. package/template/scripts/reinstall-marketplace-packs.ts +51 -0
  309. package/template/scripts/restore-classic-home.ts +13 -0
  310. package/template/scripts/sdk-pack.ts +289 -0
  311. package/template/scripts/sdk-publish.ts +75 -0
  312. package/template/scripts/seed-demo-products.ts +120 -0
  313. package/template/scripts/seed-theme-content-packs.ts +42 -0
  314. package/template/scripts/snapify-module.ts +207 -0
  315. package/template/scripts/test-theme-switch.ts +24 -0
  316. package/template/sdk/admin.css +18 -0
  317. package/template/sdk/admin.ts +95 -0
  318. package/template/sdk/build.ts +249 -0
  319. package/template/sdk/commerce.ts +512 -0
  320. package/template/sdk/content.ts +76 -0
  321. package/template/sdk/icons.ts +101 -0
  322. package/template/sdk/image.ts +60 -0
  323. package/template/sdk/index.ts +500 -0
  324. package/template/sdk/package.ts +298 -0
  325. package/template/sdk/recaptcha.tsx +122 -0
  326. package/template/sdk/settings.ts +84 -0
  327. package/template/sdk/theme.css +29 -0
  328. package/template/sdk/theme.ts +100 -0
  329. package/template/sdk/ui/commerce.tsx +385 -0
  330. package/template/sdk/ui/orders.tsx +136 -0
  331. package/template/sdk-kit/CHANGELOG.md +17 -0
  332. package/template/sdk-kit/LICENSE.md +47 -0
  333. package/template/sdk-kit/README.md +81 -0
  334. package/template/sdk-kit/cli.ts +339 -0
  335. package/template/sdk-kit/create-snapify/README.md +28 -0
  336. package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
  337. package/template/sdk-kit/create-snapify/package.template.json +16 -0
  338. package/template/sdk-kit/package.template.json +95 -0
  339. package/template/sdk-kit/templates/common/README.md.tpl +27 -0
  340. package/template/sdk-kit/templates/common/_gitignore +3 -0
  341. package/template/sdk-kit/templates/common/package.json.tpl +19 -0
  342. package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
  343. package/template/sdk-kit/templates/common/tsconfig.json +15 -0
  344. package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
  345. package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
  346. package/template/sdk-kit/templates/module/src/public.tsx +49 -0
  347. package/template/sdk-kit/templates/module/src/server.ts +30 -0
  348. package/template/sdk-kit/templates/module/src/types.ts +19 -0
  349. package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
  350. package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
  351. package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
  352. package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
  353. package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
  354. package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
  355. package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
  356. package/template/sdk-kit/templates/theme/src/styles.css +71 -0
  357. package/template/server/client/routes.ts +252 -0
  358. package/template/server/cms/analyticsStore.ts +180 -0
  359. package/template/server/cms/auth.ts +155 -0
  360. package/template/server/cms/blockCatalog.ts +342 -0
  361. package/template/server/cms/blocks.ts +521 -0
  362. package/template/server/cms/contentMigration.ts +1119 -0
  363. package/template/server/cms/defaults.ts +303 -0
  364. package/template/server/cms/permissions.ts +154 -0
  365. package/template/server/cms/restoreClassicHome.ts +183 -0
  366. package/template/server/cms/revisions.ts +99 -0
  367. package/template/server/cms/scheduler.ts +78 -0
  368. package/template/server/cms/seed.ts +72 -0
  369. package/template/server/cms/seoAnalyzer.ts +213 -0
  370. package/template/server/cms/store.ts +122 -0
  371. package/template/server/cms/themeContent.ts +1386 -0
  372. package/template/server/cms/themePacks/sidibou-branding.json +1 -0
  373. package/template/server/cms/themePacks/sidibou-home.json +1 -0
  374. package/template/server/cms/themePacks/sidibou-menus.json +1 -0
  375. package/template/server/cms/types.ts +432 -0
  376. package/template/server/community/routes.ts +410 -0
  377. package/template/server/db/client.ts +43 -0
  378. package/template/server/db/repository.ts +389 -0
  379. package/template/server/db/schema.ts +726 -0
  380. package/template/server/demo/cmsReset.ts +196 -0
  381. package/template/server/developers/sdk.ts +85 -0
  382. package/template/server/geoip.ts +15 -0
  383. package/template/server/http.ts +53 -0
  384. package/template/server/mail.ts +216 -0
  385. package/template/server/marketplace/catalog.ts +74 -0
  386. package/template/server/marketplace/packageValidate.ts +475 -0
  387. package/template/server/marketplace/payments.ts +193 -0
  388. package/template/server/marketplace/routes.ts +850 -0
  389. package/template/server/marketplace/signing.ts +116 -0
  390. package/template/server/marketplace/syncOfficial.ts +282 -0
  391. package/template/server/media/images.ts +63 -0
  392. package/template/server/middleware/adminAuth.ts +134 -0
  393. package/template/server/modules/legacy.ts +44 -0
  394. package/template/server/modules/manager.ts +1122 -0
  395. package/template/server/modules/marketplace.ts +178 -0
  396. package/template/server/modules/outbound.ts +178 -0
  397. package/template/server/modules/purchases.ts +149 -0
  398. package/template/server/modules/records.ts +168 -0
  399. package/template/server/modules/themeHtml.ts +32 -0
  400. package/template/server/modules/trust.ts +16 -0
  401. package/template/server/modules/types.ts +147 -0
  402. package/template/server/platform/auth.ts +223 -0
  403. package/template/server/platform/clientWelcome.ts +168 -0
  404. package/template/server/platform/clients.ts +139 -0
  405. package/template/server/platform/dailyImprove.ts +356 -0
  406. package/template/server/platform/flouci.ts +126 -0
  407. package/template/server/platform/host.ts +69 -0
  408. package/template/server/platform/hostingProviders.ts +143 -0
  409. package/template/server/platform/licenseOrders.ts +291 -0
  410. package/template/server/platform/money.ts +42 -0
  411. package/template/server/platform/passwords.ts +41 -0
  412. package/template/server/platform/region.ts +86 -0
  413. package/template/server/platform/routes.ts +225 -0
  414. package/template/server/platform/seed.ts +82 -0
  415. package/template/server/platform/settings.ts +72 -0
  416. package/template/server/routes/adminAccount.ts +403 -0
  417. package/template/server/routes/adminCms.ts +808 -0
  418. package/template/server/routes/adminSystem.ts +243 -0
  419. package/template/server/routes/adminUsers.ts +449 -0
  420. package/template/server/routes/googleOAuth.ts +327 -0
  421. package/template/server/routes/modules.ts +323 -0
  422. package/template/server/routes/publicCms.ts +313 -0
  423. package/template/server/routes/seoPublic.ts +158 -0
  424. package/template/server/security/accounts.ts +121 -0
  425. package/template/server/security/audit.ts +143 -0
  426. package/template/server/security/authTokens.ts +74 -0
  427. package/template/server/security/ip.ts +68 -0
  428. package/template/server/security/rateLimit.ts +85 -0
  429. package/template/server/security/retention.ts +46 -0
  430. package/template/server/security/secrets.ts +44 -0
  431. package/template/server/security/sessions.ts +140 -0
  432. package/template/server/security/totp.ts +101 -0
  433. package/template/server/superadmin/analytics.ts +109 -0
  434. package/template/server/superadmin/auth.ts +92 -0
  435. package/template/server/superadmin/backlinksCatalog.ts +210 -0
  436. package/template/server/superadmin/frSeoContent.ts +165 -0
  437. package/template/server/superadmin/landingSeo.ts +675 -0
  438. package/template/server/superadmin/ops.ts +238 -0
  439. package/template/server/superadmin/routes.ts +1095 -0
  440. package/template/server/superadmin/tnSeoContent.ts +130 -0
  441. package/template/server.ts +522 -0
  442. package/template/src/App.tsx +263 -0
  443. package/template/src/admin/AdminApp.tsx +162 -0
  444. package/template/src/admin/account/AuthLayout.tsx +57 -0
  445. package/template/src/admin/account/SecurityForms.tsx +276 -0
  446. package/template/src/admin/admin.css +127 -0
  447. package/template/src/admin/context/AuthContext.tsx +147 -0
  448. package/template/src/admin/context/SiteContext.tsx +50 -0
  449. package/template/src/admin/editor/BlockLibrary.tsx +173 -0
  450. package/template/src/admin/editor/BlocksPanel.tsx +396 -0
  451. package/template/src/admin/editor/EditorBits.tsx +106 -0
  452. package/template/src/admin/editor/PreviewFrame.tsx +145 -0
  453. package/template/src/admin/editor/RichTextEditor.tsx +276 -0
  454. package/template/src/admin/editor/SeoPanel.tsx +349 -0
  455. package/template/src/admin/editor/StyleEditor.tsx +114 -0
  456. package/template/src/admin/editor/blockSchemas.ts +699 -0
  457. package/template/src/admin/editor/fields.tsx +486 -0
  458. package/template/src/admin/layout/AdminShell.tsx +418 -0
  459. package/template/src/admin/layout/nav.ts +95 -0
  460. package/template/src/admin/lib/api.ts +408 -0
  461. package/template/src/admin/lib/audit.ts +141 -0
  462. package/template/src/admin/lib/format.ts +84 -0
  463. package/template/src/admin/lib/hooks.ts +125 -0
  464. package/template/src/admin/main.tsx +16 -0
  465. package/template/src/admin/modules/InstallDialog.tsx +160 -0
  466. package/template/src/admin/modules/SettingsForm.tsx +145 -0
  467. package/template/src/admin/modules/kit.tsx +127 -0
  468. package/template/src/admin/modules/labels.tsx +226 -0
  469. package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
  470. package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
  471. package/template/src/admin/pages/BrandingPage.tsx +154 -0
  472. package/template/src/admin/pages/DashboardPage.tsx +239 -0
  473. package/template/src/admin/pages/EngagementPage.tsx +168 -0
  474. package/template/src/admin/pages/FormsPage.tsx +435 -0
  475. package/template/src/admin/pages/GeneralPage.tsx +120 -0
  476. package/template/src/admin/pages/LoginPage.tsx +249 -0
  477. package/template/src/admin/pages/MaintenancePage.tsx +125 -0
  478. package/template/src/admin/pages/MarketplacePage.tsx +426 -0
  479. package/template/src/admin/pages/MediaPage.tsx +251 -0
  480. package/template/src/admin/pages/MenusPage.tsx +446 -0
  481. package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
  482. package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
  483. package/template/src/admin/pages/ModulesPage.tsx +135 -0
  484. package/template/src/admin/pages/NewsletterPage.tsx +193 -0
  485. package/template/src/admin/pages/PageEditorPage.tsx +322 -0
  486. package/template/src/admin/pages/PagesListPage.tsx +277 -0
  487. package/template/src/admin/pages/PostEditorPage.tsx +292 -0
  488. package/template/src/admin/pages/PostsListPage.tsx +187 -0
  489. package/template/src/admin/pages/ProfilePage.tsx +489 -0
  490. package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
  491. package/template/src/admin/pages/SeoPage.tsx +670 -0
  492. package/template/src/admin/pages/SettingsPage.tsx +189 -0
  493. package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
  494. package/template/src/admin/pages/ThemesPage.tsx +292 -0
  495. package/template/src/admin/pages/UsersPage.tsx +57 -0
  496. package/template/src/admin/pages/WidgetsPage.tsx +200 -0
  497. package/template/src/admin/seo/GoogleSetup.tsx +529 -0
  498. package/template/src/admin/settings/EmailTab.tsx +293 -0
  499. package/template/src/admin/settings/OtherTabs.tsx +270 -0
  500. package/template/src/admin/settings/SecurityTab.tsx +172 -0
  501. package/template/src/admin/settings/controls.tsx +133 -0
  502. package/template/src/admin/ui/Pagination.tsx +170 -0
  503. package/template/src/admin/ui/SaveBar.tsx +77 -0
  504. package/template/src/admin/ui/TagInput.tsx +54 -0
  505. package/template/src/admin/ui/charts.tsx +108 -0
  506. package/template/src/admin/ui/media.tsx +265 -0
  507. package/template/src/admin/ui/overlays.tsx +239 -0
  508. package/template/src/admin/ui/primitives.tsx +544 -0
  509. package/template/src/admin/users/AuditTab.tsx +104 -0
  510. package/template/src/admin/users/RolesTab.tsx +368 -0
  511. package/template/src/admin/users/UsersTab.tsx +723 -0
  512. package/template/src/admin/users/shared.tsx +278 -0
  513. package/template/src/cms/icons.ts +7 -0
  514. package/template/src/components/CookieBanner.tsx +166 -0
  515. package/template/src/components/MaintenanceGate.tsx +66 -0
  516. package/template/src/components/SiteAnalytics.tsx +64 -0
  517. package/template/src/components/SnapifyLogo.tsx +44 -0
  518. package/template/src/components/cms/BlockRenderer.tsx +154 -0
  519. package/template/src/components/cms/WidgetArea.tsx +193 -0
  520. package/template/src/context/SiteConfigContext.tsx +130 -0
  521. package/template/src/demoBase.ts +23 -0
  522. package/template/src/index.css +90 -0
  523. package/template/src/main.tsx +16 -0
  524. package/template/src/modules/ModuleHost.tsx +197 -0
  525. package/template/src/preview.ts +63 -0
  526. package/template/src/runtime.ts +38 -0
  527. package/template/src/themes/ThemeHost.tsx +168 -0
  528. package/template/src/themes/fallback.tsx +71 -0
  529. package/template/src/types.ts +94 -0
  530. package/template/src/views/BlogPostView.tsx +68 -0
  531. package/template/src/views/CmsPageView.tsx +34 -0
  532. package/template/start-pm2.sh +3 -0
  533. package/template/superadmin.html +20 -0
  534. package/template/themes/ai-canvas/screenshot.jpg +0 -0
  535. package/template/themes/ai-canvas/snapify.theme.json +96 -0
  536. package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
  537. package/template/themes/ai-canvas/src/Footer.tsx +190 -0
  538. package/template/themes/ai-canvas/src/Header.tsx +324 -0
  539. package/template/themes/ai-canvas/src/Layout.tsx +56 -0
  540. package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
  541. package/template/themes/ai-canvas/src/Post.tsx +26 -0
  542. package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
  543. package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
  544. package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
  545. package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
  546. package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
  547. package/template/themes/ai-canvas/src/index.tsx +54 -0
  548. package/template/themes/ai-canvas/src/kit.tsx +398 -0
  549. package/template/themes/ai-canvas/src/site.ts +29 -0
  550. package/template/themes/ai-canvas/src/styles.css +317 -0
  551. package/template/themes/dogga/screenshot.jpg +0 -0
  552. package/template/themes/dogga/snapify.theme.json +55 -0
  553. package/template/themes/dogga/src/Brand.tsx +41 -0
  554. package/template/themes/dogga/src/Footer.tsx +202 -0
  555. package/template/themes/dogga/src/Header.tsx +239 -0
  556. package/template/themes/dogga/src/Layout.tsx +43 -0
  557. package/template/themes/dogga/src/Views.tsx +71 -0
  558. package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
  559. package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
  560. package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
  561. package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
  562. package/template/themes/dogga/src/index.tsx +53 -0
  563. package/template/themes/dogga/src/kit.tsx +362 -0
  564. package/template/themes/dogga/src/site.ts +56 -0
  565. package/template/themes/dogga/src/styles.css +396 -0
  566. package/template/themes/mahdia/screenshot.jpg +0 -0
  567. package/template/themes/mahdia/snapify.theme.json +65 -0
  568. package/template/themes/mahdia/src/Brand.tsx +39 -0
  569. package/template/themes/mahdia/src/Footer.tsx +210 -0
  570. package/template/themes/mahdia/src/Header.tsx +291 -0
  571. package/template/themes/mahdia/src/Layout.tsx +44 -0
  572. package/template/themes/mahdia/src/Views.tsx +79 -0
  573. package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
  574. package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
  575. package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
  576. package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
  577. package/template/themes/mahdia/src/index.tsx +53 -0
  578. package/template/themes/mahdia/src/kit.tsx +362 -0
  579. package/template/themes/mahdia/src/site.ts +75 -0
  580. package/template/themes/mahdia/src/styles.css +446 -0
  581. package/template/themes/preveris/screenshot.jpg +0 -0
  582. package/template/themes/preveris/snapify.theme.json +126 -0
  583. package/template/themes/preveris/src/Brand.tsx +44 -0
  584. package/template/themes/preveris/src/Footer.tsx +202 -0
  585. package/template/themes/preveris/src/Header.tsx +239 -0
  586. package/template/themes/preveris/src/Layout.tsx +42 -0
  587. package/template/themes/preveris/src/Views.tsx +71 -0
  588. package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
  589. package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
  590. package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
  591. package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
  592. package/template/themes/preveris/src/index.tsx +53 -0
  593. package/template/themes/preveris/src/kit.tsx +365 -0
  594. package/template/themes/preveris/src/site.ts +79 -0
  595. package/template/themes/preveris/src/styles.css +446 -0
  596. package/template/themes/sidibou/screenshot.jpg +0 -0
  597. package/template/themes/sidibou/snapify.theme.json +21 -0
  598. package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
  599. package/template/themes/sidibou/src/Footer.tsx +203 -0
  600. package/template/themes/sidibou/src/Header.tsx +324 -0
  601. package/template/themes/sidibou/src/Layout.tsx +53 -0
  602. package/template/themes/sidibou/src/NotFound.tsx +58 -0
  603. package/template/themes/sidibou/src/Post.tsx +26 -0
  604. package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
  605. package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
  606. package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
  607. package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
  608. package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
  609. package/template/themes/sidibou/src/index.tsx +54 -0
  610. package/template/themes/sidibou/src/kit.tsx +398 -0
  611. package/template/themes/sidibou/src/site.ts +28 -0
  612. package/template/themes/sidibou/src/styles.css +317 -0
  613. package/template/tsconfig.json +33 -0
  614. package/template/vite.config.ts +42 -0
@@ -0,0 +1,672 @@
1
+ /**
2
+ * Akhbar — thème presse pour Snapify
3
+ * © 2026 Hamdi Ayari · blocs dérivés du thème Mahdia (NOVALABS WEB DESIGN).
4
+ */
5
+ import React, { useEffect, useState } from 'react';
6
+ import { ArrowLeft, ArrowRight, ArrowUpRight, Check, Phone, Plus, Quote, X } from 'lucide-react';
7
+ import type { BlockProps } from '@snapify/sdk/theme';
8
+ import type { CardItem, CardsData } from '@snapify/sdk/content';
9
+ import { useOptions } from '../site';
10
+ import { ButtonRow, CountUp, cx, Frame, hasIcon, Icon, Reveal, RichHtml, Section, SectionHeading, useCms, useGo } from '../kit';
11
+
12
+ const COLS = { 2: 'md:grid-cols-2', 3: 'md:grid-cols-2 lg:grid-cols-3', 4: 'sm:grid-cols-2 lg:grid-cols-4' } as const;
13
+
14
+ /* ------------------------------------------------------------------ Cartes */
15
+
16
+ function CardText({ it, d }: { it: CardItem; d: CardsData }) {
17
+ const { t } = useCms();
18
+ if (!it.text) return null;
19
+ return (
20
+ <p className="text-[0.95rem] leading-relaxed text-(--ak-fg-2)">
21
+ {d.textPrefix ? <strong className="font-semibold text-(--ak-fg)">{t(d.textPrefix)} </strong> : null}
22
+ {t(it.text)}
23
+ </p>
24
+ );
25
+ }
26
+
27
+ function CardFooter({ it, d }: { it: CardItem; d: CardsData }) {
28
+ const { t } = useCms();
29
+ const go = useGo();
30
+ return (
31
+ <>
32
+ {it.tags.length ? (
33
+ <div className="space-y-2">
34
+ {d.tagsLabel ? <div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-(--ak-fg-2)">{t(d.tagsLabel)}</div> : null}
35
+ <div className="flex flex-wrap gap-1.5">
36
+ {it.tags.map((tag) => (
37
+ <span key={tag} className="rounded-full border border-(--ak-rule) px-2.5 py-1 text-[11px] font-medium text-(--ak-fg-2) transition-colors group-hover:border-current">
38
+ {t(tag)}
39
+ </span>
40
+ ))}
41
+ </div>
42
+ </div>
43
+ ) : null}
44
+ {it.link ? (
45
+ <button type="button" onClick={() => go(it.link)} className="ak-link mt-auto self-start">
46
+ {t(it.linkLabel) || 'En savoir plus'}
47
+ <ArrowRight className="ak-btn-arrow h-4 w-4" />
48
+ </button>
49
+ ) : null}
50
+ </>
51
+ );
52
+ }
53
+
54
+ export function CardsBlock({ d, style }: BlockProps<'cards'>) {
55
+ const { t } = useCms();
56
+ const go = useGo();
57
+ const grid = cx('grid grid-cols-1', COLS[d.columns] ?? COLS[3]);
58
+
59
+ let body: React.ReactNode;
60
+ if (d.style === 'numbered') {
61
+ body = (
62
+ <div className={cx(grid, 'border-t border-(--ak-rule)')}>
63
+ {d.items.map((it, i) => (
64
+ <Reveal key={it.id} delay={i * 80} className="flex h-full flex-col gap-5 border-b border-(--ak-rule) py-10 md:border-b-0 md:border-r md:px-8 md:first:pl-0 md:last:border-r-0">
65
+ <span className="ak-num text-6xl leading-none text-(--ak-accent-text)">{String(i + 1).padStart(2, '0')}</span>
66
+ <h3 className="text-xl font-semibold leading-snug">{t(it.title)}</h3>
67
+ <CardText it={it} d={d} />
68
+ <CardFooter it={it} d={d} />
69
+ </Reveal>
70
+ ))}
71
+ </div>
72
+ );
73
+ } else if (d.style === 'minimal') {
74
+ body = (
75
+ <div className={cx(grid, 'gap-x-10 gap-y-12')}>
76
+ {d.items.map((it, i) => (
77
+ <Reveal key={it.id} delay={i * 70} className="flex h-full flex-col gap-4 border-t-2 border-(--ak-fg) pt-6">
78
+ <div className="flex items-center gap-3">
79
+ <Icon name={it.icon} className="h-5 w-5 text-(--ak-accent-text)" />
80
+ <h3 className="text-lg font-semibold leading-snug">{t(it.title)}</h3>
81
+ </div>
82
+ <CardText it={it} d={d} />
83
+ <CardFooter it={it} d={d} />
84
+ </Reveal>
85
+ ))}
86
+ </div>
87
+ );
88
+ } else if (d.style === 'image') {
89
+ body = (
90
+ <div className={cx(grid, 'gap-x-8 gap-y-14')}>
91
+ {d.items.map((it, i) => (
92
+ <Reveal key={it.id} delay={i * 80}>
93
+ <article className="group flex h-full flex-col gap-5">
94
+ <Frame shaped={i % 2 === 0} className="aspect-[4/5]">
95
+ {it.image ? (
96
+ <img src={it.image} alt={t(it.title)} loading="lazy" className="h-full w-full object-cover transition-transform duration-[1.2s] group-hover:scale-105" />
97
+ ) : (
98
+ <div className="flex h-full items-center justify-center">
99
+ <Icon name={it.icon} className="h-10 w-10 text-(--ak-accent-text)" />
100
+ </div>
101
+ )}
102
+ </Frame>
103
+ <h3 className="ak-display text-3xl">{t(it.title)}</h3>
104
+ <CardText it={it} d={d} />
105
+ <CardFooter it={it} d={d} />
106
+ </article>
107
+ </Reveal>
108
+ ))}
109
+ </div>
110
+ );
111
+ } else {
112
+ const dark = d.style === 'hoverDark';
113
+ body = (
114
+ <div className={cx(grid, 'gap-5')}>
115
+ {d.items.map((it, i) => (
116
+ <Reveal key={it.id} delay={i * 70} className="h-full">
117
+ <article
118
+ onClick={dark && it.link ? () => go(it.link) : undefined}
119
+ className={cx('ak-card group relative flex h-full flex-col gap-5 overflow-hidden p-8 sm:p-10', dark ? 'cursor-pointer transition-colors duration-500 hover:border-(--ak-ink)' : 'ak-card-hover')}
120
+ >
121
+ {dark ? <div className="absolute inset-0 origin-bottom scale-y-0 bg-(--ak-ink) transition-transform duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-y-100" /> : null}
122
+ <div className={cx('relative flex h-full flex-col gap-5', dark && 'transition-colors duration-500 group-hover:text-[#fbf8f3] group-hover:[--ak-fg:#fbf8f3] group-hover:[--ak-fg-2:rgb(251_248_243/0.7)] group-hover:[--ak-rule:rgb(255_255_255/0.18)] group-hover:[--ak-accent-text:color-mix(in_oklab,var(--ak-accent)_55%,white)]')}>
123
+ <div className="flex items-start justify-between">
124
+ {hasIcon(it.icon) ? (
125
+ <span className="flex h-14 w-14 items-center justify-center rounded-full border border-(--ak-rule) text-(--ak-accent-text) transition-colors">
126
+ <Icon name={it.icon} className="h-6 w-6" />
127
+ </span>
128
+ ) : (
129
+ <span className="mt-2 h-0.5 w-10 bg-(--ak-accent-text)" />
130
+ )}
131
+ {dark ? <ArrowUpRight className="h-5 w-5 text-(--ak-fg-2) transition-transform duration-500 group-hover:-translate-y-1 group-hover:translate-x-1" /> : null}
132
+ </div>
133
+ <h3 className={cx('leading-snug', dark ? 'ak-display text-[2rem]' : 'text-xl font-semibold')}>{t(it.title)}</h3>
134
+ <CardText it={it} d={d} />
135
+ <CardFooter it={it} d={d} />
136
+ </div>
137
+ </article>
138
+ </Reveal>
139
+ ))}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ return (
145
+ <Section style={style}>
146
+ <SectionHeading h={d} />
147
+ {body}
148
+ </Section>
149
+ );
150
+ }
151
+
152
+ /* ------------------------------------------------------------------ Vitrine à onglets */
153
+
154
+ export function ShowcaseBlock({ d, style }: BlockProps<'showcase'>) {
155
+ const { t } = useCms();
156
+ const go = useGo();
157
+ const { animations } = useOptions();
158
+ const [active, setActive] = useState(0);
159
+ const [paused, setPaused] = useState(false);
160
+ const delay = Math.max(d.interval || 7, 3) * 1000;
161
+ const items = d.items;
162
+
163
+ useEffect(() => {
164
+ if (paused || items.length < 2 || !animations) return;
165
+ const id = window.setTimeout(() => setActive((i) => (i + 1) % items.length), delay);
166
+ return () => window.clearTimeout(id);
167
+ }, [active, paused, items.length, delay, animations]);
168
+
169
+ if (!items.length) return null;
170
+ const cur = items[Math.min(active, items.length - 1)];
171
+
172
+ return (
173
+ <Section style={style}>
174
+ <SectionHeading h={d} />
175
+ <div className="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-16" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
176
+ <Reveal className="lg:col-span-5">
177
+ <ol className="border-t border-(--ak-rule)">
178
+ {items.map((it, i) => (
179
+ <li key={it.id} className="border-b border-(--ak-rule)">
180
+ <button type="button" onClick={() => setActive(i)} className="group w-full py-5 text-left" aria-expanded={i === active}>
181
+ <span className="flex items-baseline gap-5">
182
+ <span className={cx('ak-num text-lg transition-colors', i === active ? 'text-(--ak-accent-text)' : 'text-(--ak-fg-2)')}>{String(i + 1).padStart(2, '0')}</span>
183
+ <span className={cx('text-lg font-semibold transition-colors sm:text-xl', i === active ? 'text-(--ak-fg)' : 'text-(--ak-fg-2) group-hover:text-(--ak-fg)')}>{t(it.title)}</span>
184
+ </span>
185
+ <span className={cx('grid transition-all duration-500', i === active ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
186
+ <span className="overflow-hidden pl-11">
187
+ {it.text ? <span className="block pt-3 text-[0.95rem] leading-relaxed text-(--ak-fg-2)">{t(it.text)}</span> : null}
188
+ </span>
189
+ </span>
190
+ <span className="mt-4 block h-px overflow-hidden bg-transparent">
191
+ {i === active && animations && items.length > 1 ? <span key={`${active}-${paused}`} className={cx('block h-full bg-(--ak-accent)', !paused && 'ak-progress')} style={{ animationDuration: `${delay}ms` }} /> : null}
192
+ </span>
193
+ </button>
194
+ </li>
195
+ ))}
196
+ </ol>
197
+ </Reveal>
198
+ <Reveal delay={120} className="lg:col-span-7">
199
+ <div className="relative overflow-hidden rounded-(--ak-r-lg) bg-(--ak-ink)">
200
+ <div className="aspect-[4/3] w-full">
201
+ {cur.image ? <img key={cur.id} src={cur.image} alt={t(cur.title)} className="ak-word-in h-full w-full object-cover" /> : <div className="flex h-full items-center justify-center text-white/40"><Icon name={cur.icon} className="h-16 w-16" /></div>}
202
+ </div>
203
+ <div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/45 to-black/5" />
204
+ <div key={cur.id} className="ak-word-in ak-dark absolute inset-x-0 bottom-0 space-y-5 p-7 text-(--ak-fg) sm:p-10">
205
+ <div className="flex items-center gap-3">
206
+ {hasIcon(cur.icon) ? (
207
+ <span className="flex h-11 w-11 items-center justify-center rounded-full bg-(--ak-accent) text-(--ak-on-accent)">
208
+ <Icon name={cur.icon} className="h-5 w-5" />
209
+ </span>
210
+ ) : null}
211
+ <h3 className="ak-display text-3xl sm:text-4xl">{t(cur.title)}</h3>
212
+ </div>
213
+ {cur.chips.length ? (
214
+ <div className="flex flex-wrap gap-2">
215
+ {cur.chips.map((c) => (
216
+ <span key={c} className="rounded-full border border-white/25 bg-white/10 px-3 py-1 text-xs font-medium text-white backdrop-blur">
217
+ {t(c)}
218
+ </span>
219
+ ))}
220
+ </div>
221
+ ) : null}
222
+ {cur.link ? (
223
+ <button type="button" onClick={() => go(cur.link)} className="ak-btn ak-btn-light">
224
+ {t(cur.buttonLabel) || 'Découvrir'}
225
+ <ArrowRight className="ak-btn-arrow h-4 w-4" />
226
+ </button>
227
+ ) : null}
228
+ </div>
229
+ </div>
230
+ </Reveal>
231
+ </div>
232
+ </Section>
233
+ );
234
+ }
235
+
236
+ /* ------------------------------------------------------------------ Étapes */
237
+
238
+ export function StepsBlock({ d, style }: BlockProps<'steps'>) {
239
+ const { t } = useCms();
240
+ const deliverable = (text?: string) =>
241
+ text ? (
242
+ <div className="rounded-(--ak-r) border border-dashed border-(--ak-rule) px-4 py-3 text-xs">
243
+ <span className="font-semibold uppercase tracking-[0.14em] text-(--ak-accent-text)">{t(d.deliverableLabel) || 'Livrable'}</span>
244
+ <span className="mt-1 block text-(--ak-fg)">{t(text)}</span>
245
+ </div>
246
+ ) : null;
247
+
248
+ let body: React.ReactNode;
249
+ if (d.style === 'list') {
250
+ body = (
251
+ <ol className="border-t border-(--ak-rule)">
252
+ {d.items.map((s, i) => (
253
+ <Reveal key={s.id} delay={i * 60}>
254
+ <li className="grid grid-cols-1 gap-5 border-b border-(--ak-rule) py-10 md:grid-cols-12 md:gap-8">
255
+ <span className="ak-num text-6xl leading-none text-(--ak-accent-text) md:col-span-2">{t(s.num) || String(i + 1).padStart(2, '0')}</span>
256
+ <div className="space-y-3 md:col-span-6">
257
+ <h3 className="ak-display text-3xl">{t(s.title)}</h3>
258
+ {s.text ? <p className="leading-relaxed text-(--ak-fg-2)">{t(s.text)}</p> : null}
259
+ </div>
260
+ <div className="md:col-span-4 md:pt-2">{deliverable(s.deliverable)}</div>
261
+ </li>
262
+ </Reveal>
263
+ ))}
264
+ </ol>
265
+ );
266
+ } else if (d.style === 'compact') {
267
+ body = (
268
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
269
+ {d.items.map((s, i) => (
270
+ <Reveal key={s.id} delay={i * 60} className="ak-card flex h-full flex-col gap-3 p-6">
271
+ <span className="text-xs font-semibold uppercase tracking-[0.18em] text-(--ak-accent-text)">Étape {t(s.num) || i + 1}</span>
272
+ <h3 className="text-lg font-semibold leading-snug">{t(s.title)}</h3>
273
+ {s.text ? <p className="text-sm leading-relaxed text-(--ak-fg-2)">{t(s.text)}</p> : null}
274
+ </Reveal>
275
+ ))}
276
+ </div>
277
+ );
278
+ } else {
279
+ body = (
280
+ <div className="relative">
281
+ <div className="absolute left-0 right-0 top-7 hidden h-px bg-(--ak-rule) lg:block" />
282
+ <ol className={cx('relative grid grid-cols-1 gap-10 sm:grid-cols-2', d.items.length >= 4 ? 'lg:grid-cols-4' : 'lg:grid-cols-3')}>
283
+ {d.items.map((s, i) => (
284
+ <Reveal key={s.id} delay={i * 110}>
285
+ <li className="flex h-full flex-col gap-5">
286
+ <span className="ak-num flex h-14 w-14 items-center justify-center rounded-full border border-(--ak-rule) bg-(--ak-paper) text-xl text-(--ak-fg) shadow-[0_0_0_8px_var(--ak-paper)]">{t(s.num) || i + 1}</span>
287
+ <h3 className="text-xl font-semibold leading-snug">{t(s.title)}</h3>
288
+ {s.text ? <p className="text-[0.95rem] leading-relaxed text-(--ak-fg-2)">{t(s.text)}</p> : null}
289
+ <div className="mt-auto">{deliverable(s.deliverable)}</div>
290
+ </li>
291
+ </Reveal>
292
+ ))}
293
+ </ol>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ return (
299
+ <Section style={style}>
300
+ <SectionHeading h={d} />
301
+ {body}
302
+ <ButtonRow buttons={d.buttons} className="mt-14" center={d.align === 'center'} />
303
+ </Section>
304
+ );
305
+ }
306
+
307
+ /* ------------------------------------------------------------------ Chiffres */
308
+
309
+ export function StatsBlock({ d, style }: BlockProps<'stats'>) {
310
+ const { t } = useCms();
311
+ const value = (s: (typeof d.items)[number]) => <CountUp value={`${t(s.prefix)}${t(s.value)}${t(s.suffix)}`} animate={d.countUp} />;
312
+ const cols = d.items.length >= 4 ? 'lg:grid-cols-4' : d.items.length === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-2';
313
+ const wordy = d.items.some((s) => `${t(s.prefix)}${t(s.value)}${t(s.suffix)}`.length > 9);
314
+
315
+ let body: React.ReactNode;
316
+ if (d.style === 'strip') {
317
+ body = (
318
+ <div className={cx('ak-dark grid grid-cols-2 overflow-hidden rounded-(--ak-r-lg) bg-(--ak-ink) text-(--ak-fg)', cols)}>
319
+ {d.items.map((s, i) => (
320
+ <Reveal key={s.id} delay={i * 80} className="border-white/10 p-7 sm:p-9 [&:not(:last-child)]:border-r">
321
+ <div className={cx('ak-num', wordy ? 'text-2xl leading-tight sm:text-3xl' : 'text-4xl leading-none sm:text-5xl')}>{value(s)}</div>
322
+ <div className="mt-3 text-sm text-(--ak-fg-2)">{t(s.label)}</div>
323
+ </Reveal>
324
+ ))}
325
+ </div>
326
+ );
327
+ } else if (d.style === 'inline') {
328
+ body = (
329
+ <div className={cx('grid grid-cols-2 gap-y-10 border-y border-(--ak-rule) py-10', cols)}>
330
+ {d.items.map((s, i) => (
331
+ <Reveal key={s.id} delay={i * 80} className="px-4 text-center sm:px-6 lg:[&:not(:last-child)]:border-r lg:[&:not(:last-child)]:border-(--ak-rule)">
332
+ <div className={cx('ak-num', wordy ? 'text-3xl leading-tight sm:text-4xl' : 'text-5xl leading-none sm:text-6xl')}>{value(s)}</div>
333
+ <div className="mt-3 text-sm font-medium">{t(s.label)}</div>
334
+ {s.hint ? <div className="mt-1 text-xs text-(--ak-fg-2)">{t(s.hint)}</div> : null}
335
+ </Reveal>
336
+ ))}
337
+ </div>
338
+ );
339
+ } else {
340
+ body = (
341
+ <div className={cx('grid grid-cols-1 gap-4 sm:grid-cols-2', cols)}>
342
+ {d.items.map((s, i) => (
343
+ <Reveal key={s.id} delay={i * 80} className="ak-card flex h-full flex-col justify-between gap-10 p-8 backdrop-blur-sm">
344
+ <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-(--ak-fg-2)">{String(i + 1).padStart(2, '0')}</div>
345
+ <div>
346
+ <div className={cx('ak-num', wordy ? 'text-3xl leading-tight sm:text-4xl' : 'text-5xl leading-none sm:text-6xl')}>{value(s)}</div>
347
+ <div className="mt-4 text-[0.95rem] font-semibold">{t(s.label)}</div>
348
+ {s.hint ? <div className="mt-1 text-sm text-(--ak-fg-2)">{t(s.hint)}</div> : null}
349
+ </div>
350
+ </Reveal>
351
+ ))}
352
+ </div>
353
+ );
354
+ }
355
+
356
+ return (
357
+ <Section style={style}>
358
+ <SectionHeading h={d} />
359
+ {body}
360
+ </Section>
361
+ );
362
+ }
363
+
364
+ /* ------------------------------------------------------------------ Panneaux */
365
+
366
+ export function PanelsBlock({ d, style }: BlockProps<'panels'>) {
367
+ const { t } = useCms();
368
+ const go = useGo();
369
+ return (
370
+ <Section style={style}>
371
+ <SectionHeading h={d} />
372
+ <div className={cx('grid grid-cols-1 gap-5', d.items.length >= 3 ? 'lg:grid-cols-3' : 'md:grid-cols-2')}>
373
+ {d.items.map((p, i) => (
374
+ <Reveal key={p.id} delay={i * 90} className="h-full">
375
+ <article
376
+ onClick={p.link ? () => go(p.link) : undefined}
377
+ className={cx('group ak-dark relative flex h-full min-h-[32rem] flex-col justify-end overflow-hidden rounded-(--ak-r-lg) bg-(--ak-ink) text-(--ak-fg)', p.link && 'cursor-pointer')}
378
+ >
379
+ {p.image ? <img src={p.image} alt={t(p.title)} loading="lazy" className="absolute inset-0 h-full w-full object-cover opacity-80 transition-all duration-[1.2s] group-hover:scale-105 group-hover:opacity-60" /> : null}
380
+ <div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/35 to-black/5" />
381
+ <div className="relative space-y-4 p-8">
382
+ {hasIcon(p.icon) ? (
383
+ <span className="flex h-12 w-12 items-center justify-center rounded-full border border-white/30 bg-white/10 backdrop-blur">
384
+ <Icon name={p.icon} className="h-5 w-5" />
385
+ </span>
386
+ ) : null}
387
+ <h3 className="ak-display text-[2.1rem] leading-tight">{t(p.title)}</h3>
388
+ {p.text ? <p className="text-sm leading-relaxed text-(--ak-fg-2)">{t(p.text)}</p> : null}
389
+ {p.bullets.length ? (
390
+ <div className="grid grid-rows-[0fr] transition-all duration-500 group-hover:grid-rows-[1fr] max-lg:grid-rows-[1fr]">
391
+ <ul className="space-y-1.5 overflow-hidden">
392
+ {p.bullets.map((b) => (
393
+ <li key={b} className="flex items-start gap-2 text-sm text-white/85">
394
+ <Check className="mt-0.5 h-4 w-4 shrink-0 text-(--ak-accent-text)" />
395
+ {t(b)}
396
+ </li>
397
+ ))}
398
+ </ul>
399
+ </div>
400
+ ) : null}
401
+ {p.link ? (
402
+ <span className="ak-link pt-2">
403
+ {t(p.linkLabel) || 'Découvrir'}
404
+ <ArrowRight className="ak-btn-arrow h-4 w-4" />
405
+ </span>
406
+ ) : null}
407
+ </div>
408
+ </article>
409
+ </Reveal>
410
+ ))}
411
+ </div>
412
+ </Section>
413
+ );
414
+ }
415
+
416
+ /* ------------------------------------------------------------------ Galerie */
417
+
418
+ function Lightbox({ items, index, onClose, onMove }: { items: { image: string; caption?: string }[]; index: number; onClose: () => void; onMove: (step: number) => void }) {
419
+ const { t } = useCms();
420
+ useEffect(() => {
421
+ const onKey = (e: KeyboardEvent) => {
422
+ if (e.key === 'Escape') onClose();
423
+ if (e.key === 'ArrowRight') onMove(1);
424
+ if (e.key === 'ArrowLeft') onMove(-1);
425
+ };
426
+ window.addEventListener('keydown', onKey);
427
+ return () => window.removeEventListener('keydown', onKey);
428
+ }, [onClose, onMove]);
429
+ const it = items[index];
430
+ return (
431
+ <div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/90 p-4 backdrop-blur-sm" onClick={onClose} role="dialog" aria-modal="true">
432
+ <button type="button" className="absolute right-5 top-5 flex h-11 w-11 items-center justify-center rounded-full border border-white/25 text-white" aria-label="Fermer">
433
+ <X className="h-5 w-5" />
434
+ </button>
435
+ <button type="button" onClick={(e) => (e.stopPropagation(), onMove(-1))} className="absolute left-4 flex h-12 w-12 items-center justify-center rounded-full border border-white/25 text-white hover:bg-white/10" aria-label="Précédente">
436
+ <ArrowLeft className="h-5 w-5" />
437
+ </button>
438
+ <figure className="max-h-full max-w-5xl" onClick={(e) => e.stopPropagation()}>
439
+ <img src={it.image} alt={t(it.caption)} className="max-h-[80vh] w-auto rounded-(--ak-r)" />
440
+ {it.caption ? <figcaption className="ak-display mt-4 text-center text-2xl text-white">{t(it.caption)}</figcaption> : null}
441
+ </figure>
442
+ <button type="button" onClick={(e) => (e.stopPropagation(), onMove(1))} className="absolute right-4 flex h-12 w-12 items-center justify-center rounded-full border border-white/25 text-white hover:bg-white/10" aria-label="Suivante">
443
+ <ArrowRight className="h-5 w-5" />
444
+ </button>
445
+ </div>
446
+ );
447
+ }
448
+
449
+ export function GalleryBlock({ d, style }: BlockProps<'gallery'>) {
450
+ const { t } = useCms();
451
+ const items = d.items.filter((i) => i.image);
452
+ const [open, setOpen] = useState<number | null>(null);
453
+ const move = (step: number) => setOpen((i) => (i === null ? i : (i + step + items.length) % items.length));
454
+
455
+ let body: React.ReactNode;
456
+ if (d.style === 'polaroid') {
457
+ const tilt = ['-rotate-2', 'rotate-1', '-rotate-1', 'rotate-2', 'rotate-0', '-rotate-3'];
458
+ body = (
459
+ <div className="grid grid-cols-2 gap-x-5 gap-y-10 sm:grid-cols-3 lg:grid-cols-4 lg:gap-x-8">
460
+ {items.map((it, i) => (
461
+ <Reveal key={it.id} delay={(i % 4) * 70}>
462
+ <button type="button" onClick={() => setOpen(i)} className={cx('block w-full bg-white p-2.5 pb-4 text-left shadow-[0_25px_50px_-25px_rgba(26,22,18,0.45)] transition-transform duration-500 hover:z-10 hover:-translate-y-2 hover:rotate-0', tilt[i % tilt.length])}>
463
+ <img src={it.image} alt={t(it.caption)} loading="lazy" className="aspect-square w-full object-cover" />
464
+ {it.caption ? <span className="ak-display mt-3 block px-1 text-center text-lg leading-tight text-(--ak-ink)">{t(it.caption)}</span> : null}
465
+ </button>
466
+ </Reveal>
467
+ ))}
468
+ </div>
469
+ );
470
+ } else if (d.style === 'masonry') {
471
+ body = (
472
+ <div className="columns-1 gap-5 sm:columns-2 lg:columns-3 [&>*]:mb-5">
473
+ {items.map((it, i) => (
474
+ <Reveal key={it.id} delay={(i % 3) * 70} className="break-inside-avoid">
475
+ <button type="button" onClick={() => setOpen(i)} className="group relative block w-full overflow-hidden rounded-(--ak-r-lg)">
476
+ <img src={it.image} alt={t(it.caption)} loading="lazy" className="w-full transition-transform duration-700 group-hover:scale-105" />
477
+ {it.caption ? <span className="absolute inset-x-0 bottom-0 translate-y-full bg-gradient-to-t from-black/75 to-transparent p-5 text-left text-sm text-white transition-transform duration-500 group-hover:translate-y-0">{t(it.caption)}</span> : null}
478
+ </button>
479
+ </Reveal>
480
+ ))}
481
+ </div>
482
+ );
483
+ } else {
484
+ body = (
485
+ <div className="grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-4">
486
+ {items.map((it, i) => (
487
+ <Reveal key={it.id} delay={(i % 4) * 60} className={cx(i % 5 === 0 && 'col-span-2 row-span-2')}>
488
+ <button type="button" onClick={() => setOpen(i)} className="group relative block h-full w-full overflow-hidden rounded-(--ak-r-lg)">
489
+ <img src={it.image} alt={t(it.caption)} loading="lazy" className="aspect-square h-full w-full object-cover transition-transform duration-700 group-hover:scale-105" />
490
+ {it.caption ? <span className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent p-4 text-left text-xs font-medium text-white opacity-0 transition-opacity group-hover:opacity-100">{t(it.caption)}</span> : null}
491
+ </button>
492
+ </Reveal>
493
+ ))}
494
+ </div>
495
+ );
496
+ }
497
+
498
+ return (
499
+ <Section style={style}>
500
+ <SectionHeading h={d} />
501
+ {body}
502
+ {d.handNote || d.note ? (
503
+ <Reveal className="mt-14 flex flex-col items-center gap-2 text-center">
504
+ {d.handNote ? <p className="ak-display text-3xl text-(--ak-accent-text)">{t(d.handNote)}</p> : null}
505
+ {d.note ? <p className="max-w-2xl text-sm text-(--ak-fg-2)">{t(d.note)}</p> : null}
506
+ </Reveal>
507
+ ) : null}
508
+ {open !== null && items[open] ? <Lightbox items={items} index={open} onClose={() => setOpen(null)} onMove={move} /> : null}
509
+ </Section>
510
+ );
511
+ }
512
+
513
+ /* ------------------------------------------------------------------ Listes à cocher */
514
+
515
+ export function ChecklistBlock({ d, style }: BlockProps<'checklist'>) {
516
+ const { t } = useCms();
517
+ return (
518
+ <Section style={style}>
519
+ <SectionHeading h={d} />
520
+ <div className={cx('grid grid-cols-1 gap-5', d.cards.length >= 3 ? 'lg:grid-cols-3' : 'md:grid-cols-2')}>
521
+ {d.cards.map((c, i) => (
522
+ <Reveal key={c.id} delay={i * 80} className="h-full">
523
+ <div className="ak-card relative flex h-full flex-col gap-6 overflow-hidden p-8">
524
+ <span className="absolute inset-x-0 top-0 h-1 bg-(--ak-accent)" style={{ opacity: 1 - i * 0.25 }} />
525
+ <div className="flex items-center justify-between">
526
+ {hasIcon(c.icon) ? (
527
+ <span className="flex h-12 w-12 items-center justify-center rounded-full bg-(--ak-paper-2) text-(--ak-accent-text)">
528
+ <Icon name={c.icon} className="h-5 w-5" />
529
+ </span>
530
+ ) : <span />}
531
+ {c.tag ? <span className="rounded-full border border-(--ak-rule) px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-(--ak-fg-2)">{t(c.tag)}</span> : null}
532
+ </div>
533
+ {c.title ? <h3 className="ak-display text-[1.9rem] leading-tight">{t(c.title)}</h3> : null}
534
+ <ul className="space-y-4">
535
+ {c.entries.map((e) => (
536
+ <li key={e.id} className="flex gap-3">
537
+ <span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-(--ak-accent) text-(--ak-on-accent)">
538
+ <Check className="h-3 w-3" strokeWidth={3} />
539
+ </span>
540
+ <span>
541
+ <span className="block text-sm font-semibold">{t(e.title)}</span>
542
+ {e.text ? <span className="mt-0.5 block text-sm leading-relaxed text-(--ak-fg-2)">{t(e.text)}</span> : null}
543
+ </span>
544
+ </li>
545
+ ))}
546
+ </ul>
547
+ </div>
548
+ </Reveal>
549
+ ))}
550
+ </div>
551
+ </Section>
552
+ );
553
+ }
554
+
555
+ /* ------------------------------------------------------------------ Questions fréquentes */
556
+
557
+ export function FaqBlock({ d, style }: BlockProps<'faq'>) {
558
+ const { t, tokens } = useCms();
559
+ const [open, setOpen] = useState<string | null>(d.items[0]?.id ?? null);
560
+ const list = (
561
+ <div className="border-t border-(--ak-rule)">
562
+ {d.items.map((q, i) => {
563
+ const isOpen = open === q.id;
564
+ return (
565
+ <Reveal key={q.id} delay={Math.min(i, 6) * 40}>
566
+ <div className="border-b border-(--ak-rule)">
567
+ <button type="button" onClick={() => setOpen(isOpen ? null : q.id)} className="flex w-full items-start justify-between gap-6 py-6 text-left" aria-expanded={isOpen}>
568
+ <span className="text-lg font-semibold leading-snug">{t(q.question)}</span>
569
+ <span className={cx('flex h-9 w-9 shrink-0 items-center justify-center rounded-full border transition-all duration-300', isOpen ? 'rotate-45 border-(--ak-accent) bg-(--ak-accent) text-(--ak-on-accent)' : 'border-(--ak-rule)')}>
570
+ <Plus className="h-4 w-4" />
571
+ </span>
572
+ </button>
573
+ <div className={cx('grid transition-all duration-500', isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
574
+ <div className="overflow-hidden">
575
+ <RichHtml html={q.answer} className="pb-7 pr-12 text-[0.98rem]!" />
576
+ </div>
577
+ </div>
578
+ </div>
579
+ </Reveal>
580
+ );
581
+ })}
582
+ </div>
583
+ );
584
+
585
+ const contact = d.showContactCard ? (
586
+ <Reveal delay={100}>
587
+ <div className="ak-dark mt-10 rounded-(--ak-r-lg) bg-(--ak-ink) p-8 text-(--ak-fg)">
588
+ <p className="ak-display text-2xl">{t(d.contactLabel) || 'Une autre question ?'}</p>
589
+ <a href={`tel:${tokens.phoneClean}`} className="mt-6 inline-flex items-center gap-3 text-lg font-semibold transition-colors hover:text-(--ak-accent-text)">
590
+ <span className="flex h-11 w-11 items-center justify-center rounded-full bg-(--ak-accent) text-(--ak-on-accent)">
591
+ <Phone className="h-4 w-4" />
592
+ </span>
593
+ {tokens.phone}
594
+ </a>
595
+ </div>
596
+ </Reveal>
597
+ ) : null;
598
+
599
+ if (d.layout === 'stacked') {
600
+ return (
601
+ <Section style={style}>
602
+ <div className="mx-auto max-w-3xl">
603
+ <SectionHeading h={{ ...d, align: d.align ?? 'center' }} />
604
+ {list}
605
+ {contact}
606
+ </div>
607
+ </Section>
608
+ );
609
+ }
610
+ return (
611
+ <Section style={style}>
612
+ <div className="grid grid-cols-1 gap-12 lg:grid-cols-12 lg:gap-16">
613
+ <div className="lg:col-span-5">
614
+ <div className="lg:sticky lg:top-28">
615
+ <SectionHeading h={{ ...d, align: 'left', headerLink: undefined }} className="mb-0! sm:mb-0!" />
616
+ {contact}
617
+ </div>
618
+ </div>
619
+ <div className="lg:col-span-7">{list}</div>
620
+ </div>
621
+ </Section>
622
+ );
623
+ }
624
+
625
+ /* ------------------------------------------------------------------ Témoignages */
626
+
627
+ export function TestimonialsBlock({ d, style }: BlockProps<'testimonials'>) {
628
+ const { t } = useCms();
629
+ const [index, setIndex] = useState(0);
630
+ if (!d.items.length) return null;
631
+ const cur = d.items[Math.min(index, d.items.length - 1)];
632
+ const move = (step: number) => setIndex((i) => (i + step + d.items.length) % d.items.length);
633
+ return (
634
+ <Section style={style}>
635
+ <SectionHeading h={d} />
636
+ <Reveal>
637
+ <div className="grid grid-cols-1 gap-10 lg:grid-cols-12">
638
+ <div className="lg:col-span-1">
639
+ <Quote className="h-12 w-12 text-(--ak-accent-text)" strokeWidth={1.2} />
640
+ </div>
641
+ <div className="lg:col-span-11">
642
+ <blockquote key={cur.id} className="ak-word-in ak-display text-3xl leading-[1.2] sm:text-4xl lg:text-[2.9rem]">
643
+ « {t(cur.quote)} »
644
+ </blockquote>
645
+ <div className="mt-10 flex flex-col gap-6 border-t border-(--ak-rule) pt-8 sm:flex-row sm:items-center sm:justify-between">
646
+ <div className="flex items-center gap-4">
647
+ {cur.image ? <img src={cur.image} alt={t(cur.author)} className="h-14 w-14 rounded-full object-cover" /> : <span className="ak-display flex h-14 w-14 items-center justify-center rounded-full bg-(--ak-paper-2) text-2xl text-(--ak-ink)">{t(cur.author).charAt(0)}</span>}
648
+ <div>
649
+ <div className="font-semibold">{t(cur.author)}</div>
650
+ {cur.role ? <div className="text-sm text-(--ak-fg-2)">{t(cur.role)}</div> : null}
651
+ </div>
652
+ </div>
653
+ {d.items.length > 1 ? (
654
+ <div className="flex items-center gap-3">
655
+ <span className="mr-2 font-mono text-xs text-(--ak-fg-2)">
656
+ {String(index + 1).padStart(2, '0')} / {String(d.items.length).padStart(2, '0')}
657
+ </span>
658
+ <button type="button" onClick={() => move(-1)} className="flex h-12 w-12 items-center justify-center rounded-full border border-(--ak-rule) transition-colors hover:bg-(--ak-fg) hover:text-(--ak-paper)" aria-label="Témoignage précédent">
659
+ <ArrowLeft className="h-4 w-4" />
660
+ </button>
661
+ <button type="button" onClick={() => move(1)} className="flex h-12 w-12 items-center justify-center rounded-full border border-(--ak-rule) transition-colors hover:bg-(--ak-fg) hover:text-(--ak-paper)" aria-label="Témoignage suivant">
662
+ <ArrowRight className="h-4 w-4" />
663
+ </button>
664
+ </div>
665
+ ) : null}
666
+ </div>
667
+ </div>
668
+ </div>
669
+ </Reveal>
670
+ </Section>
671
+ );
672
+ }