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,298 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /**
8
+ * Format de paquet `.snapmod` (Node uniquement) : JSON compressé gzip contenant le manifeste
9
+ * empaqueté, les fichiers en base64, la signature Ed25519 du manifeste et, pour un module
10
+ * gratuit, sa licence signée. Le manifeste liste l'empreinte SHA-256 de chaque fichier :
11
+ * signer le manifeste revient donc à signer tout le contenu.
12
+ */
13
+ import crypto from 'crypto';
14
+ import zlib from 'zlib';
15
+ import {
16
+ LICENSE_KEY_PREFIX,
17
+ MODULE_ID_PATTERN,
18
+ MODULE_SLOTS,
19
+ PERMISSION_PATTERN,
20
+ ROUTE_PATTERN_PUBLIC,
21
+ SCREENSHOT_FILES,
22
+ SEMVER_PATTERN,
23
+ type LicensePayload,
24
+ type PackageKind,
25
+ type PackagedManifest,
26
+ type Signature,
27
+ type SignedLicense,
28
+ } from './index';
29
+ import { checkFieldDefinition } from './settings';
30
+
31
+ export const PACKAGE_FORMAT = 'snapmod/1';
32
+ export const PACKAGE_EXTENSION = '.snapmod';
33
+ export const THEME_PACKAGE_EXTENSION = '.snaptheme';
34
+ export const PACKAGE_EXTENSIONS = [PACKAGE_EXTENSION, THEME_PACKAGE_EXTENSION];
35
+ export const packageExtension = (kind: PackageKind) => (kind === 'theme' ? THEME_PACKAGE_EXTENSION : PACKAGE_EXTENSION);
36
+ export const MAX_PACKAGE_BYTES = 20 * 1024 * 1024;
37
+ const MAX_UNPACKED_BYTES = 40 * 1024 * 1024;
38
+
39
+ /** Seuls ces fichiers peuvent figurer dans un paquet. */
40
+ export const PACKAGE_FILES = ['public.js', 'public.css', 'server.mjs', 'admin.js', 'admin.css', 'README.md', ...SCREENSHOT_FILES] as const;
41
+
42
+ export interface ModulePackage {
43
+ format: typeof PACKAGE_FORMAT;
44
+ manifest: PackagedManifest;
45
+ files: Record<string, string>;
46
+ signature?: Signature;
47
+ license?: SignedLicense;
48
+ }
49
+
50
+ export class PackageError extends Error {}
51
+
52
+ /* ------------------------------------------------------------------ */
53
+ /* Primitives */
54
+ /* ------------------------------------------------------------------ */
55
+
56
+ /** JSON à clés triées : représentation stable et donc signable. */
57
+ export function canonicalJson(value: unknown): string {
58
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
59
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
60
+ const entries = Object.entries(value as Record<string, unknown>)
61
+ .filter(([, v]) => v !== undefined)
62
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
63
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(',')}}`;
64
+ }
65
+
66
+ export const sha256 = (data: Buffer | string) => crypto.createHash('sha256').update(data).digest('hex');
67
+
68
+ export function publicKeyFromBase64(b64: string): crypto.KeyObject {
69
+ return crypto.createPublicKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'spki' });
70
+ }
71
+
72
+ export function sign(data: unknown, privateKeyPem: string, kid: string): Signature {
73
+ const value = crypto.sign(null, Buffer.from(canonicalJson(data)), crypto.createPrivateKey(privateKeyPem)).toString('base64');
74
+ return { alg: 'ed25519', kid, value };
75
+ }
76
+
77
+ /** `trusted` : identifiant de clé → clé publique SPKI encodée en base64. */
78
+ export function verify(data: unknown, signature: Signature | undefined, trusted: Record<string, string>): boolean {
79
+ if (!signature || signature.alg !== 'ed25519' || typeof signature.value !== 'string') return false;
80
+ const key = trusted[signature.kid];
81
+ if (!key) return false;
82
+ try {
83
+ return crypto.verify(null, Buffer.from(canonicalJson(data)), publicKeyFromBase64(key), Buffer.from(signature.value, 'base64'));
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /* ------------------------------------------------------------------ */
90
+ /* Versions */
91
+ /* ------------------------------------------------------------------ */
92
+
93
+ function parseVersion(v: string): [number, number, number] | null {
94
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
95
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
96
+ }
97
+
98
+ export function compareVersions(a: string, b: string): number {
99
+ const pa = parseVersion(a) ?? [0, 0, 0];
100
+ const pb = parseVersion(b) ?? [0, 0, 0];
101
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
102
+ return 0;
103
+ }
104
+
105
+ /** Plages acceptées : `*`, `1.2.3`, `>=1.2.3`, `^1.2.3`. */
106
+ export function satisfies(version: string, range: string): boolean {
107
+ const r = range.trim();
108
+ if (r === '*') return true;
109
+ if (r.startsWith('>=')) return compareVersions(version, r.slice(2)) >= 0;
110
+ if (r.startsWith('^')) {
111
+ const base = parseVersion(r.slice(1));
112
+ const v = parseVersion(version);
113
+ if (!base || !v) return false;
114
+ return v[0] === base[0] && compareVersions(version, r.slice(1)) >= 0;
115
+ }
116
+ return compareVersions(version, r) === 0;
117
+ }
118
+
119
+ /* ------------------------------------------------------------------ */
120
+ /* Manifeste */
121
+ /* ------------------------------------------------------------------ */
122
+
123
+ const isStr = (v: unknown, max: number) => typeof v === 'string' && v.trim().length > 0 && v.length <= max;
124
+
125
+ /** Contrôle structurel d'un manifeste ; `packaged` exige les champs ajoutés à l'empaquetage. */
126
+ export function checkManifest(m: PackagedManifest, packaged: boolean): string[] {
127
+ const issues: string[] = [];
128
+ if (!m || typeof m !== 'object') return ['manifeste absent'];
129
+ if (typeof m.id !== 'string' || m.id.length > 64 || !MODULE_ID_PATTERN.test(m.id)) issues.push('identifiant invalide (format éditeur.module)');
130
+ if (typeof m.version !== 'string' || !SEMVER_PATTERN.test(m.version)) issues.push('version invalide (semver attendu)');
131
+ if (!isStr(m.name, 60)) issues.push('nom invalide');
132
+ if (!isStr(m.description, 500)) issues.push('description invalide');
133
+ if (!m.author || !isStr(m.author.name, 120)) issues.push('auteur invalide');
134
+ if (m.pricing !== 'free' && m.pricing !== 'paid') issues.push('tarification invalide (free ou paid)');
135
+ if (!m.engines || !isStr(m.engines.snapify, 40)) issues.push('engines.snapify manquant');
136
+ if (m.icon !== undefined && !/^[a-z0-9-]{1,40}$/.test(m.icon)) issues.push('icône invalide');
137
+ if (!Array.isArray(m.permissions) || m.permissions.some((p) => typeof p !== 'string' || !PERMISSION_PATTERN.test(p))) issues.push('permissions invalides');
138
+ else if (new Set(m.permissions).size !== m.permissions.length) issues.push('permissions en double');
139
+ if (!Array.isArray(m.slots) || m.slots.some((s) => !MODULE_SLOTS.includes(s))) issues.push('emplacements invalides');
140
+ if (!Array.isArray(m.settings)) issues.push('réglages invalides');
141
+ else {
142
+ m.settings.forEach((f) => issues.push(...checkFieldDefinition(f)));
143
+ if (new Set(m.settings.map((f) => f.key)).size !== m.settings.length) issues.push('clés de réglage en double');
144
+ }
145
+ if (m.kind !== undefined && m.kind !== 'module' && m.kind !== 'theme') issues.push('type de paquet invalide (module ou theme)');
146
+ if (m.kind === 'theme') {
147
+ if (m.permissions?.length) issues.push('un thème ne peut pas demander de permissions');
148
+ if (m.slots?.length) issues.push('un thème ne déclare pas d’emplacements');
149
+ if (m.entries?.server) issues.push('un thème ne peut pas contenir de code serveur');
150
+ if (m.entries?.admin) issues.push('un thème ne peut pas contenir de pages d’administration');
151
+ if (!m.entries?.public) issues.push('entrée public obligatoire pour un thème');
152
+ if (m.routes?.length || m.widgets?.length || m.adminPages?.length || m.requires || m.uses?.length) issues.push('routes, widgets, pages d’administration et dépendances sont réservés aux modules');
153
+ }
154
+ issues.push(...checkExtensions(m));
155
+ if (m.screenshot !== undefined && (typeof m.screenshot !== 'string' || (packaged && !(SCREENSHOT_FILES as readonly string[]).includes(m.screenshot)))) {
156
+ issues.push('capture d’écran invalide');
157
+ }
158
+ if (!m.entries || typeof m.entries !== 'object' || (!m.entries.public && !m.entries.server && !m.entries.admin)) issues.push('aucune entrée public/server/admin');
159
+ if (m.slots?.length && !m.entries?.public) issues.push('emplacements déclarés sans entrée public');
160
+ if ((m.routes?.length || m.widgets?.length) && !m.entries?.public) issues.push('routes ou widgets déclarés sans entrée public');
161
+ if (m.adminPages?.length && !m.entries?.admin) issues.push('pages d’administration déclarées sans entrée admin');
162
+ if (packaged) {
163
+ if (m.entries?.public && m.entries.public !== 'public.js') issues.push('entrée public empaquetée invalide');
164
+ if (m.entries?.server && m.entries.server !== 'server.mjs') issues.push('entrée server empaquetée invalide');
165
+ if (m.entries?.admin && m.entries.admin !== 'admin.js') issues.push('entrée admin empaquetée invalide');
166
+ if (!m.files || typeof m.files !== 'object') issues.push('liste des fichiers manquante');
167
+ else {
168
+ for (const [name, meta] of Object.entries(m.files)) {
169
+ if (!(PACKAGE_FILES as readonly string[]).includes(name)) issues.push(`fichier non autorisé : ${name}`);
170
+ if (!meta || !/^[a-f0-9]{64}$/.test(meta.sha256) || !Number.isInteger(meta.size) || meta.size < 0) issues.push(`empreinte invalide : ${name}`);
171
+ }
172
+ for (const entry of [m.entries?.public, m.entries?.server, m.entries?.admin, m.styles, m.adminStyles, m.screenshot]) if (entry && !m.files[entry]) issues.push(`fichier manquant : ${entry}`);
173
+ }
174
+ }
175
+ return issues;
176
+ }
177
+
178
+ const LOCAL_ID = /^[a-z][a-z0-9-]{0,39}$/;
179
+
180
+ /** Routes publiques, widgets, pages d'administration et dépendances (API 1.2). */
181
+ function checkExtensions(m: PackagedManifest): string[] {
182
+ const issues: string[] = [];
183
+ if (m.routes !== undefined) {
184
+ if (!Array.isArray(m.routes) || m.routes.length > 20 || m.routes.some((r) => typeof r !== 'string' || r === '/' || r === '/*' || !ROUTE_PATTERN_PUBLIC.test(r))) {
185
+ issues.push('routes invalides (ex. /boutique/* ou /panier/)');
186
+ } else if (m.routes.some((r) => /^\/(administrator|api|_modules|uploads|blog)\//.test(r))) issues.push('route réservée par Snapify');
187
+ }
188
+ if (m.widgets !== undefined) {
189
+ if (!Array.isArray(m.widgets) || m.widgets.length > 20) issues.push('widgets invalides');
190
+ else {
191
+ for (const w of m.widgets) {
192
+ if (!w || !LOCAL_ID.test(w.id ?? '')) issues.push('identifiant de widget invalide');
193
+ if (!isStr(w?.label, 60)) issues.push(`widget « ${w?.id} » : libellé invalide`);
194
+ if (!Array.isArray(w?.fields)) issues.push(`widget « ${w?.id} » : champs invalides`);
195
+ else w.fields.forEach((f) => issues.push(...checkFieldDefinition(f).map((i) => `widget « ${w.id} » : ${i}`)));
196
+ }
197
+ if (new Set(m.widgets.map((w) => w?.id)).size !== m.widgets.length) issues.push('widgets en double');
198
+ }
199
+ }
200
+ if (m.adminPages !== undefined) {
201
+ if (!Array.isArray(m.adminPages) || m.adminPages.length > 10) issues.push('pages d’administration invalides');
202
+ else {
203
+ for (const p of m.adminPages) {
204
+ if (!p || !LOCAL_ID.test(p.id ?? '')) issues.push('identifiant de page d’administration invalide');
205
+ if (!isStr(p?.label, 40)) issues.push(`page d’administration « ${p?.id} » : libellé invalide`);
206
+ if (p?.icon !== undefined && !/^[a-z0-9-]{1,40}$/.test(p.icon)) issues.push(`page d’administration « ${p.id} » : icône invalide`);
207
+ }
208
+ if (new Set(m.adminPages.map((p) => p?.id)).size !== m.adminPages.length) issues.push('pages d’administration en double');
209
+ }
210
+ }
211
+ if (m.requires !== undefined) {
212
+ if (!m.requires || typeof m.requires !== 'object' || Array.isArray(m.requires)) issues.push('dépendances invalides');
213
+ else {
214
+ for (const [id, range] of Object.entries(m.requires)) {
215
+ if (!MODULE_ID_PATTERN.test(id) || id === m.id || !isStr(range, 40)) issues.push(`dépendance invalide : ${id}`);
216
+ }
217
+ }
218
+ }
219
+ if (m.uses !== undefined && (!Array.isArray(m.uses) || m.uses.some((id) => typeof id !== 'string' || !MODULE_ID_PATTERN.test(id) || id === m.id))) {
220
+ issues.push('modules facultatifs invalides');
221
+ }
222
+ return issues;
223
+ }
224
+
225
+ /* ------------------------------------------------------------------ */
226
+ /* Paquet */
227
+ /* ------------------------------------------------------------------ */
228
+
229
+ export function encodePackage(pkg: ModulePackage): Buffer {
230
+ return zlib.gzipSync(Buffer.from(JSON.stringify(pkg)), { level: 9 });
231
+ }
232
+
233
+ /** Décompresse et contrôle un paquet : structure, manifeste et empreintes des fichiers. */
234
+ export function decodePackage(buf: Buffer): { pkg: ModulePackage; files: Map<string, Buffer> } {
235
+ if (buf.length > MAX_PACKAGE_BYTES) throw new PackageError('Paquet trop volumineux (20 Mo maximum)');
236
+ let pkg: ModulePackage;
237
+ try {
238
+ pkg = JSON.parse(zlib.gunzipSync(buf, { maxOutputLength: MAX_UNPACKED_BYTES }).toString('utf8'));
239
+ } catch {
240
+ throw new PackageError('Fichier illisible : ce n’est pas un paquet .snapmod valide');
241
+ }
242
+ if (!pkg || pkg.format !== PACKAGE_FORMAT) throw new PackageError('Format de paquet non pris en charge');
243
+ const issues = checkManifest(pkg.manifest, true);
244
+ if (issues.length) throw new PackageError(`Manifeste invalide : ${issues.join(', ')}`);
245
+ if (!pkg.files || typeof pkg.files !== 'object') throw new PackageError('Paquet sans fichiers');
246
+
247
+ const files = new Map<string, Buffer>();
248
+ const declared = Object.keys(pkg.manifest.files).sort();
249
+ const shipped = Object.keys(pkg.files).sort();
250
+ if (canonicalJson(declared) !== canonicalJson(shipped)) throw new PackageError('Les fichiers du paquet ne correspondent pas au manifeste');
251
+ for (const name of declared) {
252
+ const data = Buffer.from(String(pkg.files[name]), 'base64');
253
+ const meta = pkg.manifest.files[name];
254
+ if (data.length !== meta.size || sha256(data) !== meta.sha256) throw new PackageError(`Fichier altéré : ${name}`);
255
+ files.set(name, data);
256
+ }
257
+ return { pkg, files };
258
+ }
259
+
260
+ /* ------------------------------------------------------------------ */
261
+ /* Licences */
262
+ /* ------------------------------------------------------------------ */
263
+
264
+ export function encodeLicenseKey(license: SignedLicense): string {
265
+ return LICENSE_KEY_PREFIX + Buffer.from(JSON.stringify(license)).toString('base64url');
266
+ }
267
+
268
+ export function decodeLicenseKey(key: string): SignedLicense {
269
+ const trimmed = key.replace(/\s+/g, '');
270
+ if (!trimmed.startsWith(LICENSE_KEY_PREFIX)) throw new PackageError('Clé de licence invalide');
271
+ try {
272
+ const parsed = JSON.parse(Buffer.from(trimmed.slice(LICENSE_KEY_PREFIX.length), 'base64url').toString('utf8')) as SignedLicense;
273
+ if (!parsed?.payload || !parsed.signature) throw new Error();
274
+ return parsed;
275
+ } catch {
276
+ throw new PackageError('Clé de licence illisible');
277
+ }
278
+ }
279
+
280
+ export function checkLicensePayload(p: LicensePayload): string[] {
281
+ const issues: string[] = [];
282
+ if (!p || p.v !== 1) return ['version de licence inconnue'];
283
+ if (!isStr(p.licenseId, 80)) issues.push('identifiant de licence invalide');
284
+ if (typeof p.moduleId !== 'string' || !MODULE_ID_PATTERN.test(p.moduleId)) issues.push('module invalide');
285
+ if (!isStr(p.licensee, 160)) issues.push('titulaire invalide');
286
+ if (!['free', 'standard', 'pro', 'enterprise'].includes(p.plan)) issues.push('formule invalide');
287
+ if (!Array.isArray(p.domains) || !p.domains.length || p.domains.some((d) => typeof d !== 'string' || !/^(\*|(\*\.)?[a-z0-9.-]+)$/.test(d))) issues.push('domaines invalides');
288
+ if (Number.isNaN(Date.parse(p.issuedAt))) issues.push('date d’émission invalide');
289
+ if (p.expiresAt !== null && Number.isNaN(Date.parse(p.expiresAt))) issues.push('date d’expiration invalide');
290
+ return issues;
291
+ }
292
+
293
+ /** Le domaine du site est-il couvert par la licence ? `localhost` est toujours accepté pour le développement. */
294
+ export function domainAllowed(domains: string[], host: string): boolean {
295
+ const h = host.toLowerCase().replace(/^www\./, '');
296
+ if (h === 'localhost' || h === '127.0.0.1') return true;
297
+ return domains.some((d) => d === '*' || d === h || (d.startsWith('*.') && (h === d.slice(2) || h.endsWith(d.slice(1)))));
298
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Snapify — champ reCAPTCHA pour les formulaires publics (module hamdiayari.recaptcha)
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ */
5
+ import { useEffect, useRef, useState } from 'react';
6
+
7
+ export type RecaptchaPublicConfig = {
8
+ siteKey: string;
9
+ version: 'v2' | 'v3';
10
+ action?: string;
11
+ };
12
+
13
+ declare global {
14
+ interface Window {
15
+ grecaptcha?: {
16
+ ready: (cb: () => void) => void;
17
+ render: (el: HTMLElement, opts: Record<string, unknown>) => number;
18
+ getResponse: (id?: number) => string;
19
+ reset: (id?: number) => void;
20
+ execute: (siteKey: string, opts: { action: string }) => Promise<string>;
21
+ };
22
+ __snapifyRecaptcha?: {
23
+ siteKey: string;
24
+ version: 'v2' | 'v3';
25
+ action: string;
26
+ getToken: () => Promise<string>;
27
+ };
28
+ }
29
+ }
30
+
31
+ const SCRIPT_ID = 'snapify-recaptcha-script';
32
+
33
+ function ensureScript(siteKey: string, version: 'v2' | 'v3') {
34
+ if (document.getElementById(SCRIPT_ID)) return;
35
+ const s = document.createElement('script');
36
+ s.id = SCRIPT_ID;
37
+ s.async = true;
38
+ s.defer = true;
39
+ s.src =
40
+ version === 'v3'
41
+ ? `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKey)}`
42
+ : 'https://www.google.com/recaptcha/api.js?render=explicit';
43
+ document.head.appendChild(s);
44
+ }
45
+
46
+ /** Récupère un jeton (v2 widget ou v3 execute). */
47
+ export async function getRecaptchaToken(cfg?: RecaptchaPublicConfig | null): Promise<string> {
48
+ if (window.__snapifyRecaptcha?.getToken) return window.__snapifyRecaptcha.getToken();
49
+ if (!cfg?.siteKey) return '';
50
+ const version = cfg.version === 'v3' ? 'v3' : 'v2';
51
+ ensureScript(cfg.siteKey, version);
52
+ await new Promise<void>((resolve, reject) => {
53
+ let n = 0;
54
+ const tick = () => {
55
+ if (window.grecaptcha) return resolve();
56
+ if (n++ > 50) return reject(new Error('reCAPTCHA indisponible'));
57
+ setTimeout(tick, 100);
58
+ };
59
+ tick();
60
+ });
61
+ return new Promise((resolve, reject) => {
62
+ window.grecaptcha!.ready(() => {
63
+ if (version === 'v3') {
64
+ window
65
+ .grecaptcha!.execute(cfg.siteKey, { action: cfg.action || 'contact' })
66
+ .then(resolve)
67
+ .catch(() => reject(new Error('reCAPTCHA v3 impossible')));
68
+ } else {
69
+ const token = window.grecaptcha!.getResponse();
70
+ if (!token) reject(new Error('Cochez la case « Je ne suis pas un robot ».'));
71
+ else resolve(token);
72
+ }
73
+ });
74
+ });
75
+ }
76
+
77
+ /** Widget v2 (case à cocher). Invisible pour v3. */
78
+ export function RecaptchaField({ config }: { config?: RecaptchaPublicConfig | null }) {
79
+ const ref = useRef<HTMLDivElement>(null);
80
+ const widgetId = useRef<number | null>(null);
81
+ const [err, setErr] = useState('');
82
+
83
+ useEffect(() => {
84
+ if (!config?.siteKey || config.version === 'v3') return;
85
+ ensureScript(config.siteKey, 'v2');
86
+ let cancelled = false;
87
+ const tryRender = (n = 0) => {
88
+ if (cancelled || !ref.current) return;
89
+ if (!window.grecaptcha) {
90
+ if (n < 50) setTimeout(() => tryRender(n + 1), 100);
91
+ else setErr('Impossible de charger reCAPTCHA.');
92
+ return;
93
+ }
94
+ window.grecaptcha.ready(() => {
95
+ if (cancelled || !ref.current || widgetId.current !== null) return;
96
+ try {
97
+ widgetId.current = window.grecaptcha!.render(ref.current, {
98
+ sitekey: config.siteKey,
99
+ theme: 'light',
100
+ });
101
+ } catch {
102
+ setErr('Widget reCAPTCHA indisponible.');
103
+ }
104
+ });
105
+ };
106
+ tryRender();
107
+ return () => {
108
+ cancelled = true;
109
+ };
110
+ }, [config?.siteKey, config?.version]);
111
+
112
+ if (!config?.siteKey) return null;
113
+ if (config.version === 'v3') {
114
+ return <p className="text-[11px] text-slate-400">Protection anti-robot Google reCAPTCHA activée.</p>;
115
+ }
116
+ return (
117
+ <div className="space-y-1">
118
+ <div ref={ref} className="g-recaptcha" />
119
+ {err ? <p className="text-xs text-rose-600">{err}</p> : null}
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ import type { SettingField, SettingFieldType } from './index';
8
+
9
+ export const SETTING_TYPES: readonly SettingFieldType[] = ['boolean', 'text', 'textarea', 'number', 'select', 'phone', 'paths'];
10
+ export const SETTING_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]{0,40}$/;
11
+
12
+ const MAX_TEXT = 20_000;
13
+ const MAX_PATHS = 100;
14
+
15
+ /** Valeurs par défaut du manifeste. */
16
+ export function defaultSettings(fields: SettingField[]): Record<string, unknown> {
17
+ return Object.fromEntries(fields.map((f) => [f.key, structuredClone(f.default)]));
18
+ }
19
+
20
+ function checkValue(field: SettingField, value: unknown): string | null {
21
+ switch (field.type) {
22
+ case 'boolean':
23
+ return typeof value === 'boolean' ? null : 'Valeur oui/non attendue';
24
+ case 'number': {
25
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 'Nombre attendu';
26
+ if (field.min !== undefined && value < field.min) return `Minimum ${field.min}`;
27
+ if (field.max !== undefined && value > field.max) return `Maximum ${field.max}`;
28
+ return null;
29
+ }
30
+ case 'text':
31
+ case 'textarea':
32
+ case 'phone': {
33
+ if (typeof value !== 'string') return 'Texte attendu';
34
+ if (value.length > (field.maxLength ?? MAX_TEXT)) return `${field.maxLength ?? MAX_TEXT} caractères maximum`;
35
+ if (field.required && !value.trim()) return 'Champ obligatoire';
36
+ if (field.type === 'phone' && value.trim() && value.replace(/\D/g, '').length < 8) return 'Numéro trop court';
37
+ return null;
38
+ }
39
+ case 'select':
40
+ return typeof value === 'string' && (field.options ?? []).some((o) => o.value === value) ? null : 'Option inconnue';
41
+ case 'paths':
42
+ if (!Array.isArray(value) || value.length > MAX_PATHS) return 'Liste d’adresses invalide';
43
+ return value.every((p) => typeof p === 'string' && /^\/[^\s]{0,200}$/.test(p)) ? null : 'Chaque adresse doit commencer par /';
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Valide un objet de réglages selon le manifeste. Les clés inconnues sont ignorées,
49
+ * les clés absentes reprennent la valeur `base` (réglages courants) puis la valeur par défaut.
50
+ */
51
+ export function validateSettings(
52
+ fields: SettingField[],
53
+ input: unknown,
54
+ base: Record<string, unknown> = {}
55
+ ): { value: Record<string, unknown>; errors: Record<string, string> } {
56
+ const source = input && typeof input === 'object' && !Array.isArray(input) ? (input as Record<string, unknown>) : {};
57
+ const value: Record<string, unknown> = {};
58
+ const errors: Record<string, string> = {};
59
+ for (const field of fields) {
60
+ const candidate = field.key in source ? source[field.key] : field.key in base ? base[field.key] : field.default;
61
+ const error = checkValue(field, candidate);
62
+ if (error) errors[field.key] = error;
63
+ value[field.key] = error ? structuredClone(field.default) : structuredClone(candidate);
64
+ }
65
+ return { value, errors };
66
+ }
67
+
68
+ /** Réglages destinés au navigateur des visiteurs (`public: true`). */
69
+ export function publicSettings(fields: SettingField[], settings: Record<string, unknown>): Record<string, unknown> {
70
+ return Object.fromEntries(fields.filter((f) => f.public).map((f) => [f.key, settings[f.key]]));
71
+ }
72
+
73
+ /** Vérifie la cohérence d'une définition de champ ; renvoie la liste des problèmes. */
74
+ export function checkFieldDefinition(field: SettingField): string[] {
75
+ const issues: string[] = [];
76
+ const where = `réglage « ${field?.key ?? '?'} »`;
77
+ if (!field || typeof field !== 'object') return ['définition de réglage invalide'];
78
+ if (!SETTING_KEY_PATTERN.test(field.key ?? '')) issues.push(`${where} : clé invalide`);
79
+ if (!SETTING_TYPES.includes(field.type)) issues.push(`${where} : type inconnu`);
80
+ if (typeof field.label !== 'string' || !field.label.trim() || field.label.length > 120) issues.push(`${where} : libellé invalide`);
81
+ if (field.type === 'select' && (!Array.isArray(field.options) || field.options.length === 0)) issues.push(`${where} : options manquantes`);
82
+ if (SETTING_TYPES.includes(field.type) && checkValue({ ...field, required: false }, field.default)) issues.push(`${where} : valeur par défaut invalide`);
83
+ return issues;
84
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /*
8
+ * Jetons de marque disponibles dans les modules (bg-px-navy, text-px-primary…).
9
+ * Les valeurs proviennent des variables CSS du site : un module suit automatiquement
10
+ * l'identité visuelle configurée dans l'administration.
11
+ */
12
+ @theme inline reference {
13
+ --color-px-primary: var(--px-primary);
14
+ --color-px-primary-dark: var(--px-primary-dark);
15
+ --color-px-primary-light: var(--px-primary-light);
16
+ --color-px-accent: var(--px-accent);
17
+ --color-px-accent-mid: var(--px-accent-mid);
18
+ --color-px-accent-light: var(--px-accent-light);
19
+ --color-px-navy: var(--px-navy);
20
+ --color-px-navy-2: var(--px-navy-2);
21
+ --color-px-surface: var(--px-surface);
22
+ --color-px-surface-2: var(--px-surface-2);
23
+ --color-px-border: var(--px-border);
24
+ --color-px-border-soft: var(--px-border-soft);
25
+ --color-px-text: var(--px-text);
26
+ --color-px-muted: var(--px-muted);
27
+ --font-sans: var(--px-font-body), system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
+ --font-heading: var(--px-font-heading), var(--px-font-body), system-ui, sans-serif;
29
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /**
8
+ * Contrat d'un thème : composants de présentation (en-tête, pied de page, rendu des blocs…).
9
+ * Le site hôte charge les données, gère la navigation, l'aperçu et les modules ; le thème
10
+ * ne fait qu'afficher. React et le contexte du site sont fournis par l'hôte à l'exécution.
11
+ */
12
+ import { createElement, useContext, type ComponentType, type Context, type ReactNode } from 'react';
13
+ import type { BlockDataMap, BlockKind, BlockStyle, BlogPost, CmsPage, SiteConfigPayload, SiteInfo, TokenValues } from './content';
14
+
15
+ export interface ThemeRuntimeValue<S = Record<string, unknown>> {
16
+ site: SiteConfigPayload;
17
+ info: SiteInfo;
18
+ /** Valeurs des variables {{telephone}}, {{email}}… utilisables dans les textes. */
19
+ tokens: TokenValues;
20
+ /** Options du thème choisies dans « Apparence > Personnaliser ». */
21
+ settings: S;
22
+ /** Adresse de la page affichée, toujours terminée par `/`. */
23
+ path: string;
24
+ navigate: (path: string) => void;
25
+ openCookies: () => void;
26
+ /** Site affiché dans l'aperçu de l'administration (aucune action réelle : formulaires, liens externes). */
27
+ isPreview: boolean;
28
+ }
29
+
30
+ export interface ThemeChromeProps {
31
+ path: string;
32
+ navigate: (path: string) => void;
33
+ openCookies: () => void;
34
+ }
35
+
36
+ export interface BlockProps<K extends BlockKind> {
37
+ /** Données du bloc, complétées par les valeurs par défaut. */
38
+ d: BlockDataMap[K];
39
+ style: BlockStyle;
40
+ }
41
+
42
+ export type BlockRenderers = { [K in BlockKind]: ComponentType<BlockProps<K>> };
43
+
44
+ export interface ThemeDefinition {
45
+ Header: ComponentType<ThemeChromeProps>;
46
+ Footer: ComponentType<ThemeChromeProps>;
47
+ /** Rendu de chaque type de bloc. Un bloc sans rendu n'est pas affiché. */
48
+ blocks: Partial<BlockRenderers>;
49
+ /** Enveloppe de tout le site : applique les options du thème (variables CSS, classes…). */
50
+ Layout?: ComponentType<{ children: ReactNode }>;
51
+ /** Mise en page d'une page ; `children` contient les blocs déjà rendus. */
52
+ Page?: ComponentType<{ page: CmsPage; children: ReactNode }>;
53
+ /** Mise en page d'un article ; `children` contient les blocs déjà rendus. */
54
+ Post?: ComponentType<{ post: BlogPost; children: ReactNode }>;
55
+ NotFound?: ComponentType<{ navigate: (path: string) => void }>;
56
+ }
57
+
58
+ export function defineTheme(theme: ThemeDefinition): ThemeDefinition {
59
+ return theme;
60
+ }
61
+
62
+ interface HostRuntime {
63
+ ThemeContext?: Context<ThemeRuntimeValue | null>;
64
+ isPreview?: boolean;
65
+ ModuleSlot?: ComponentType<{ name: ModuleSlotName }>;
66
+ activeModules?: string[];
67
+ slotModules?: Partial<Record<ModuleSlotName, string[]>>;
68
+ }
69
+
70
+ const host = () => (globalThis as { __SNAPIFY_RUNTIME__?: HostRuntime }).__SNAPIFY_RUNTIME__;
71
+
72
+ /** Emplacements que le thème place lui-même dans sa mise en page. */
73
+ export type ModuleSlotName = 'header-actions';
74
+
75
+ /**
76
+ * Affiche les éléments des modules actifs pour cet emplacement (ex. icônes panier et compte
77
+ * dans l'en-tête). Rien n'est rendu si aucun module ne l'utilise.
78
+ */
79
+ export function ModuleSlot({ name }: { name: ModuleSlotName }) {
80
+ const Slot = host()?.ModuleSlot;
81
+ return Slot ? createElement(Slot, { name }) : null;
82
+ }
83
+
84
+ /** Un module actif occupe-t-il cet emplacement ? */
85
+ export const hasModuleSlot = (name: ModuleSlotName): boolean => !!host()?.slotModules?.[name]?.length;
86
+
87
+ /** Module actif sur le site (ex. `novalabs.shop`). */
88
+ export const isModuleActive = (id: string): boolean => !!host()?.activeModules?.includes(id);
89
+
90
+ /** Données du site et options du thème. Utilisable dans tout composant du thème. */
91
+ export function useSnapify<S = Record<string, unknown>>(): ThemeRuntimeValue<S> {
92
+ const ctx = host()?.ThemeContext;
93
+ if (!ctx) throw new Error('useSnapify() doit être utilisé dans un thème chargé par Snapify');
94
+ const value = useContext(ctx);
95
+ if (!value) throw new Error('useSnapify() appelé hors du site');
96
+ return value as ThemeRuntimeValue<S>;
97
+ }
98
+
99
+ /** Équivalent hors composant de `useSnapify().isPreview`. */
100
+ export const isPreviewMode = (): boolean => !!host()?.isPreview;