@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211

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 (908) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +51 -51
  4. package/astro.mjs +162 -162
  5. package/bin/morpheme.mjs +38 -38
  6. package/client.d.ts +10 -10
  7. package/defaults/blockTexts.ts +33 -33
  8. package/defaults/collections.ts +774 -774
  9. package/defaults/colors.ts +125 -125
  10. package/defaults/company.ts +20 -20
  11. package/defaults/editor.ts +178 -178
  12. package/defaults/internalLinks.ts +30 -30
  13. package/defaults/opens.ts +18 -18
  14. package/defaults/popup.tsx +93 -93
  15. package/defaults/privacy.ts +79 -79
  16. package/defaults/rules.ts +450 -450
  17. package/defaults/schema.ts +16 -16
  18. package/defaults/sitedata.ts +129 -129
  19. package/defaults/standard.ts +106 -106
  20. package/defaults/standardPages.ts +87 -87
  21. package/defaults/texts.ts +481 -481
  22. package/demo/README.md +26 -26
  23. package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
  24. package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
  25. package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
  26. package/demo/src/content/blog/every-save-is-a-version.md +21 -21
  27. package/demo/src/content/blog/schedule-a-post.md +17 -17
  28. package/demo/src/content/blog/secure-from-day-one.md +17 -17
  29. package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
  30. package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
  31. package/demo/src/content/blog/the-app-store.md +21 -21
  32. package/demo/src/content/pages/about.md +22 -22
  33. package/demo/src/content/pages/app-store.md +30 -30
  34. package/demo/src/content/pages/blocks.md +98 -98
  35. package/demo/src/content/pages/blog-page.md +11 -11
  36. package/demo/src/content/pages/contact.md +16 -16
  37. package/demo/src/content/pages/get-started.md +20 -20
  38. package/demo/src/content/pages/home.md +26 -26
  39. package/demo/src/content/pages/page-not-found.md +11 -11
  40. package/demo/src/content/pages/privacy-policy.md +24 -24
  41. package/demo/src/data/blocks.json +25 -25
  42. package/demo/src/data/booking.json +6 -6
  43. package/demo/src/data/categories/apps.json +6 -6
  44. package/demo/src/data/categories/design.json +6 -6
  45. package/demo/src/data/categories/editing.json +6 -6
  46. package/demo/src/data/categories/security.json +6 -6
  47. package/demo/src/data/categories/seo.json +6 -6
  48. package/demo/src/data/colors.json +75 -75
  49. package/demo/src/data/contact.json +8 -8
  50. package/demo/src/data/cookies.json +21 -21
  51. package/demo/src/data/footer.json +27 -27
  52. package/demo/src/data/menu.json +35 -35
  53. package/demo/src/data/mobile-menu.json +25 -25
  54. package/demo/src/data/popups/booking-bar.json +13 -13
  55. package/demo/src/data/popups/try-the-editor.json +16 -16
  56. package/demo/src/data/standard-pages.json +8 -8
  57. package/demo/src/data/testimonials/sample-bakery.json +10 -10
  58. package/demo/src/data/testimonials/sample-clinic.json +10 -10
  59. package/demo/src/data/testimonials/sample-studio.json +10 -10
  60. package/demo/src/site/colors.ts +11 -11
  61. package/demo/src/site/config.ts +19 -19
  62. package/icons/lucide/LICENSE +43 -43
  63. package/icons/lucide/address-book.svg +1 -1
  64. package/icons/lucide/app-window.svg +1 -1
  65. package/icons/lucide/arrow-counter-clockwise.svg +1 -1
  66. package/icons/lucide/arrow-down.svg +1 -1
  67. package/icons/lucide/arrow-left.svg +1 -1
  68. package/icons/lucide/arrow-right.svg +1 -1
  69. package/icons/lucide/arrow-square-out.svg +1 -1
  70. package/icons/lucide/arrow-u-up-left.svg +1 -1
  71. package/icons/lucide/arrow-u-up-right.svg +1 -1
  72. package/icons/lucide/arrow-up-right.svg +1 -1
  73. package/icons/lucide/arrow-up.svg +1 -1
  74. package/icons/lucide/arrows-down-up.svg +1 -1
  75. package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
  76. package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
  77. package/icons/lucide/article.svg +1 -1
  78. package/icons/lucide/bank.svg +1 -1
  79. package/icons/lucide/bell.svg +1 -1
  80. package/icons/lucide/bicycle.svg +1 -1
  81. package/icons/lucide/binoculars.svg +1 -1
  82. package/icons/lucide/book-open-text.svg +1 -1
  83. package/icons/lucide/book-open.svg +1 -1
  84. package/icons/lucide/browser.svg +1 -1
  85. package/icons/lucide/buildings.svg +1 -1
  86. package/icons/lucide/bus.svg +1 -1
  87. package/icons/lucide/calendar-blank.svg +1 -1
  88. package/icons/lucide/calendar-check.svg +1 -1
  89. package/icons/lucide/car.svg +1 -1
  90. package/icons/lucide/cards.svg +1 -1
  91. package/icons/lucide/caret-down.svg +1 -1
  92. package/icons/lucide/caret-left.svg +1 -1
  93. package/icons/lucide/caret-right.svg +1 -1
  94. package/icons/lucide/caret-up.svg +1 -1
  95. package/icons/lucide/certificate.svg +1 -1
  96. package/icons/lucide/chart-line-up.svg +1 -1
  97. package/icons/lucide/chats.svg +1 -1
  98. package/icons/lucide/check-circle.svg +1 -1
  99. package/icons/lucide/check.svg +1 -1
  100. package/icons/lucide/circle-half.svg +1 -1
  101. package/icons/lucide/circle.svg +1 -1
  102. package/icons/lucide/clock-counter-clockwise.svg +1 -1
  103. package/icons/lucide/clock.svg +1 -1
  104. package/icons/lucide/code.svg +1 -1
  105. package/icons/lucide/coffee.svg +1 -1
  106. package/icons/lucide/columns.svg +1 -1
  107. package/icons/lucide/cookie.svg +1 -1
  108. package/icons/lucide/copy.svg +1 -1
  109. package/icons/lucide/credit-card.svg +1 -1
  110. package/icons/lucide/currency-circle-dollar.svg +1 -1
  111. package/icons/lucide/cursor-click.svg +1 -1
  112. package/icons/lucide/database.svg +1 -1
  113. package/icons/lucide/desktop.svg +1 -1
  114. package/icons/lucide/device-mobile.svg +1 -1
  115. package/icons/lucide/door-open.svg +1 -1
  116. package/icons/lucide/download-simple.svg +1 -1
  117. package/icons/lucide/envelope-simple.svg +1 -1
  118. package/icons/lucide/eye-slash.svg +1 -1
  119. package/icons/lucide/eye.svg +1 -1
  120. package/icons/lucide/file-pdf.svg +1 -1
  121. package/icons/lucide/file-text.svg +1 -1
  122. package/icons/lucide/file-video.svg +1 -1
  123. package/icons/lucide/file-zip.svg +1 -1
  124. package/icons/lucide/flower-lotus.svg +1 -1
  125. package/icons/lucide/folder-simple.svg +1 -1
  126. package/icons/lucide/funnel.svg +1 -1
  127. package/icons/lucide/gauge.svg +1 -1
  128. package/icons/lucide/gear.svg +1 -1
  129. package/icons/lucide/github-logo.svg +1 -1
  130. package/icons/lucide/globe.svg +1 -1
  131. package/icons/lucide/google-logo.svg +1 -1
  132. package/icons/lucide/graduation-cap.svg +1 -1
  133. package/icons/lucide/hammer.svg +1 -1
  134. package/icons/lucide/hand-heart.svg +1 -1
  135. package/icons/lucide/handshake.svg +1 -1
  136. package/icons/lucide/hard-drives.svg +1 -1
  137. package/icons/lucide/heart.svg +1 -1
  138. package/icons/lucide/house.svg +1 -1
  139. package/icons/lucide/image-square.svg +1 -1
  140. package/icons/lucide/image.svg +1 -1
  141. package/icons/lucide/info.svg +1 -1
  142. package/icons/lucide/keyboard.svg +1 -1
  143. package/icons/lucide/leaf.svg +1 -1
  144. package/icons/lucide/lifebuoy.svg +1 -1
  145. package/icons/lucide/lightbulb.svg +1 -1
  146. package/icons/lucide/link-break.svg +1 -1
  147. package/icons/lucide/link-simple.svg +1 -1
  148. package/icons/lucide/list-bullets.svg +1 -1
  149. package/icons/lucide/list-checks.svg +1 -1
  150. package/icons/lucide/list-numbers.svg +1 -1
  151. package/icons/lucide/list.svg +1 -1
  152. package/icons/lucide/lock-simple-open.svg +1 -1
  153. package/icons/lucide/lock-simple.svg +1 -1
  154. package/icons/lucide/magnifying-glass.svg +1 -1
  155. package/icons/lucide/map-pin.svg +1 -1
  156. package/icons/lucide/map-trifold.svg +1 -1
  157. package/icons/lucide/medal.svg +1 -1
  158. package/icons/lucide/megaphone.svg +1 -1
  159. package/icons/lucide/minus.svg +1 -1
  160. package/icons/lucide/moon.svg +1 -1
  161. package/icons/lucide/newspaper.svg +1 -1
  162. package/icons/lucide/pack.json +23 -23
  163. package/icons/lucide/paint-brush-broad.svg +1 -1
  164. package/icons/lucide/palette.svg +1 -1
  165. package/icons/lucide/paper-plane-tilt.svg +1 -1
  166. package/icons/lucide/pencil-simple.svg +1 -1
  167. package/icons/lucide/phone.svg +1 -1
  168. package/icons/lucide/play-fill.svg +1 -1
  169. package/icons/lucide/plus.svg +1 -1
  170. package/icons/lucide/prohibit.svg +1 -1
  171. package/icons/lucide/puzzle-piece.svg +1 -1
  172. package/icons/lucide/question.svg +1 -1
  173. package/icons/lucide/quotes.svg +1 -1
  174. package/icons/lucide/rows.svg +1 -1
  175. package/icons/lucide/ruler.svg +1 -1
  176. package/icons/lucide/scan.svg +1 -1
  177. package/icons/lucide/shapes.svg +1 -1
  178. package/icons/lucide/share-network.svg +1 -1
  179. package/icons/lucide/shield-check.svg +1 -1
  180. package/icons/lucide/sidebar-simple.svg +1 -1
  181. package/icons/lucide/sign-out.svg +1 -1
  182. package/icons/lucide/signpost.svg +1 -1
  183. package/icons/lucide/sliders-horizontal.svg +1 -1
  184. package/icons/lucide/smiley.svg +1 -1
  185. package/icons/lucide/sparkle.svg +1 -1
  186. package/icons/lucide/square-half-bottom.svg +1 -1
  187. package/icons/lucide/square.svg +1 -1
  188. package/icons/lucide/squares-four.svg +1 -1
  189. package/icons/lucide/sticker.svg +1 -1
  190. package/icons/lucide/storefront.svg +1 -1
  191. package/icons/lucide/sun.svg +1 -1
  192. package/icons/lucide/swatches.svg +1 -1
  193. package/icons/lucide/tag.svg +1 -1
  194. package/icons/lucide/text-aa.svg +1 -1
  195. package/icons/lucide/text-align-left.svg +1 -1
  196. package/icons/lucide/text-b.svg +1 -1
  197. package/icons/lucide/text-h-one.svg +1 -1
  198. package/icons/lucide/text-h-three.svg +1 -1
  199. package/icons/lucide/text-h-two.svg +1 -1
  200. package/icons/lucide/text-italic.svg +1 -1
  201. package/icons/lucide/text-t.svg +1 -1
  202. package/icons/lucide/ticket.svg +1 -1
  203. package/icons/lucide/toggle-right.svg +1 -1
  204. package/icons/lucide/train.svg +1 -1
  205. package/icons/lucide/trash.svg +1 -1
  206. package/icons/lucide/upload-simple.svg +1 -1
  207. package/icons/lucide/user-circle.svg +1 -1
  208. package/icons/lucide/user.svg +1 -1
  209. package/icons/lucide/users-three.svg +1 -1
  210. package/icons/lucide/users.svg +1 -1
  211. package/icons/lucide/video-camera.svg +1 -1
  212. package/icons/lucide/warning-circle.svg +1 -1
  213. package/icons/lucide/warning.svg +1 -1
  214. package/icons/lucide/wave-sine.svg +1 -1
  215. package/icons/lucide/wheelchair.svg +1 -1
  216. package/icons/lucide/wrench.svg +1 -1
  217. package/icons/lucide/x.svg +1 -1
  218. package/icons/material-symbols/LICENSE +201 -201
  219. package/icons/material-symbols/address-book.svg +1 -1
  220. package/icons/material-symbols/app-window.svg +1 -1
  221. package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
  222. package/icons/material-symbols/arrow-down.svg +1 -1
  223. package/icons/material-symbols/arrow-left.svg +1 -1
  224. package/icons/material-symbols/arrow-right.svg +1 -1
  225. package/icons/material-symbols/arrow-square-out.svg +1 -1
  226. package/icons/material-symbols/arrow-u-up-left.svg +1 -1
  227. package/icons/material-symbols/arrow-u-up-right.svg +1 -1
  228. package/icons/material-symbols/arrow-up-right.svg +1 -1
  229. package/icons/material-symbols/arrow-up.svg +1 -1
  230. package/icons/material-symbols/arrows-down-up.svg +1 -1
  231. package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
  232. package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
  233. package/icons/material-symbols/article.svg +1 -1
  234. package/icons/material-symbols/bank.svg +1 -1
  235. package/icons/material-symbols/bell.svg +1 -1
  236. package/icons/material-symbols/bicycle.svg +1 -1
  237. package/icons/material-symbols/binoculars.svg +1 -1
  238. package/icons/material-symbols/book-open-text.svg +1 -1
  239. package/icons/material-symbols/book-open.svg +1 -1
  240. package/icons/material-symbols/browser.svg +1 -1
  241. package/icons/material-symbols/buildings.svg +1 -1
  242. package/icons/material-symbols/bus.svg +1 -1
  243. package/icons/material-symbols/calendar-blank.svg +1 -1
  244. package/icons/material-symbols/calendar-check.svg +1 -1
  245. package/icons/material-symbols/car.svg +1 -1
  246. package/icons/material-symbols/cards.svg +1 -1
  247. package/icons/material-symbols/caret-down.svg +1 -1
  248. package/icons/material-symbols/caret-left.svg +1 -1
  249. package/icons/material-symbols/caret-right.svg +1 -1
  250. package/icons/material-symbols/caret-up.svg +1 -1
  251. package/icons/material-symbols/certificate.svg +1 -1
  252. package/icons/material-symbols/chart-line-up.svg +1 -1
  253. package/icons/material-symbols/chats.svg +1 -1
  254. package/icons/material-symbols/check-circle.svg +1 -1
  255. package/icons/material-symbols/check.svg +1 -1
  256. package/icons/material-symbols/circle-half.svg +1 -1
  257. package/icons/material-symbols/circle.svg +1 -1
  258. package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
  259. package/icons/material-symbols/clock.svg +1 -1
  260. package/icons/material-symbols/code.svg +1 -1
  261. package/icons/material-symbols/coffee.svg +1 -1
  262. package/icons/material-symbols/columns.svg +1 -1
  263. package/icons/material-symbols/cookie.svg +1 -1
  264. package/icons/material-symbols/copy.svg +1 -1
  265. package/icons/material-symbols/credit-card.svg +1 -1
  266. package/icons/material-symbols/currency-circle-dollar.svg +1 -1
  267. package/icons/material-symbols/cursor-click.svg +1 -1
  268. package/icons/material-symbols/database.svg +1 -1
  269. package/icons/material-symbols/desktop.svg +1 -1
  270. package/icons/material-symbols/device-mobile.svg +1 -1
  271. package/icons/material-symbols/door-open.svg +1 -1
  272. package/icons/material-symbols/download-simple.svg +1 -1
  273. package/icons/material-symbols/envelope-simple.svg +1 -1
  274. package/icons/material-symbols/eye-slash.svg +1 -1
  275. package/icons/material-symbols/eye.svg +1 -1
  276. package/icons/material-symbols/file-pdf.svg +1 -1
  277. package/icons/material-symbols/file-text.svg +1 -1
  278. package/icons/material-symbols/file-video.svg +1 -1
  279. package/icons/material-symbols/file-zip.svg +1 -1
  280. package/icons/material-symbols/flower-lotus.svg +1 -1
  281. package/icons/material-symbols/folder-simple.svg +1 -1
  282. package/icons/material-symbols/funnel.svg +1 -1
  283. package/icons/material-symbols/gauge.svg +1 -1
  284. package/icons/material-symbols/gear.svg +1 -1
  285. package/icons/material-symbols/github-logo.svg +1 -1
  286. package/icons/material-symbols/globe.svg +1 -1
  287. package/icons/material-symbols/google-logo.svg +1 -1
  288. package/icons/material-symbols/graduation-cap.svg +1 -1
  289. package/icons/material-symbols/hammer.svg +1 -1
  290. package/icons/material-symbols/hand-heart.svg +1 -1
  291. package/icons/material-symbols/handshake.svg +1 -1
  292. package/icons/material-symbols/hard-drives.svg +1 -1
  293. package/icons/material-symbols/heart.svg +1 -1
  294. package/icons/material-symbols/house.svg +1 -1
  295. package/icons/material-symbols/image-square.svg +1 -1
  296. package/icons/material-symbols/image.svg +1 -1
  297. package/icons/material-symbols/info.svg +1 -1
  298. package/icons/material-symbols/keyboard.svg +1 -1
  299. package/icons/material-symbols/leaf.svg +1 -1
  300. package/icons/material-symbols/lifebuoy.svg +1 -1
  301. package/icons/material-symbols/lightbulb.svg +1 -1
  302. package/icons/material-symbols/link-break.svg +1 -1
  303. package/icons/material-symbols/link-simple.svg +1 -1
  304. package/icons/material-symbols/list-bullets.svg +1 -1
  305. package/icons/material-symbols/list-checks.svg +1 -1
  306. package/icons/material-symbols/list-numbers.svg +1 -1
  307. package/icons/material-symbols/list.svg +1 -1
  308. package/icons/material-symbols/lock-simple-open.svg +1 -1
  309. package/icons/material-symbols/lock-simple.svg +1 -1
  310. package/icons/material-symbols/magnifying-glass.svg +1 -1
  311. package/icons/material-symbols/map-pin.svg +1 -1
  312. package/icons/material-symbols/map-trifold.svg +1 -1
  313. package/icons/material-symbols/medal.svg +1 -1
  314. package/icons/material-symbols/megaphone.svg +1 -1
  315. package/icons/material-symbols/minus.svg +1 -1
  316. package/icons/material-symbols/moon.svg +1 -1
  317. package/icons/material-symbols/newspaper.svg +1 -1
  318. package/icons/material-symbols/pack.json +29 -29
  319. package/icons/material-symbols/paint-brush-broad.svg +1 -1
  320. package/icons/material-symbols/palette.svg +1 -1
  321. package/icons/material-symbols/paper-plane-tilt.svg +1 -1
  322. package/icons/material-symbols/pencil-simple.svg +1 -1
  323. package/icons/material-symbols/phone.svg +1 -1
  324. package/icons/material-symbols/play-fill.svg +1 -1
  325. package/icons/material-symbols/plus.svg +1 -1
  326. package/icons/material-symbols/prohibit.svg +1 -1
  327. package/icons/material-symbols/puzzle-piece.svg +1 -1
  328. package/icons/material-symbols/question.svg +1 -1
  329. package/icons/material-symbols/quotes.svg +1 -1
  330. package/icons/material-symbols/rows.svg +1 -1
  331. package/icons/material-symbols/ruler.svg +1 -1
  332. package/icons/material-symbols/scan.svg +1 -1
  333. package/icons/material-symbols/shapes.svg +1 -1
  334. package/icons/material-symbols/share-network.svg +1 -1
  335. package/icons/material-symbols/shield-check.svg +1 -1
  336. package/icons/material-symbols/sidebar-simple.svg +1 -1
  337. package/icons/material-symbols/sign-out.svg +1 -1
  338. package/icons/material-symbols/signpost.svg +1 -1
  339. package/icons/material-symbols/sliders-horizontal.svg +1 -1
  340. package/icons/material-symbols/smiley.svg +1 -1
  341. package/icons/material-symbols/sparkle.svg +1 -1
  342. package/icons/material-symbols/square-half-bottom.svg +1 -1
  343. package/icons/material-symbols/square.svg +1 -1
  344. package/icons/material-symbols/squares-four.svg +1 -1
  345. package/icons/material-symbols/sticker.svg +1 -1
  346. package/icons/material-symbols/storefront.svg +1 -1
  347. package/icons/material-symbols/sun.svg +1 -1
  348. package/icons/material-symbols/swatches.svg +1 -1
  349. package/icons/material-symbols/tag.svg +1 -1
  350. package/icons/material-symbols/text-aa.svg +1 -1
  351. package/icons/material-symbols/text-align-left.svg +1 -1
  352. package/icons/material-symbols/text-b.svg +1 -1
  353. package/icons/material-symbols/text-h-one.svg +1 -1
  354. package/icons/material-symbols/text-h-three.svg +1 -1
  355. package/icons/material-symbols/text-h-two.svg +1 -1
  356. package/icons/material-symbols/text-italic.svg +1 -1
  357. package/icons/material-symbols/text-t.svg +1 -1
  358. package/icons/material-symbols/ticket.svg +1 -1
  359. package/icons/material-symbols/toggle-right.svg +1 -1
  360. package/icons/material-symbols/train.svg +1 -1
  361. package/icons/material-symbols/trash.svg +1 -1
  362. package/icons/material-symbols/upload-simple.svg +1 -1
  363. package/icons/material-symbols/user-circle.svg +1 -1
  364. package/icons/material-symbols/user.svg +1 -1
  365. package/icons/material-symbols/users-three.svg +1 -1
  366. package/icons/material-symbols/users.svg +1 -1
  367. package/icons/material-symbols/video-camera.svg +1 -1
  368. package/icons/material-symbols/warning-circle.svg +1 -1
  369. package/icons/material-symbols/warning.svg +1 -1
  370. package/icons/material-symbols/wave-sine.svg +1 -1
  371. package/icons/material-symbols/wheelchair.svg +1 -1
  372. package/icons/material-symbols/wrench.svg +1 -1
  373. package/icons/material-symbols/x.svg +1 -1
  374. package/icons/phosphor/pack.json +8 -8
  375. package/lib/apps.mjs +142 -139
  376. package/lib/apps.test.d.mts +1 -0
  377. package/lib/apps.test.mjs +50 -0
  378. package/lib/blocks.mjs +90 -90
  379. package/lib/caseCollisions.d.mts +4 -0
  380. package/lib/caseCollisions.mjs +62 -0
  381. package/lib/caseCollisions.test.d.mts +1 -0
  382. package/lib/caseCollisions.test.mjs +74 -0
  383. package/lib/colors.ts +86 -86
  384. package/lib/contactBar.ts +18 -18
  385. package/lib/contracts.d.mts +15 -0
  386. package/lib/contracts.mjs +17 -0
  387. package/lib/cookieCategories.mjs +28 -28
  388. package/lib/crop.mjs +83 -83
  389. package/lib/csp.mjs +130 -130
  390. package/lib/cspSources.mjs +60 -60
  391. package/lib/dynamic-colors.ts +426 -426
  392. package/lib/git.mjs +64 -64
  393. package/lib/hooks.ts +51 -51
  394. package/lib/inline.ts +20 -20
  395. package/lib/license-texts/Apache-2.0.txt +190 -190
  396. package/lib/license-texts/BSD-2-Clause.txt +24 -24
  397. package/lib/license-texts/BSD-3-Clause.txt +28 -28
  398. package/lib/license-texts/ISC.txt +15 -15
  399. package/lib/license-texts/MIT.txt +21 -21
  400. package/lib/licenses.d.mts +1 -1
  401. package/lib/licenses.mjs +683 -683
  402. package/lib/markdown.ts +42 -42
  403. package/lib/motion-schemes.mjs +5 -5
  404. package/lib/motion.ts +131 -131
  405. package/lib/notice.mjs +34 -34
  406. package/lib/scheduled.mjs +37 -37
  407. package/lib/snackbar.ts +149 -149
  408. package/lib/status.mjs +18 -18
  409. package/lib/storeEntries.d.mts +18 -0
  410. package/lib/storeEntries.mjs +64 -0
  411. package/lib/storeEntries.test.d.mts +1 -0
  412. package/lib/storeEntries.test.mjs +71 -0
  413. package/lib/svg.mjs +231 -231
  414. package/lib/text-colors.mjs +43 -43
  415. package/lib/video-id.mjs +72 -72
  416. package/lib/video.mjs +91 -91
  417. package/package.json +136 -131
  418. package/pages/404.astro +10 -10
  419. package/pages/[...slug].astro +51 -51
  420. package/pages/_headers.txt +31 -31
  421. package/pages/blog/index.astro +10 -10
  422. package/pages/blog/kategori/[category].astro +18 -18
  423. package/pages/editor/code-images.json.js +25 -25
  424. package/pages/editor/index.astro +75 -75
  425. package/pages/editor/manifest.webmanifest.js +23 -23
  426. package/pages/editor/sw.js +33 -33
  427. package/pages/editor/sw.js.js +4 -4
  428. package/pages/editor/thumbnails.json.js +21 -21
  429. package/pages/editor/version.json.js +22 -22
  430. package/pages/headers.js +4 -4
  431. package/pages/index.astro +10 -10
  432. package/pages/ny.astro +6 -6
  433. package/pages/robots.txt.js +3 -3
  434. package/pages/rss.xml.js +20 -20
  435. package/scripts/app-screenshots.mjs +164 -164
  436. package/scripts/app-workers.mjs +6 -6
  437. package/scripts/bin.mjs +12 -12
  438. package/scripts/built-site.mjs +134 -99
  439. package/scripts/check-consent.mjs +113 -113
  440. package/scripts/check-content.mjs +151 -149
  441. package/scripts/check-demo.mjs +24 -24
  442. package/scripts/check-store.mjs +160 -0
  443. package/scripts/check-themes.mjs +45 -45
  444. package/scripts/check-worker.mjs +569 -569
  445. package/scripts/compare-html.mjs +81 -81
  446. package/scripts/declarations.mjs +10 -0
  447. package/scripts/declarations.test.mjs +32 -0
  448. package/scripts/demo-site.mjs +42 -42
  449. package/scripts/editor-icons.mjs +56 -56
  450. package/scripts/editor-roundtrip.mjs +134 -134
  451. package/scripts/editor-screens.mjs +101 -101
  452. package/scripts/git-env.mjs +9 -9
  453. package/scripts/icon-packs.mjs +151 -151
  454. package/scripts/link-counts.mjs +22 -22
  455. package/scripts/page-colors.mjs +42 -42
  456. package/scripts/releases.mjs +80 -80
  457. package/scripts/responsive-images.mjs +94 -94
  458. package/scripts/scheduled-list.mjs +15 -15
  459. package/scripts/theme-screenshots.mjs +88 -88
  460. package/scripts/types.mjs +17 -21
  461. package/scripts/video-thumbnails.mjs +89 -89
  462. package/scripts/worker-modules.mjs +34 -34
  463. package/site/appCookies.mjs +74 -74
  464. package/site/apps.ts +21 -21
  465. package/site/autolinks.mjs +200 -200
  466. package/site/blockScripts.ts +31 -31
  467. package/site/categories.ts +16 -16
  468. package/site/consent.ts +26 -26
  469. package/site/content.config.ts +84 -84
  470. package/site/content.ts +104 -104
  471. package/site/cookieList.ts +17 -17
  472. package/site/defineSite.d.mts +2 -14
  473. package/site/defineSite.mjs +131 -136
  474. package/site/directives/interact.mjs +31 -31
  475. package/site/editor-version.mjs +26 -26
  476. package/site/fill.ts +13 -13
  477. package/site/format.ts +11 -11
  478. package/site/home.ts +66 -66
  479. package/site/iconPacks.mjs +43 -43
  480. package/site/icons.mjs +605 -605
  481. package/site/images.mjs +157 -157
  482. package/site/motion.ts +13 -13
  483. package/site/noindex.mjs +21 -21
  484. package/site/pageColors.ts +110 -110
  485. package/site/popups.ts +98 -98
  486. package/site/schema.ts +189 -189
  487. package/site/sections.ts +3 -3
  488. package/site/seo.ts +28 -28
  489. package/site/site.ts +76 -76
  490. package/site/siteConfig.mjs +8 -8
  491. package/site/siteCsp.mjs +75 -75
  492. package/site/siteModules.mjs +27 -27
  493. package/site/standardPages.ts +59 -59
  494. package/site/storeTheme.d.mts +6 -0
  495. package/site/storeTheme.mjs +29 -0
  496. package/site/storeTheme.test.d.mts +1 -0
  497. package/site/storeTheme.test.mjs +53 -0
  498. package/site/testimonials.ts +48 -48
  499. package/site/text.ts +43 -43
  500. package/site/theme.mjs +483 -472
  501. package/site/tracking.client.ts +225 -225
  502. package/site/tracking.ts +9 -9
  503. package/src/ActionParts.tsx +205 -205
  504. package/src/Advanced.tsx +152 -152
  505. package/src/App.tsx +901 -904
  506. package/src/AppList.tsx +1362 -1345
  507. package/src/BlockDialog.tsx +360 -360
  508. package/src/BlockParts.tsx +652 -652
  509. package/src/BlockSettings.tsx +533 -533
  510. package/src/BrandForm.tsx +164 -164
  511. package/src/Colors.tsx +476 -476
  512. package/src/CommunicationParts.tsx +199 -199
  513. package/src/CompanyForm.tsx +209 -209
  514. package/src/Consents.tsx +175 -175
  515. package/src/ContainmentParts.tsx +185 -185
  516. package/src/Crop.tsx +152 -152
  517. package/src/DataForm.tsx +434 -434
  518. package/src/Deleted.tsx +177 -177
  519. package/src/EntryForm.tsx +740 -740
  520. package/src/FileView.tsx +30 -30
  521. package/src/FormFrame.tsx +237 -237
  522. package/src/Keywords.tsx +225 -225
  523. package/src/Library.tsx +1563 -1563
  524. package/src/Licenses.tsx +336 -336
  525. package/src/LinkSheet.tsx +138 -138
  526. package/src/MarkdownEditor.tsx +1075 -1075
  527. package/src/Media.tsx +677 -677
  528. package/src/NavigationParts.tsx +350 -350
  529. package/src/NavigationRestParts.tsx +247 -247
  530. package/src/Notifications.tsx +59 -59
  531. package/src/PagePicker.tsx +136 -136
  532. package/src/Parts.tsx +991 -991
  533. package/src/PartsShown.tsx +168 -168
  534. package/src/PickerParts.tsx +183 -183
  535. package/src/Redirects.tsx +450 -450
  536. package/src/Screen.tsx +184 -184
  537. package/src/SectionsField.tsx +65 -65
  538. package/src/SelectionParts.tsx +134 -134
  539. package/src/SeoScores.tsx +184 -184
  540. package/src/Sheet.tsx +35 -35
  541. package/src/ThemeStore.tsx +376 -376
  542. package/src/Tokens.tsx +346 -346
  543. package/src/Versions.tsx +214 -214
  544. package/src/addresses.ts +75 -75
  545. package/src/allIcons.ts +7 -7
  546. package/src/appFiles.ts +29 -29
  547. package/src/apps.ts +281 -281
  548. package/src/ask.tsx +123 -123
  549. package/src/blocks.ts +251 -251
  550. package/src/brand.ts +153 -153
  551. package/src/cache.ts +42 -42
  552. package/src/check.ts +285 -285
  553. package/src/checkSite.ts +256 -256
  554. package/src/{consents.ts → consentData.ts} +26 -26
  555. package/src/cookieList.ts +70 -70
  556. package/src/countries.ts +59 -59
  557. package/src/deploy.ts +155 -155
  558. package/src/editor.css +602 -602
  559. package/src/examples.ts +85 -85
  560. package/src/fields.tsx +942 -942
  561. package/src/frontmatter.mjs +42 -42
  562. package/src/github.ts +376 -376
  563. package/src/i18n/da-DK/advanced.ts +28 -28
  564. package/src/i18n/da-DK/app.ts +71 -71
  565. package/src/i18n/da-DK/apps.ts +90 -90
  566. package/src/i18n/da-DK/blockSettings.ts +38 -38
  567. package/src/i18n/da-DK/blocks.ts +408 -408
  568. package/src/i18n/da-DK/check.ts +25 -25
  569. package/src/i18n/da-DK/colors.ts +64 -64
  570. package/src/i18n/da-DK/common.ts +118 -118
  571. package/src/i18n/da-DK/company.ts +24 -24
  572. package/src/i18n/da-DK/consents.ts +23 -23
  573. package/src/i18n/da-DK/forms.ts +116 -116
  574. package/src/i18n/da-DK/history.ts +35 -35
  575. package/src/i18n/da-DK/index.ts +49 -49
  576. package/src/i18n/da-DK/keywords.ts +19 -19
  577. package/src/i18n/da-DK/library.ts +135 -135
  578. package/src/i18n/da-DK/licenses.ts +25 -25
  579. package/src/i18n/da-DK/media.ts +79 -79
  580. package/src/i18n/da-DK/parts.ts +696 -696
  581. package/src/i18n/da-DK/preview.ts +13 -13
  582. package/src/i18n/da-DK/redirects.ts +51 -51
  583. package/src/i18n/da-DK/seo.ts +20 -20
  584. package/src/i18n/da-DK/text.ts +35 -35
  585. package/src/i18n/da-DK/theme.ts +41 -41
  586. package/src/i18n/en-US/advanced.ts +32 -32
  587. package/src/i18n/en-US/app.ts +84 -84
  588. package/src/i18n/en-US/apps.ts +134 -134
  589. package/src/i18n/en-US/blockSettings.ts +48 -48
  590. package/src/i18n/en-US/blocks.ts +441 -441
  591. package/src/i18n/en-US/check.ts +31 -31
  592. package/src/i18n/en-US/colors.ts +82 -82
  593. package/src/i18n/en-US/common.ts +144 -144
  594. package/src/i18n/en-US/company.ts +25 -25
  595. package/src/i18n/en-US/consents.ts +25 -25
  596. package/src/i18n/en-US/forms.ts +145 -145
  597. package/src/i18n/en-US/history.ts +43 -43
  598. package/src/i18n/en-US/index.ts +51 -51
  599. package/src/i18n/en-US/keywords.ts +24 -24
  600. package/src/i18n/en-US/library.ts +176 -176
  601. package/src/i18n/en-US/licenses.ts +36 -36
  602. package/src/i18n/en-US/media.ts +80 -80
  603. package/src/i18n/en-US/parts.ts +718 -718
  604. package/src/i18n/en-US/preview.ts +15 -15
  605. package/src/i18n/en-US/redirects.ts +66 -66
  606. package/src/i18n/en-US/seo.ts +26 -26
  607. package/src/i18n/en-US/text.ts +41 -41
  608. package/src/i18n/en-US/theme.ts +41 -41
  609. package/src/i18n/index.ts +124 -124
  610. package/src/icon.tsx +160 -160
  611. package/src/iconPacks.ts +89 -89
  612. package/src/images.ts +163 -163
  613. package/src/{licenses.ts → licenseData.ts} +79 -79
  614. package/src/markdown.mjs +204 -204
  615. package/src/nav.ts +68 -68
  616. package/src/notFound.ts +28 -28
  617. package/src/{notifications.ts → notificationData.ts} +223 -223
  618. package/src/pagePreview.tsx +204 -204
  619. package/src/r2Caching.ts +55 -55
  620. package/src/{redirects.ts → redirectData.ts} +149 -149
  621. package/src/registry.tsx +188 -188
  622. package/src/schema.ts +417 -417
  623. package/src/sections.mjs +303 -303
  624. package/src/seo.ts +333 -333
  625. package/src/shared.ts +44 -44
  626. package/src/sidebar.ts +31 -31
  627. package/src/site.ts +317 -317
  628. package/src/standard/Block.astro +19 -19
  629. package/src/standard/BlockSite.astro +18 -18
  630. package/src/standard/accordion/index.tsx +47 -47
  631. package/src/standard/accordion/load.ts +23 -23
  632. package/src/standard/accordion/shared.ts +15 -15
  633. package/src/standard/accordion/view.ts +32 -32
  634. package/src/standard/buttons/fields.ts +98 -98
  635. package/src/standard/buttons/index.tsx +23 -23
  636. package/src/standard/buttons/load.ts +34 -34
  637. package/src/standard/buttons/shared.ts +12 -12
  638. package/src/standard/buttons/view.ts +30 -30
  639. package/src/standard/cards/index.tsx +71 -71
  640. package/src/standard/cards/load.ts +17 -17
  641. package/src/standard/cards/shared.ts +27 -27
  642. package/src/standard/cards/view.ts +29 -29
  643. package/src/standard/checklist/index.tsx +27 -27
  644. package/src/standard/checklist/load.ts +5 -5
  645. package/src/standard/checklist/view.ts +13 -13
  646. package/src/standard/cookies/index.tsx +17 -17
  647. package/src/standard/cookies/list.ts +56 -56
  648. package/src/standard/cookies/load.ts +11 -11
  649. package/src/standard/cookies/shared.ts +20 -20
  650. package/src/standard/cookies/view.ts +28 -28
  651. package/src/standard/faq/index.tsx +29 -29
  652. package/src/standard/faq/load.ts +14 -14
  653. package/src/standard/faq/view.ts +21 -21
  654. package/src/standard/head.ts +23 -23
  655. package/src/standard/hero/fields.ts +48 -48
  656. package/src/standard/hero/index.tsx +57 -57
  657. package/src/standard/hero/load.ts +23 -23
  658. package/src/standard/hero/view.ts +38 -38
  659. package/src/standard/links/index.tsx +41 -41
  660. package/src/standard/links/load.ts +17 -17
  661. package/src/standard/links/view.ts +25 -25
  662. package/src/standard/list/index.tsx +50 -50
  663. package/src/standard/list/load.ts +5 -5
  664. package/src/standard/list/view.ts +18 -18
  665. package/src/standard/load.ts +7 -7
  666. package/src/standard/notice/index.tsx +30 -30
  667. package/src/standard/notice/load.ts +16 -16
  668. package/src/standard/notice/view.ts +10 -10
  669. package/src/standard/points/index.tsx +42 -42
  670. package/src/standard/points/load.ts +5 -5
  671. package/src/standard/points/view.ts +12 -12
  672. package/src/standard/posts/index.tsx +132 -132
  673. package/src/standard/posts/load.ts +36 -36
  674. package/src/standard/posts/shared.ts +21 -21
  675. package/src/standard/posts/view.ts +63 -63
  676. package/src/standard/pricecards/index.tsx +57 -57
  677. package/src/standard/pricecards/load.ts +11 -11
  678. package/src/standard/pricecards/view.ts +50 -50
  679. package/src/standard/sharedTexts.ts +6 -6
  680. package/src/standard/site.ts +124 -124
  681. package/src/standard/siteLooks.ts +19 -19
  682. package/src/standard/sites.ts +13 -13
  683. package/src/standard/steps/index.tsx +28 -28
  684. package/src/standard/steps/load.ts +5 -5
  685. package/src/standard/steps/view.ts +11 -11
  686. package/src/standard/testimonials/index.tsx +87 -87
  687. package/src/standard/testimonials/list.ts +43 -43
  688. package/src/standard/testimonials/load.ts +58 -58
  689. package/src/standard/testimonials/shared.ts +13 -13
  690. package/src/standard/testimonials/view.ts +76 -76
  691. package/src/standard/themeBlocks.ts +13 -13
  692. package/src/standard/video/index.tsx +23 -23
  693. package/src/standard/video/load.ts +10 -10
  694. package/src/standard/video/view.ts +42 -42
  695. package/src/standard/view.tsx +105 -105
  696. package/src/standard/views.ts +37 -37
  697. package/src/store.ts +479 -499
  698. package/src/theme.ts +90 -90
  699. package/src/themeUi.ts +97 -97
  700. package/src/themes.ts +91 -91
  701. package/src/ui.tsx +3402 -3402
  702. package/src/virtual.d.ts +5 -5
  703. package/src/when.ts +22 -22
  704. package/starter/.githooks/pre-push +4 -4
  705. package/starter/.nvmrc +1 -1
  706. package/starter/.prettierignore +11 -11
  707. package/starter/.prettierrc.json +7 -7
  708. package/starter/README.md +27 -26
  709. package/starter/astro.config.mjs +5 -5
  710. package/starter/package.json +67 -67
  711. package/starter/public/_redirects +1 -1
  712. package/starter/public/favicon.svg +1 -1
  713. package/starter/public/logo.svg +1 -1
  714. package/starter/src/content/pages/blog.md +8 -8
  715. package/starter/src/content/pages/forside.md +9 -9
  716. package/starter/src/content/pages/kontakt.md +7 -7
  717. package/starter/src/content/pages/privatlivspolitik.md +9 -9
  718. package/starter/src/content/pages/siden-findes-ikke.md +7 -7
  719. package/starter/src/content.config.ts +2 -2
  720. package/starter/src/data/apps.json +1 -1
  721. package/starter/src/data/block-settings.json +3 -3
  722. package/starter/src/data/blocks.json +1 -1
  723. package/starter/src/data/booking.json +6 -6
  724. package/starter/src/data/colors.json +70 -70
  725. package/starter/src/data/contact.json +8 -8
  726. package/starter/src/data/cookies.json +21 -21
  727. package/starter/src/data/csp.json +1 -1
  728. package/starter/src/data/footer.json +15 -15
  729. package/starter/src/data/internal-links.json +4 -4
  730. package/starter/src/data/menu.json +4 -4
  731. package/starter/src/data/mobile-menu.json +4 -4
  732. package/starter/src/data/old-addresses.json +1 -1
  733. package/starter/src/data/redirects.json +1 -1
  734. package/starter/src/data/standard-pages.json +8 -8
  735. package/starter/src/data/theme.json +1 -1
  736. package/starter/src/env.d.ts +2 -2
  737. package/starter/src/site/config.ts +13 -13
  738. package/starter/src/site/worker.js +14 -14
  739. package/starter/tsconfig.json +17 -17
  740. package/starter/worker/apps.js +6 -6
  741. package/starter/wrangler.jsonc +39 -39
  742. package/theme/LICENSE +21 -21
  743. package/theme/blocks/ArrowLink.tsx +14 -14
  744. package/theme/blocks/BlockHead.tsx +26 -26
  745. package/theme/blocks/Panel.tsx +32 -32
  746. package/theme/blocks/Person.tsx +26 -26
  747. package/theme/blocks/accordion/PageRow.tsx +25 -25
  748. package/theme/blocks/accordion/View.tsx +17 -17
  749. package/theme/blocks/buttons/Buttons.tsx +28 -28
  750. package/theme/blocks/buttons/View.tsx +12 -12
  751. package/theme/blocks/cards/LinkCards.tsx +72 -72
  752. package/theme/blocks/cards/View.tsx +20 -20
  753. package/theme/blocks/checklist/Checks.tsx +15 -15
  754. package/theme/blocks/checklist/View.tsx +14 -14
  755. package/theme/blocks/cookies/View.tsx +36 -36
  756. package/theme/blocks/faq/Accordion.tsx +34 -34
  757. package/theme/blocks/faq/View.tsx +16 -16
  758. package/theme/blocks/faq/script.ts +56 -56
  759. package/theme/blocks/hero/View.tsx +44 -44
  760. package/theme/blocks/links/LinkGroups.tsx +24 -24
  761. package/theme/blocks/links/View.tsx +17 -17
  762. package/theme/blocks/list/Rows.tsx +24 -24
  763. package/theme/blocks/list/View.tsx +14 -14
  764. package/theme/blocks/notice/View.tsx +13 -13
  765. package/theme/blocks/points/PointList.tsx +34 -34
  766. package/theme/blocks/points/View.tsx +14 -14
  767. package/theme/blocks/posts/View.tsx +26 -26
  768. package/theme/blocks/pricecards/View.tsx +40 -40
  769. package/theme/blocks/steps/StepList.tsx +23 -23
  770. package/theme/blocks/steps/View.tsx +14 -14
  771. package/theme/blocks/testimonials/List.tsx +46 -46
  772. package/theme/blocks/testimonials/Slider.tsx +44 -44
  773. package/theme/blocks/testimonials/Testimonial.tsx +124 -124
  774. package/theme/blocks/testimonials/View.tsx +21 -21
  775. package/theme/blocks/testimonials/script.ts +52 -52
  776. package/theme/blocks/video/View.tsx +41 -41
  777. package/theme/components/ActionBar.astro +63 -63
  778. package/theme/components/AppSlot.astro +21 -21
  779. package/theme/components/ArrowLink.tsx +21 -21
  780. package/theme/components/BlogIndex.astro +48 -48
  781. package/theme/components/BlogPosts.astro +18 -18
  782. package/theme/components/BlogPosts.tsx +45 -45
  783. package/theme/components/BookButton.tsx +22 -22
  784. package/theme/components/Container.astro +11 -11
  785. package/theme/components/CookieConsent.astro +121 -121
  786. package/theme/components/EndBlock.astro +21 -21
  787. package/theme/components/FontPreload.astro +6 -6
  788. package/theme/components/Footer.astro +85 -85
  789. package/theme/components/Header.astro +186 -186
  790. package/theme/components/Icon.astro +16 -16
  791. package/theme/components/Img.astro +29 -29
  792. package/theme/components/Img.tsx +33 -33
  793. package/theme/components/Inline.astro +11 -11
  794. package/theme/components/Logo.astro +23 -23
  795. package/theme/components/LogoMark.astro +10 -10
  796. package/theme/components/Markdown.astro +17 -17
  797. package/theme/components/MobileMenu.tsx +224 -224
  798. package/theme/components/MobileNav.astro +33 -33
  799. package/theme/components/PageHeader.astro +22 -22
  800. package/theme/components/PageSection.astro +24 -24
  801. package/theme/components/PhoneButton.tsx +11 -11
  802. package/theme/components/Popup.astro +145 -145
  803. package/theme/components/PopupButton.tsx +24 -24
  804. package/theme/components/PopupView.tsx +48 -48
  805. package/theme/components/PostList.tsx +58 -58
  806. package/theme/components/PostMeta.tsx +17 -17
  807. package/theme/components/RichText.astro +28 -28
  808. package/theme/components/Section.astro +29 -29
  809. package/theme/components/SectionHeading.tsx +8 -8
  810. package/theme/components/TextSection.astro +16 -16
  811. package/theme/components/home/PageBlock.astro +19 -19
  812. package/theme/components/home/Text.astro +13 -13
  813. package/theme/components/loadPopup.ts +22 -22
  814. package/theme/components/loadPosts.ts +16 -16
  815. package/theme/components/posts.ts +15 -15
  816. package/theme/editor/looks.mjs +7 -7
  817. package/theme/editor/previews.tsx +56 -56
  818. package/theme/layouts/Base.astro +273 -273
  819. package/theme/layouts/PageLayout.astro +126 -126
  820. package/theme/layouts/PostLayout.astro +137 -137
  821. package/theme/scripts/motion.ts +29 -29
  822. package/theme/styles/base.css +320 -320
  823. package/theme/styles/blocks.css +1808 -1808
  824. package/theme/styles/components.css +206 -206
  825. package/theme/styles/global.css +1302 -1302
  826. package/theme/styles/ui.css +6544 -6544
  827. package/theme/templates/Blog.astro +38 -38
  828. package/theme/templates/Category.astro +19 -19
  829. package/theme/templates/Home.astro +36 -36
  830. package/theme/templates/New.astro +68 -68
  831. package/theme/templates/NotFound.astro +117 -117
  832. package/theme/theme.json +28 -28
  833. package/theme/ui/Badge.tsx +45 -45
  834. package/theme/ui/BottomSheet.tsx +148 -148
  835. package/theme/ui/Button.tsx +79 -79
  836. package/theme/ui/ButtonGroup.tsx +31 -31
  837. package/theme/ui/Card.tsx +45 -45
  838. package/theme/ui/Carousel.tsx +124 -124
  839. package/theme/ui/Checkbox.tsx +59 -59
  840. package/theme/ui/Chip.tsx +101 -101
  841. package/theme/ui/ChipSet.tsx +27 -27
  842. package/theme/ui/DatePicker.tsx +149 -149
  843. package/theme/ui/DatePickerContainer.tsx +316 -316
  844. package/theme/ui/Dialog.tsx +100 -100
  845. package/theme/ui/Divider.tsx +27 -27
  846. package/theme/ui/ExposedDropdownMenu.tsx +77 -77
  847. package/theme/ui/Fab.tsx +88 -88
  848. package/theme/ui/FabMenu.tsx +67 -67
  849. package/theme/ui/IconButton.tsx +78 -78
  850. package/theme/ui/List.tsx +287 -287
  851. package/theme/ui/LoadingIndicator.tsx +29 -29
  852. package/theme/ui/Menu.tsx +278 -278
  853. package/theme/ui/NavigationBar.tsx +62 -62
  854. package/theme/ui/NavigationDrawer.tsx +80 -80
  855. package/theme/ui/NavigationRail.tsx +154 -154
  856. package/theme/ui/ProgressIndicator.tsx +162 -162
  857. package/theme/ui/RadioButton.tsx +50 -50
  858. package/theme/ui/Search.tsx +129 -129
  859. package/theme/ui/SegmentedButton.tsx +49 -49
  860. package/theme/ui/SideSheet.tsx +54 -54
  861. package/theme/ui/Slider.tsx +155 -155
  862. package/theme/ui/Snackbar.tsx +67 -67
  863. package/theme/ui/SplitButton.tsx +67 -67
  864. package/theme/ui/Switch.tsx +60 -60
  865. package/theme/ui/Tabs.tsx +114 -114
  866. package/theme/ui/TextField.tsx +278 -278
  867. package/theme/ui/TimePicker.tsx +128 -128
  868. package/theme/ui/TimePickerContainer.tsx +234 -234
  869. package/theme/ui/Toolbar.tsx +67 -67
  870. package/theme/ui/Tooltip.tsx +58 -58
  871. package/theme/ui/TopAppBar.tsx +77 -77
  872. package/theme/ui/bottomSheet.script.ts +8 -8
  873. package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
  874. package/theme/ui/carousel.script.ts +28 -28
  875. package/theme/ui/carouselLayout.ts +229 -229
  876. package/theme/ui/checkbox.script.ts +8 -8
  877. package/theme/ui/datePicker.script.ts +302 -302
  878. package/theme/ui/dates.ts +185 -185
  879. package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
  880. package/theme/ui/dropdownMenu.script.ts +166 -166
  881. package/theme/ui/fabMenu.script.ts +57 -57
  882. package/theme/ui/navigationRail.script.ts +47 -47
  883. package/theme/ui/ripple.script.ts +159 -159
  884. package/theme/ui/search.script.ts +92 -92
  885. package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
  886. package/theme/ui/slider.script.ts +12 -12
  887. package/theme/ui/sliderUpdate.ts +52 -52
  888. package/theme/ui/snackbar.script.ts +16 -16
  889. package/theme/ui/splitButton.script.ts +63 -63
  890. package/theme/ui/tabs.script.ts +48 -48
  891. package/theme/ui/textField.script.ts +17 -17
  892. package/theme/ui/textFieldUpdate.ts +44 -44
  893. package/theme/ui/timePicker.script.ts +248 -248
  894. package/theme/ui/toolbar.script.ts +43 -43
  895. package/theme/ui/tooltip.script.ts +68 -68
  896. package/worker/consent.js +141 -141
  897. package/worker/editor-github.js +167 -167
  898. package/worker/editor-host.js +74 -74
  899. package/worker/files.js +401 -401
  900. package/worker/github.js +36 -36
  901. package/worker/index.js +188 -184
  902. package/worker/not-found.js +97 -97
  903. package/worker/oauth.js +95 -95
  904. package/worker/publish.js +52 -52
  905. package/worker/push.js +270 -270
  906. package/worker/r2-status.js +50 -50
  907. package/worker/session.js +58 -58
  908. package/worker/store.js +46 -0
package/src/store.ts CHANGED
@@ -1,499 +1,479 @@
1
- // The app store ("Indstillinger" › "Appstore", #indstillinger/appstore, AppList.tsx): the apps that are not in the site
2
- // and can be installed. The store is a GitHub repository (config.store) with a catalog (catalog.json): each app with
3
- // its id, its version and the version of the contract it is written to (APP_API), its name, who made it, its icon
4
- // (Phosphor's name), what it does (in a sentence, and more fully), its category and the words it is found by, what it
5
- // adds (its blocks, the lists it brings and its kinds of the site's lists), what it needs (needsOf), the cookies it
6
- // sets, its pictures, what is new in each version (its release notes), its texts in the editor's other languages (as an app's texts.ts), its
7
- // licence and those of the third-party work it brings (StoreLicenses), and where its folder is in the
8
- // repository at which commit, file by file with their blob ids.
9
- // Installing one copies its folder into the site's (config.apps.folder/<id>/, at that commit, each file checked
10
- // against its id), with a note of where it came from (store.json, StoreRecord), and puts it among the apps installed,
11
- // in one commit (appChanges); the site's next build has it. Updating copies the newer version over it, and
12
- // uninstalling deletes the folder, also in one commit each. What was made with it (its settings, its entries) is the
13
- // site's data and stays.
14
- // The store has themes too (`themes` in the catalog): each with its id, its version and the version of the contract it
15
- // is written to (THEME_API), its name, who made it, its picture, what it is and gives the site, its pictures of a site
16
- // in it, and its folder file by file. Installing one copies its folder into the site's themes (config.themes/<id>/,
17
- // with its store.json), and it can then be chosen under "Udseende" › "Temaer"; updating and uninstalling are as for
18
- // an app (themeChanges), and the theme in use cannot be uninstalled.
19
- // The catalog is what is released: an app's or a theme's entry is written from its folder by `npm run store:release
20
- // -- <id>` (scripts/store-release.mjs) when the owner releases it, not by each merge (packages/editor/scripts/releases.mjs). Until the real store exists, it is this repository's own (store/catalog.json, with the apps' folders in
21
- // src/apps/ and the themes' in src/themes/).
22
- import { config } from '@site/config';
23
- import { loadBytes, loadFrom, type Change, type RepoFile } from './github';
24
- import { locale, t } from './i18n';
25
- import { hooksOf, type AppHook } from '../lib/hooks';
26
- import type { StoreLicenses } from './licenses';
27
-
28
- /** The version of the contract between the editor and an app (apps.ts: defineApp, its folder, and the theme's parts it
29
- * draws with: the standard blocks' and the UI components, @theme/blocks/… and @theme/ui/…, imported from
30
- * @morphemecms/cms): an app in the store says which it is written to, and only those the editor can run are
31
- * offered. Raised when an app written to the one before no longer works. */
32
- export const APP_API = 1;
33
-
34
- /** The version of the contract between the editor and a theme (what a theme must and may have, "Hvad et tema må" in
35
- * CLAUDE.md, themeProblems in packages/editor/site/theme.mjs, the standard theme's UI parts with Material 3's names and
36
- * variants, the motion scheme's --motion-* springs and motionSpring/motionTiming (@morphemecms/cms/lib/motion),
37
- * and the classes it styles): only themes written to it are offered. Raised when a theme written to the one before no
38
- * longer works. */
39
- export const THEME_API = 2;
40
-
41
- /** The kinds of apps, a filter in the store (t.apps.categories). */
42
- export const appCategories = ['booking', 'newsletter', 'payment', 'analytics', 'content', 'design'] as const;
43
- export type AppCategory = (typeof appCategories)[number];
44
-
45
- /** A file of an app in the store: its path in the app's folder and its git blob id. */
46
- export type StoreFile = { path: string; oid: string };
47
-
48
- /** Something an app adds, as the catalog says it (its page before it is installed): its name, icon and what it is. */
49
- export type StoreAdd = { label: string; icon: string; hint?: string };
50
-
51
- /** What an app needs of the site, as its folder says (needsOf): its code on every page (slots/), what it loads from
52
- * other addresses and where its forms send to (its csp.json, by host), its own part of the site's Worker (worker.js),
53
- * a step when the site is built (build.mjs) and what it changes of what the site does itself (hooks/,
54
- * @morphemecms/cms/lib/hooks.ts; not in a catalog written before there were any). Shown on its page under "Rettigheder",
55
- * before it is installed. */
56
- export type AppNeeds = { pages: boolean; loads: string[]; sends: string[]; server: boolean; build: boolean; changes?: AppHook[] };
57
-
58
- /** What an app needs, from the paths of the files in its folder and its csp.json. */
59
- export function needsOf(files: string[], csp: Partial<Record<string, string[]>> = {}): AppNeeds {
60
- const hosts = (kinds: string[]) => [...new Set(kinds.flatMap((k) => csp[k] ?? []).map((a) => a.replace(/^https:\/\//, '')))].sort();
61
- return {
62
- pages: files.some((f) => f.startsWith('slots/')),
63
- loads: hosts(['script', 'style', 'img', 'connect', 'frame']),
64
- sends: hosts(['form']),
65
- server: files.includes('worker.js'),
66
- build: files.includes('build.mjs'),
67
- changes: hooksOf(files).hooks,
68
- };
69
- }
70
-
71
- /** A cookie an app sets (also one a service sets in its own frame, as Calendly's), from its cookies.json, on its page
72
- * under "Cookies": its name (`_ga`, `_ga_*`; none for those a service decides itself, only on its page), what it is
73
- * for and how long it lasts; with a name, also who sets it and its category (lib/cookieCategories.mjs: `necessary`,
74
- * also without a yes in the cookie banner, or one the visitor says yes to; `when` in an app from before them, categoryOf),
75
- * for the cookie list in the cookie policy (the site's packages/editor/site/appCookies.mjs). */
76
- export type AppCookie = { name?: string; provider?: string; purpose: string; lasts?: string; category?: string; when?: string };
77
-
78
- /** A picture of an app in its store page, picked to give an idea of it (`screenshots` in its index.ts, as a theme's):
79
- * what it shows (`shows`: one of its blocks, `blok:<type>`; a variant of its kind of a list, `slags:<list>:<n>`, 0
80
- * without variants; one of its lists, `liste:<id>`; its settings, `indstillinger`, with made-up `values`, never the
81
- * site's own; its own page, `side`) and what it says under it (`label`, an app's text). Only made-up content, never the
82
- * site's (packages/editor/scripts/app-screenshots.mjs takes them with the examples). */
83
- export type AppShot = { shows: string; label: string; values?: Record<string, unknown> };
84
-
85
- /** A picture of an app as the catalog has it: its file in the app's screenshots/ and what it says under it. */
86
- export type AppScreenshot = { file: string; label: string };
87
-
88
- /** What an app adds, as a picture of it can show it (shotOf). */
89
- type Shown = {
90
- kinds?: Record<string, { variants?: unknown[] }>;
91
- lists?: { id: string }[];
92
- settings?: unknown;
93
- page?: unknown;
94
- };
95
-
96
- /** Whether a picture shows something the app has (AppShot.shows): one of its blocks (`blocks`: their types), a variant
97
- * of its kind of a list, one of its lists, its settings or its own page. */
98
- export function shotOf(shows: string, app: Shown, blocks: string[]): boolean {
99
- const [what, key = '', n] = shows.split(':');
100
- if (what === 'blok') return blocks.includes(key);
101
- if (what === 'slags') {
102
- const kind = app.kinds?.[key];
103
- return !!kind && /^\d+$/.test(n ?? '') && Number(n) < Math.max(1, kind.variants?.length ?? 0);
104
- }
105
- if (what === 'liste') return !!app.lists?.some((l) => l.id === key);
106
- if (what === 'indstillinger') return !!app.settings && !key;
107
- return what === 'side' && !!app.page && !key;
108
- }
109
-
110
- /** The file of an app's picture in its screenshots/, by its place among them and what it shows. */
111
- export const shotFile = (i: number, shows: string) => `${i + 1}-${shows.replace(/:/g, '-')}.jpg`;
112
-
113
- /** The folder of an app's pictures. */
114
- export const SCREENSHOTS = 'screenshots';
115
-
116
- /** An app in the store's catalog. */
117
- export type StoreApp = {
118
- id: string;
119
- version: string;
120
- api: number;
121
- label: string;
122
- publisher: string;
123
- icon: string;
124
- hint: string;
125
- description?: string;
126
- /** What is new in each version, by the version (AppDefinition.releaseNotes): before it is updated (notesSince). */
127
- releaseNotes?: Record<string, string>;
128
- category?: AppCategory;
129
- keywords?: string[];
130
- /** What it needs of the site; the cookies it sets, and whether it asks in the cookie banner first (AppDefinition
131
- * .cookies); its pictures. */
132
- needs?: AppNeeds;
133
- /** The names of the kinds of notifications it sends (AppDefinition.notifications). */
134
- notifies?: string[];
135
- cookies?: AppCookie[];
136
- consent?: boolean;
137
- screenshots?: AppScreenshot[];
138
- /** Its own licence and the third-party work it brings, each with its licence (its `license` and `thirdParty`; not in
139
- * a catalog written before they were). */
140
- licenses?: StoreLicenses;
141
- /** Its blocks, the lists it brings and its kinds of the site's lists (by the list's id). */
142
- adds: { blocks: StoreAdd[]; lists: StoreAdd[]; kinds: (StoreAdd & { list: string })[] };
143
- /** Its texts in the editor's other languages, by the text as it is written. */
144
- texts?: Partial<Record<string, Record<string, string>>>;
145
- /** Where its folder is in the store's repository, and at which commit. */
146
- path: string;
147
- ref: string;
148
- files: StoreFile[];
149
- };
150
-
151
- /** A theme in the store's catalog: as its theme.json has it (its texts by the editor's language), its pictures of a
152
- * site in it (screenshots/<n>-<key>.jpg, t.library.themeShots) and where its folder is in the store's repository. */
153
- export type StoreTheme = {
154
- id: string;
155
- version: string;
156
- api: number;
157
- name: string;
158
- author: string;
159
- icon: string;
160
- description?: Record<string, string>;
161
- highlights?: Record<string, string[]>;
162
- /** What is new in each version, by the language and then the version, as its theme.json has it. */
163
- releaseNotes?: Record<string, Record<string, string>>;
164
- screenshots?: { file: string; key: string }[];
165
- /** Its own licence and the third-party work it brings, as its theme.json declares them (not in a catalog written
166
- * before they were). */
167
- licenses?: StoreLicenses;
168
- /** The icon packs it brings (its icon-packs/<id>/, with their pack.json: name, who made it, licence), installed with it
169
- * and chosen under "Udseende" › "Ikonpakker". */
170
- iconPacks?: { id: string; name: string; author: string; license: string }[];
171
- path: string;
172
- ref: string;
173
- files: StoreFile[];
174
- };
175
-
176
- /** A theme of the catalog as the editor shows it: its texts in the editor's language, and its words for the search. */
177
- export type ShownTheme = Omit<StoreTheme, 'description' | 'highlights' | 'releaseNotes'> & {
178
- description?: string;
179
- highlights: string[];
180
- releaseNotes?: Record<string, string>;
181
- words: string;
182
- };
183
-
184
- /** An app of the catalog as the editor shows it: its texts in the editor's language, its name as the catalog writes
185
- * it (for the commits), and all its words in both, for the search. */
186
- export type ShownApp = StoreApp & { written: string; words: string };
187
-
188
- /** Where an app installed from the store came from: its version, the store's commit and its files (store.json). */
189
- export type StoreRecord = { version: string; ref: string; files: StoreFile[] };
190
-
191
- /** The store's address in the editor, under "Indstillinger" (its apps; its themes at STORE_THEMES), its address
192
- * before (STORE_BEFORE, which opens it, App.tsx MOVED), and that of a developer's page (DEVELOPER_ROUTE/<slug>); no
193
- * app can have the ids of the last two. */
194
- export const STORE_ROUTE = 'indstillinger/appstore';
195
- export const STORE_THEMES = 'indstillinger/appstore/temaer';
196
- export const STORE_BEFORE = 'apps/butik';
197
- export const DEVELOPER_ROUTE = 'apps/udvikler';
198
-
199
- /** An app's folder in the site, and the note in it of where it came from. */
200
- export const appFolder = (id: string) => `${config.apps.folder}/${id}`;
201
- /** A theme's folder in the site; the standard theme's is always there. */
202
- export const themeFolder = (id: string) => `${config.themes}/${id}`;
203
- export const STANDARD_THEME = 'saturn';
204
- export const RECORD = 'store.json';
205
-
206
- /** Whether a version is newer than another ("1.10.0" than "1.9.2"). */
207
- export function newer(a: string, b: string): boolean {
208
- const parts = (v: string) => v.split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
209
- const [x, y] = [parts(a), parts(b)];
210
- for (let i = 0; i < Math.max(x.length, y.length); i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
211
- return false;
212
- }
213
-
214
- /** Whether a version has a higher first number than another ("2.0.0" than "1.9.2"): it has changes that can break
215
- * something, so it is not updated to on its own (autoUpdate in AppList.tsx). */
216
- export const breaking = (a: string, b: string) => (Number.parseInt(a, 10) || 0) > (Number.parseInt(b, 10) || 0);
217
-
218
- /** What an update brings: the release notes of the versions after the one installed (`from`) up to the new one (`to`),
219
- * the newest first. */
220
- export function notesSince(notes: Record<string, string> | undefined, from: string, to: string): { version: string; text: string }[] {
221
- return Object.entries(notes ?? {})
222
- .filter(([v, text]) => isText(text) && newer(v, from) && !newer(v, to))
223
- .sort(([a], [b]) => (newer(a, b) ? -1 : 1))
224
- .map(([version, text]) => ({ version, text }));
225
- }
226
-
227
- // Release notes as the catalog has them: only texts, by their version.
228
- const notesOf = (x: unknown) =>
229
- x && typeof x === 'object' && !Array.isArray(x)
230
- ? Object.fromEntries(Object.entries(x).filter((e): e is [string, string] => isText(e[1])))
231
- : undefined;
232
-
233
- const isText = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';
234
- const isAdd = (v: any) => isText(v?.label) && isText(v?.icon);
235
- const ID = /^[a-z0-9][a-z0-9-]*$/;
236
- // A file's path in an app's folder: only within it.
237
- const isPath = (v: unknown) => isText(v) && v.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
238
-
239
- /** Whether an entry of the catalog has what an app needs, written to the contract the editor runs, with files only in
240
- * its own folder, and not the store's own address. */
241
- function valid(a: any): a is StoreApp {
242
- return (
243
- ID.test(a?.id ?? '') &&
244
- ![STORE_BEFORE, DEVELOPER_ROUTE].includes(`apps/${a.id}`) &&
245
- isText(a.version) &&
246
- a.api === APP_API &&
247
- [a.label, a.publisher, a.icon, a.hint, a.path, a.ref].every(isText) &&
248
- Array.isArray(a.files) &&
249
- a.files.length > 0 &&
250
- a.files.every((f: any) => isText(f?.oid) && isPath(f?.path) && f.path !== RECORD) &&
251
- ['blocks', 'lists', 'kinds'].every((k) => Array.isArray(a.adds?.[k]) && a.adds[k].every(isAdd))
252
- );
253
- }
254
-
255
- /** The apps in a catalog (catalog.json, as text) the editor can install, with their texts in its language. */
256
- export function catalogApps(text: string): ShownApp[] {
257
- let json: any;
258
- try {
259
- json = JSON.parse(text);
260
- } catch {
261
- return [];
262
- }
263
- const apps: unknown[] = Array.isArray(json?.apps) ? json.apps : [];
264
- return apps.filter(valid).map((a) => {
265
- const own = a.texts?.[locale];
266
- const say = (s: string) => own?.[s] ?? s;
267
- const add = <A extends StoreAdd>(x: A): A => ({ ...x, label: say(x.label), ...(x.hint ? { hint: say(x.hint) } : {}) });
268
- const keywords = a.keywords ?? [];
269
- return {
270
- ...a,
271
- label: say(a.label),
272
- hint: say(a.hint),
273
- ...(a.description ? { description: say(a.description) } : {}),
274
- releaseNotes: Object.fromEntries(Object.entries(notesOf(a.releaseNotes) ?? {}).map(([v, x]) => [v, say(x)])),
275
- keywords: keywords.map(say),
276
- notifies: (Array.isArray(a.notifies) ? a.notifies : []).filter(isText).map(say),
277
- cookies: (Array.isArray(a.cookies) ? a.cookies : [])
278
- .filter((c) => isText(c?.purpose))
279
- .map((c) => ({
280
- ...c,
281
- purpose: say(c.purpose),
282
- ...(c.lasts ? { lasts: say(c.lasts) } : {}),
283
- })),
284
- screenshots: (Array.isArray(a.screenshots) ? a.screenshots : [])
285
- .filter((x) => isText(x?.file) && isText(x?.label) && a.files.some((f) => f.path === `${SCREENSHOTS}/${x.file}`))
286
- .map((x) => ({ ...x, label: say(x.label) })),
287
- adds: { blocks: a.adds.blocks.map(add), lists: a.adds.lists.map(add), kinds: a.adds.kinds.map(add) },
288
- written: a.label,
289
- words: [a.label, a.hint, a.publisher, ...keywords, ...[a.label, a.hint, ...keywords].map(say)].join(' ').toLowerCase(),
290
- };
291
- });
292
- }
293
-
294
- /** Whether an entry of the catalog has what a theme needs, written to the contract the editor runs, with files only
295
- * in its own folder; never the standard theme, which every site has. */
296
- function validTheme(x: any): x is StoreTheme {
297
- return (
298
- ID.test(x?.id ?? '') &&
299
- x.id !== STANDARD_THEME &&
300
- x.api === THEME_API &&
301
- [x.version, x.name, x.author, x.icon, x.path, x.ref].every(isText) &&
302
- Array.isArray(x.files) &&
303
- x.files.length > 0 &&
304
- x.files.every((f: any) => isText(f?.oid) && isPath(f?.path) && f.path !== RECORD)
305
- );
306
- }
307
-
308
- /** The themes in a catalog (catalog.json, as text) the editor can install, with their texts in its language (else the
309
- * first they have). */
310
- export function catalogThemes(text: string): ShownTheme[] {
311
- let json: any;
312
- try {
313
- json = JSON.parse(text);
314
- } catch {
315
- return [];
316
- }
317
- const themes: unknown[] = Array.isArray(json?.themes) ? json.themes : [];
318
- const inLocale = <T>(x?: Record<string, T>) => x && (x[locale] ?? Object.values(x)[0]);
319
- return themes.filter(validTheme).map((x) => {
320
- const description = inLocale(x.description);
321
- const highlights = inLocale(x.highlights) ?? [];
322
- return {
323
- ...x,
324
- description: typeof description === 'string' ? description : undefined,
325
- highlights: Array.isArray(highlights) ? highlights.filter(isText) : [],
326
- releaseNotes: notesOf(inLocale(x.releaseNotes)),
327
- screenshots: (Array.isArray(x.screenshots) ? x.screenshots : []).filter(
328
- (s) => isText(s?.file) && isText(s?.key) && x.files.some((f) => f.path === `${SCREENSHOTS}/${s.file}`),
329
- ),
330
- iconPacks: (Array.isArray(x.iconPacks) ? x.iconPacks : []).filter(
331
- (p) => [p?.id, p?.name, p?.author, p?.license].every(isText) && x.files.some((f) => f.path === `icon-packs/${p.id}/pack.json`),
332
- ),
333
- words: [x.name, x.author, ...Object.values(x.description ?? {}), ...Object.values(x.highlights ?? {}).flat()].join(' ').toLowerCase(),
334
- };
335
- });
336
- }
337
-
338
- // The catalog, read once while the editor is open (again after it could not be read).
339
- let catalog: Promise<string> | null = null;
340
-
341
- /** Whether the site has an app store (config.store); without one, "Appstore" has only the site's own apps and themes. */
342
- export const hasStore = !!config.store;
343
-
344
- // The store's repository; only asked for what is in its catalog, so never without one.
345
- const storeRepo = () => {
346
- if (!config.store) throw new Error(t.apps.noStore);
347
- return config.store;
348
- };
349
-
350
- function catalogText(): Promise<string> {
351
- if (!config.store) return Promise.resolve('{}');
352
- catalog ??= loadFrom(config.store, config.store.branch, [config.store.catalog]).then(([file]) => {
353
- if (file?.text == null) throw new Error(t.apps.storeFailed);
354
- return file.text;
355
- });
356
- catalog.catch(() => (catalog = null));
357
- return catalog;
358
- }
359
-
360
- /** The store's apps. Fails when the catalog cannot be read. */
361
- export const readCatalog = (): Promise<ShownApp[]> => catalogText().then(catalogApps);
362
-
363
- /** The store's themes. Fails when the catalog cannot be read. */
364
- export const readThemes = (): Promise<ShownTheme[]> => catalogText().then(catalogThemes);
365
-
366
- /** An app's (or a theme's) files from the store (its folder at the catalog's commit), each checked against its blob id. */
367
- export async function readApp(app: {
368
- path: string;
369
- ref: string;
370
- files: StoreFile[];
371
- label?: string;
372
- name?: string;
373
- }): Promise<{ path: string; content: string | Uint8Array }[]> {
374
- const found = await loadFrom(
375
- storeRepo(),
376
- app.ref,
377
- app.files.map((f) => `${app.path}/${f.path}`),
378
- );
379
- return Promise.all(
380
- app.files.map(async (f) => {
381
- const file = found.find((x) => x.path === `${app.path}/${f.path}`);
382
- if (!file || file.oid !== f.oid) throw new Error(t.apps.storeBroken(app.label ?? app.name ?? app.path));
383
- return { path: f.path, content: file.text ?? (await loadBytes(f.oid, undefined, storeRepo())) };
384
- }),
385
- );
386
- }
387
-
388
- /** An app's pictures from the store (its screenshots/, AppScreenshot), each as an address in this browser (a blob:)
389
- * with what it shows; one that cannot be read is left out. */
390
- export async function readScreenshots(app: StoreApp): Promise<{ src: string; label: string }[]> {
391
- const types: Record<string, string> = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp' };
392
- const read = await Promise.all(
393
- (app.screenshots ?? []).map(async (x) => {
394
- const oid = app.files.find((f) => f.path === `${SCREENSHOTS}/${x.file}`)?.oid;
395
- const type = types[x.file.split('.').pop()!.toLowerCase()];
396
- if (!oid || !type) return null;
397
- try {
398
- const bytes = await loadBytes(oid, undefined, storeRepo());
399
- return { src: URL.createObjectURL(new Blob([bytes as BlobPart], { type })), label: x.label };
400
- } catch {
401
- return null;
402
- }
403
- }),
404
- );
405
- return read.filter((x) => !!x);
406
- }
407
-
408
- /** A theme's pictures of a site in it from the store (its screenshots/), each as an address in this browser (a blob:)
409
- * with what it shows (its key, t.library.themeShots); one that cannot be read is left out. */
410
- export async function readThemeScreenshots(theme: Pick<StoreTheme, 'screenshots' | 'files'>): Promise<{ key: string; src: string }[]> {
411
- const read = await Promise.all(
412
- (theme.screenshots ?? []).map(async (x) => {
413
- const oid = theme.files.find((f) => f.path === `${SCREENSHOTS}/${x.file}`)?.oid;
414
- if (!oid) return null;
415
- try {
416
- const bytes = await loadBytes(oid, undefined, storeRepo());
417
- return { key: x.key, src: URL.createObjectURL(new Blob([bytes as BlobPart], { type: 'image/jpeg' })) };
418
- } catch {
419
- return null;
420
- }
421
- }),
422
- );
423
- return read.filter((x) => !!x);
424
- }
425
-
426
- /** The changes that make a theme's folder in the site (`current`, its files now) the store's version, with the note of
427
- * where it came from, or remove it (`next` null: uninstalling). In one commit. */
428
- export function themeChanges(
429
- id: string,
430
- current: RepoFile[],
431
- next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
432
- ): Change[] {
433
- return folderChanges(themeFolder(id), current, next);
434
- }
435
-
436
- /** The changes that make a folder in the site (`current`, its files now) the store's version of an app or a theme, with
437
- * the note of where it came from, or remove it (`next` null). */
438
- function folderChanges(
439
- folder: string,
440
- current: RepoFile[],
441
- next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
442
- ): Change[] {
443
- const now = new Map(current.map((f) => [f.path, f.oid]));
444
- const wanted: { path: string; content: string | Uint8Array; oid?: string }[] = next
445
- ? [
446
- ...next.files.map((f) => ({
447
- path: `${folder}/${f.path}`,
448
- content: f.content,
449
- oid: next.record.files.find((r) => r.path === f.path)?.oid,
450
- })),
451
- { path: `${folder}/${RECORD}`, content: `${JSON.stringify(next.record, null, 2)}\n` },
452
- ]
453
- : [];
454
- return [
455
- // A file the site has as it is in the store is left as it is.
456
- ...wanted
457
- .filter((w) => !w.oid || now.get(w.path) !== w.oid)
458
- .map((w) => ({ path: w.path, content: w.content, oid: now.get(w.path) ?? null })),
459
- ...current.filter((f) => !wanted.some((w) => w.path === f.path)).map((f) => ({ path: f.path, content: null, oid: f.oid })),
460
- ];
461
- }
462
-
463
- /** The changes that make an app's folder in the site (`current`, its files now) the store's version, with the note of
464
- * where it came from, or remove it (`next` null: uninstalling); and the site's file of apps with it among those
465
- * installed or not (`installed`: their ids now, `file`: the file). In one commit. */
466
- export function appChanges(
467
- id: string,
468
- current: RepoFile[],
469
- next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
470
- installed: string[],
471
- file: RepoFile | undefined,
472
- ): Change[] {
473
- const changes = folderChanges(appFolder(id), current, next);
474
- const ids = [...new Set(installed.filter((x) => x !== id).concat(next ? [id] : []))].sort();
475
- const text = `${JSON.stringify({ installed: ids }, null, 2)}\n`;
476
- if (text !== file?.text) changes.push({ path: config.apps.installed, content: text, oid: file?.oid || null });
477
- return changes;
478
- }
479
-
480
- /** The note in an app's (or a theme's) folder of where it came from (store.json), from its files now (`current`). */
481
- export function recordIn(current: RepoFile[], root: string): StoreRecord | undefined {
482
- try {
483
- const record = JSON.parse(current.find((f) => f.path === `${root}/${RECORD}`)?.text ?? '');
484
- return isText(record?.version) && Array.isArray(record.files) ? record : undefined;
485
- } catch {
486
- return undefined;
487
- }
488
- }
489
-
490
- /** The files of an app installed from the store that were changed in the site since (in its folder, `current`, not
491
- * as its note says), by their paths in the folder: an update writes over them. */
492
- export function changedFiles(id: string, record: StoreRecord, current: RepoFile[], root = appFolder(id)): string[] {
493
- const folder = `${root}/`;
494
- const own = current.map((f) => ({ path: f.path.slice(folder.length), oid: f.oid }));
495
- return [
496
- ...own.filter((f) => f.path !== RECORD && record.files.find((r) => r.path === f.path)?.oid !== f.oid).map((f) => f.path),
497
- ...record.files.filter((r) => !own.some((f) => f.path === r.path)).map((r) => r.path),
498
- ];
499
- }
1
+ // The app store ("Indstillinger" › "Appstore", #indstillinger/appstore, AppList.tsx): the apps that are not in the site
2
+ // and can be installed. The store is reached through the site's Worker (/api/store/catalog, /api/store/files/<oid>;
3
+ // the Worker holds the site's key to it, and the editor never calls the store itself): each app of its catalog with
4
+ // its id, its version and the version of the contract it is written to (APP_API), its name, who made it, its icon
5
+ // (Phosphor's name), what it does (in a sentence, and more fully), its category and the words it is found by, what it
6
+ // adds (its blocks, the lists it brings and its kinds of the site's lists), what it needs (needsOf), the cookies it
7
+ // sets, its pictures, what is new in each version (its release notes), its texts in the editor's other languages (as an app's texts.ts), its
8
+ // licence and those of the third-party work it brings (StoreLicenses), and its folder file by file with their blob ids.
9
+ // Installing one copies its folder into the site's (config.apps.folder/<id>/, each file checked against its id), with a
10
+ // note of where it came from (store.json, StoreRecord; its `ref` is the version), and puts it among the apps installed,
11
+ // in one commit (appChanges); the site's next build has it. Updating copies the newer version over it, and
12
+ // uninstalling deletes the folder, also in one commit each. What was made with it (its settings, its entries) is the
13
+ // site's data and stays.
14
+ // The store has themes too (`themes` in the catalog): each with its id, its version and the version of the contract it
15
+ // is written to (THEME_API), its name, who made it, its picture, what it is and gives the site, its pictures of a site
16
+ // in it, and its folder file by file. Installing one copies its folder into the site's themes (config.themes/<id>/,
17
+ // with its store.json), and it can then be chosen under "Udseende" › "Temaer"; updating and uninstalling are as for
18
+ // an app (themeChanges), and the theme in use cannot be uninstalled.
19
+ // The catalog is what is released: an app's or a theme's entry is written from its folder by `npm run store:release
20
+ // -- <id>` (scripts/store-release.mjs) when the owner releases it, not by each merge (packages/editor/scripts/releases.mjs).
21
+ // A site whose Worker has no store key has no store (storeConnected): "Appstore" has only the site's own.
22
+ import { config } from '@site/config';
23
+ import type { Change, RepoFile } from './github';
24
+ import { blobId } from '../lib/git.mjs';
25
+ import { locale, t } from './i18n';
26
+ import { hooksOf, type AppHook } from '../lib/hooks';
27
+ import type { StoreLicenses } from './licenseData';
28
+ import { APP_API, THEME_API } from '../lib/contracts.mjs';
29
+ import { RECORD, STANDARD, isText, validApp, validTheme } from '../lib/storeEntries.mjs';
30
+
31
+ // The contracts this editor runs (lib/contracts.mjs, also read by the site's Worker).
32
+ export { APP_API, THEME_API };
33
+
34
+ /** The kinds of apps, a filter in the store (t.apps.categories). */
35
+ export const appCategories = ['booking', 'newsletter', 'payment', 'analytics', 'content', 'design'] as const;
36
+ export type AppCategory = (typeof appCategories)[number];
37
+
38
+ /** A file of an app in the store: its path in the app's folder and its git blob id. */
39
+ export type StoreFile = { path: string; oid: string };
40
+
41
+ /** Something an app adds, as the catalog says it (its page before it is installed): its name, icon and what it is. */
42
+ export type StoreAdd = { label: string; icon: string; hint?: string };
43
+
44
+ /** What an app needs of the site, as its folder says (needsOf): its code on every page (slots/), what it loads from
45
+ * other addresses and where its forms send to (its csp.json, by host), its own part of the site's Worker (worker.js),
46
+ * a step when the site is built (build.mjs) and what it changes of what the site does itself (hooks/,
47
+ * @morphemecms/cms/lib/hooks.ts; not in a catalog written before there were any). Shown on its page under "Rettigheder",
48
+ * before it is installed. */
49
+ export type AppNeeds = { pages: boolean; loads: string[]; sends: string[]; server: boolean; build: boolean; changes?: AppHook[] };
50
+
51
+ /** What an app needs, from the paths of the files in its folder and its csp.json. */
52
+ export function needsOf(files: string[], csp: Partial<Record<string, string[]>> = {}): AppNeeds {
53
+ const hosts = (kinds: string[]) => [...new Set(kinds.flatMap((k) => csp[k] ?? []).map((a) => a.replace(/^https:\/\//, '')))].sort();
54
+ return {
55
+ pages: files.some((f) => f.startsWith('slots/')),
56
+ loads: hosts(['script', 'style', 'img', 'connect', 'frame']),
57
+ sends: hosts(['form']),
58
+ server: files.includes('worker.js'),
59
+ build: files.includes('build.mjs'),
60
+ changes: hooksOf(files).hooks,
61
+ };
62
+ }
63
+
64
+ /** A cookie an app sets (also one a service sets in its own frame, as Calendly's), from its cookies.json, on its page
65
+ * under "Cookies": its name (`_ga`, `_ga_*`; none for those a service decides itself, only on its page), what it is
66
+ * for and how long it lasts; with a name, also who sets it and its category (lib/cookieCategories.mjs: `necessary`,
67
+ * also without a yes in the cookie banner, or one the visitor says yes to; `when` in an app from before them, categoryOf),
68
+ * for the cookie list in the cookie policy (the site's packages/editor/site/appCookies.mjs). */
69
+ export type AppCookie = { name?: string; provider?: string; purpose: string; lasts?: string; category?: string; when?: string };
70
+
71
+ /** A picture of an app in its store page, picked to give an idea of it (`screenshots` in its index.ts, as a theme's):
72
+ * what it shows (`shows`: one of its blocks, `blok:<type>`; a variant of its kind of a list, `slags:<list>:<n>`, 0
73
+ * without variants; one of its lists, `liste:<id>`; its settings, `indstillinger`, with made-up `values`, never the
74
+ * site's own; its own page, `side`) and what it says under it (`label`, an app's text). Only made-up content, never the
75
+ * site's (packages/editor/scripts/app-screenshots.mjs takes them with the examples). */
76
+ export type AppShot = { shows: string; label: string; values?: Record<string, unknown> };
77
+
78
+ /** A picture of an app as the catalog has it: its file in the app's screenshots/ and what it says under it. */
79
+ export type AppScreenshot = { file: string; label: string };
80
+
81
+ /** What an app adds, as a picture of it can show it (shotOf). */
82
+ type Shown = {
83
+ kinds?: Record<string, { variants?: unknown[] }>;
84
+ lists?: { id: string }[];
85
+ settings?: unknown;
86
+ page?: unknown;
87
+ };
88
+
89
+ /** Whether a picture shows something the app has (AppShot.shows): one of its blocks (`blocks`: their types), a variant
90
+ * of its kind of a list, one of its lists, its settings or its own page. */
91
+ export function shotOf(shows: string, app: Shown, blocks: string[]): boolean {
92
+ const [what, key = '', n] = shows.split(':');
93
+ if (what === 'blok') return blocks.includes(key);
94
+ if (what === 'slags') {
95
+ const kind = app.kinds?.[key];
96
+ return !!kind && /^\d+$/.test(n ?? '') && Number(n) < Math.max(1, kind.variants?.length ?? 0);
97
+ }
98
+ if (what === 'liste') return !!app.lists?.some((l) => l.id === key);
99
+ if (what === 'indstillinger') return !!app.settings && !key;
100
+ return what === 'side' && !!app.page && !key;
101
+ }
102
+
103
+ /** The file of an app's picture in its screenshots/, by its place among them and what it shows. */
104
+ export const shotFile = (i: number, shows: string) => `${i + 1}-${shows.replace(/:/g, '-')}.jpg`;
105
+
106
+ /** The folder of an app's pictures. */
107
+ export const SCREENSHOTS = 'screenshots';
108
+
109
+ /** An app in the store's catalog. */
110
+ export type StoreApp = {
111
+ id: string;
112
+ version: string;
113
+ api: number;
114
+ label: string;
115
+ publisher: string;
116
+ icon: string;
117
+ hint: string;
118
+ description?: string;
119
+ /** What is new in each version, by the version (AppDefinition.releaseNotes): before it is updated (notesSince). */
120
+ releaseNotes?: Record<string, string>;
121
+ category?: AppCategory;
122
+ keywords?: string[];
123
+ /** What it needs of the site; the cookies it sets, and whether it asks in the cookie banner first (AppDefinition
124
+ * .cookies); its pictures. */
125
+ needs?: AppNeeds;
126
+ /** The names of the kinds of notifications it sends (AppDefinition.notifications). */
127
+ notifies?: string[];
128
+ cookies?: AppCookie[];
129
+ consent?: boolean;
130
+ screenshots?: AppScreenshot[];
131
+ /** Its own licence and the third-party work it brings, each with its licence (its `license` and `thirdParty`; not in
132
+ * a catalog written before they were). */
133
+ licenses?: StoreLicenses;
134
+ /** Its blocks, the lists it brings and its kinds of the site's lists (by the list's id). */
135
+ adds: { blocks: StoreAdd[]; lists: StoreAdd[]; kinds: (StoreAdd & { list: string })[] };
136
+ /** Its texts in the editor's other languages, by the text as it is written. */
137
+ texts?: Partial<Record<string, Record<string, string>>>;
138
+ /** Its folder, file by file. */
139
+ files: StoreFile[];
140
+ };
141
+
142
+ /** A theme in the store's catalog: as its theme.json has it (its texts by the editor's language), its pictures of a
143
+ * site in it (screenshots/<n>-<key>.jpg, t.library.themeShots) and its folder, file by file. */
144
+ export type StoreTheme = {
145
+ id: string;
146
+ version: string;
147
+ api: number;
148
+ name: string;
149
+ author: string;
150
+ icon: string;
151
+ description?: Record<string, string>;
152
+ highlights?: Record<string, string[]>;
153
+ /** What is new in each version, by the language and then the version, as its theme.json has it. */
154
+ releaseNotes?: Record<string, Record<string, string>>;
155
+ screenshots?: { file: string; key: string }[];
156
+ /** Its own licence and the third-party work it brings, as its theme.json declares them (not in a catalog written
157
+ * before they were). */
158
+ licenses?: StoreLicenses;
159
+ /** The icon packs it brings (its icon-packs/<id>/, with their pack.json: name, who made it, licence), installed with it
160
+ * and chosen under "Udseende" › "Ikonpakker". */
161
+ iconPacks?: { id: string; name: string; author: string; license: string }[];
162
+ files: StoreFile[];
163
+ };
164
+
165
+ /** A theme of the catalog as the editor shows it: its texts in the editor's language, and its words for the search. */
166
+ export type ShownTheme = Omit<StoreTheme, 'description' | 'highlights' | 'releaseNotes'> & {
167
+ description?: string;
168
+ highlights: string[];
169
+ releaseNotes?: Record<string, string>;
170
+ words: string;
171
+ };
172
+
173
+ /** An app of the catalog as the editor shows it: its texts in the editor's language, its name as the catalog writes
174
+ * it (for the commits), and all its words in both, for the search. */
175
+ export type ShownApp = StoreApp & { written: string; words: string };
176
+
177
+ /** Where an app installed from the store came from: its version, the version again as `ref` (an older store.json has
178
+ * the store's commit there) and its files (store.json). */
179
+ export type StoreRecord = { version: string; ref: string; files: StoreFile[] };
180
+
181
+ /** The store's address in the editor, under "Indstillinger" (its apps; its themes at STORE_THEMES), its address
182
+ * before (STORE_BEFORE, which opens it, App.tsx MOVED), and that of a developer's page (DEVELOPER_ROUTE/<slug>); no
183
+ * app can have the ids of the last two. */
184
+ export const STORE_ROUTE = 'indstillinger/appstore';
185
+ export const STORE_THEMES = 'indstillinger/appstore/temaer';
186
+ export const STORE_BEFORE = 'apps/butik';
187
+ export const DEVELOPER_ROUTE = 'apps/udvikler';
188
+
189
+ /** An app's folder in the site, and the note in it of where it came from. */
190
+ export const appFolder = (id: string) => `${config.apps.folder}/${id}`;
191
+ /** A theme's folder in the site; the standard theme's is always there. */
192
+ export const themeFolder = (id: string) => `${config.themes}/${id}`;
193
+ export const STANDARD_THEME = STANDARD;
194
+ export { RECORD };
195
+
196
+ /** Whether a version is newer than another ("1.10.0" than "1.9.2"). */
197
+ export function newer(a: string, b: string): boolean {
198
+ const parts = (v: string) => v.split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
199
+ const [x, y] = [parts(a), parts(b)];
200
+ for (let i = 0; i < Math.max(x.length, y.length); i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
201
+ return false;
202
+ }
203
+
204
+ /** Whether a version has a higher first number than another ("2.0.0" than "1.9.2"): it has changes that can break
205
+ * something, so it is not updated to on its own (autoUpdate in AppList.tsx). */
206
+ export const breaking = (a: string, b: string) => (Number.parseInt(a, 10) || 0) > (Number.parseInt(b, 10) || 0);
207
+
208
+ /** What an update brings: the release notes of the versions after the one installed (`from`) up to the new one (`to`),
209
+ * the newest first. */
210
+ export function notesSince(notes: Record<string, string> | undefined, from: string, to: string): { version: string; text: string }[] {
211
+ return Object.entries(notes ?? {})
212
+ .filter(([v, text]) => isText(text) && newer(v, from) && !newer(v, to))
213
+ .sort(([a], [b]) => (newer(a, b) ? -1 : 1))
214
+ .map(([version, text]) => ({ version, text }));
215
+ }
216
+
217
+ // Release notes as the catalog has them: only texts, by their version.
218
+ const notesOf = (x: unknown) =>
219
+ x && typeof x === 'object' && !Array.isArray(x)
220
+ ? Object.fromEntries(Object.entries(x).filter((e): e is [string, string] => isText(e[1])))
221
+ : undefined;
222
+
223
+ const validStoreApp = (a: any): a is StoreApp => validApp(a, APP_API);
224
+ const validStoreTheme = (x: any): x is StoreTheme => validTheme(x, THEME_API);
225
+
226
+ /** The apps in a catalog (catalog.json, as text) the editor can install, with their texts in its language. */
227
+ export function catalogApps(text: string): ShownApp[] {
228
+ let json: any;
229
+ try {
230
+ json = JSON.parse(text);
231
+ } catch {
232
+ return [];
233
+ }
234
+ const apps: unknown[] = Array.isArray(json?.apps) ? json.apps : [];
235
+ return apps.filter(validStoreApp).map((a) => {
236
+ const own = a.texts?.[locale];
237
+ const say = (s: string) => own?.[s] ?? s;
238
+ const add = <A extends StoreAdd>(x: A): A => ({ ...x, label: say(x.label), ...(x.hint ? { hint: say(x.hint) } : {}) });
239
+ const keywords = a.keywords ?? [];
240
+ return {
241
+ ...a,
242
+ label: say(a.label),
243
+ hint: say(a.hint),
244
+ ...(a.description ? { description: say(a.description) } : {}),
245
+ releaseNotes: Object.fromEntries(Object.entries(notesOf(a.releaseNotes) ?? {}).map(([v, x]) => [v, say(x)])),
246
+ keywords: keywords.map(say),
247
+ notifies: (Array.isArray(a.notifies) ? a.notifies : []).filter(isText).map(say),
248
+ cookies: (Array.isArray(a.cookies) ? a.cookies : [])
249
+ .filter((c) => isText(c?.purpose))
250
+ .map((c) => ({
251
+ ...c,
252
+ purpose: say(c.purpose),
253
+ ...(c.lasts ? { lasts: say(c.lasts) } : {}),
254
+ })),
255
+ screenshots: (Array.isArray(a.screenshots) ? a.screenshots : [])
256
+ .filter((x) => isText(x?.file) && isText(x?.label) && a.files.some((f) => f.path === `${SCREENSHOTS}/${x.file}`))
257
+ .map((x) => ({ ...x, label: say(x.label) })),
258
+ adds: { blocks: a.adds.blocks.map(add), lists: a.adds.lists.map(add), kinds: a.adds.kinds.map(add) },
259
+ written: a.label,
260
+ words: [a.label, a.hint, a.publisher, ...keywords, ...[a.label, a.hint, ...keywords].map(say)].join(' ').toLowerCase(),
261
+ };
262
+ });
263
+ }
264
+
265
+ /** The themes in a catalog (catalog.json, as text) the editor can install, with their texts in its language (else the
266
+ * first they have). */
267
+ export function catalogThemes(text: string): ShownTheme[] {
268
+ let json: any;
269
+ try {
270
+ json = JSON.parse(text);
271
+ } catch {
272
+ return [];
273
+ }
274
+ const themes: unknown[] = Array.isArray(json?.themes) ? json.themes : [];
275
+ const inLocale = <T>(x?: Record<string, T>) => x && (x[locale] ?? Object.values(x)[0]);
276
+ return themes.filter(validStoreTheme).map((x) => {
277
+ const description = inLocale(x.description);
278
+ const highlights = inLocale(x.highlights) ?? [];
279
+ return {
280
+ ...x,
281
+ description: typeof description === 'string' ? description : undefined,
282
+ highlights: Array.isArray(highlights) ? highlights.filter(isText) : [],
283
+ releaseNotes: notesOf(inLocale(x.releaseNotes)),
284
+ screenshots: (Array.isArray(x.screenshots) ? x.screenshots : []).filter(
285
+ (s) => isText(s?.file) && isText(s?.key) && x.files.some((f) => f.path === `${SCREENSHOTS}/${s.file}`),
286
+ ),
287
+ iconPacks: (Array.isArray(x.iconPacks) ? x.iconPacks : []).filter(
288
+ (p) => [p?.id, p?.name, p?.author, p?.license].every(isText) && x.files.some((f) => f.path === `icon-packs/${p.id}/pack.json`),
289
+ ),
290
+ words: [x.name, x.author, ...Object.values(x.description ?? {}), ...Object.values(x.highlights ?? {}).flat()].join(' ').toLowerCase(),
291
+ };
292
+ });
293
+ }
294
+
295
+ // The catalog, read once while the editor is open (again after it could not be read).
296
+ let catalog: Promise<string> | null = null;
297
+
298
+ // The catalog as the site's Worker passes it on: `{ connected: false }` when the site has no key to the store.
299
+ function catalogText(): Promise<string> {
300
+ catalog ??= fetch('/api/store/catalog').then(
301
+ (res) => {
302
+ if (!res.ok) throw new Error(t.apps.storeFailed);
303
+ return res.text();
304
+ },
305
+ () => {
306
+ throw new Error(t.apps.storeFailed);
307
+ },
308
+ );
309
+ catalog.catch(() => (catalog = null));
310
+ return catalog;
311
+ }
312
+
313
+ /** Whether the site's Worker has a store key; without one, "Appstore" has only the site's own apps and themes. Fails when
314
+ * the catalog cannot be read. */
315
+ export const storeConnected = (): Promise<boolean> =>
316
+ catalogText().then((text) => {
317
+ try {
318
+ return JSON.parse(text)?.connected !== false;
319
+ } catch {
320
+ return true;
321
+ }
322
+ });
323
+
324
+ /** The store's apps. Fails when the catalog cannot be read. */
325
+ export const readCatalog = (): Promise<ShownApp[]> => catalogText().then(catalogApps);
326
+
327
+ /** The store's themes. Fails when the catalog cannot be read. */
328
+ export const readThemes = (): Promise<ShownTheme[]> => catalogText().then(catalogThemes);
329
+
330
+ // A file from the store by its blob id (through the site's Worker), checked against the id.
331
+ async function readFile(oid: string): Promise<Uint8Array> {
332
+ const res = await fetch(`/api/store/files/${oid}`);
333
+ if (!res.ok) throw new Error(t.apps.storeFailed);
334
+ const bytes = new Uint8Array(await res.arrayBuffer());
335
+ if ((await blobId(bytes)) !== oid) throw new Error(t.apps.storeFailed);
336
+ return bytes;
337
+ }
338
+
339
+ // The text of a file, or null when it is binary (a NUL, or not UTF-8), as GitHub told them apart before.
340
+ function textOf(bytes: Uint8Array): string | null {
341
+ if (bytes.includes(0)) return null;
342
+ try {
343
+ // ignoreBOM keeps a leading BOM in the text, so it encodes back to the same bytes (and the same blob id).
344
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
345
+ } catch {
346
+ return null;
347
+ }
348
+ }
349
+
350
+ /** An app's (or a theme's) files from the store, each checked against its blob id; text files as text. */
351
+ export async function readApp(app: {
352
+ files: StoreFile[];
353
+ label?: string;
354
+ name?: string;
355
+ }): Promise<{ path: string; content: string | Uint8Array }[]> {
356
+ return Promise.all(
357
+ app.files.map(async (f) => {
358
+ try {
359
+ const bytes = await readFile(f.oid);
360
+ return { path: f.path, content: textOf(bytes) ?? bytes };
361
+ } catch {
362
+ throw new Error(t.apps.storeBroken(app.label ?? app.name ?? f.path));
363
+ }
364
+ }),
365
+ );
366
+ }
367
+
368
+ /** An app's pictures from the store (its screenshots/, AppScreenshot), each as an address in this browser (a blob:)
369
+ * with what it shows; one that cannot be read is left out. */
370
+ export async function readScreenshots(app: StoreApp): Promise<{ src: string; label: string }[]> {
371
+ const types: Record<string, string> = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp' };
372
+ const read = await Promise.all(
373
+ (app.screenshots ?? []).map(async (x) => {
374
+ const oid = app.files.find((f) => f.path === `${SCREENSHOTS}/${x.file}`)?.oid;
375
+ const type = types[x.file.split('.').pop()!.toLowerCase()];
376
+ if (!oid || !type) return null;
377
+ try {
378
+ const bytes = await readFile(oid);
379
+ return { src: URL.createObjectURL(new Blob([bytes as BlobPart], { type })), label: x.label };
380
+ } catch {
381
+ return null;
382
+ }
383
+ }),
384
+ );
385
+ return read.filter((x) => !!x);
386
+ }
387
+
388
+ /** A theme's pictures of a site in it from the store (its screenshots/), each as an address in this browser (a blob:)
389
+ * with what it shows (its key, t.library.themeShots); one that cannot be read is left out. */
390
+ export async function readThemeScreenshots(theme: Pick<StoreTheme, 'screenshots' | 'files'>): Promise<{ key: string; src: string }[]> {
391
+ const read = await Promise.all(
392
+ (theme.screenshots ?? []).map(async (x) => {
393
+ const oid = theme.files.find((f) => f.path === `${SCREENSHOTS}/${x.file}`)?.oid;
394
+ if (!oid) return null;
395
+ try {
396
+ const bytes = await readFile(oid);
397
+ return { key: x.key, src: URL.createObjectURL(new Blob([bytes as BlobPart], { type: 'image/jpeg' })) };
398
+ } catch {
399
+ return null;
400
+ }
401
+ }),
402
+ );
403
+ return read.filter((x) => !!x);
404
+ }
405
+
406
+ /** The changes that make a theme's folder in the site (`current`, its files now) the store's version, with the note of
407
+ * where it came from, or remove it (`next` null: uninstalling). In one commit. */
408
+ export function themeChanges(
409
+ id: string,
410
+ current: RepoFile[],
411
+ next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
412
+ ): Change[] {
413
+ return folderChanges(themeFolder(id), current, next);
414
+ }
415
+
416
+ /** The changes that make a folder in the site (`current`, its files now) the store's version of an app or a theme, with
417
+ * the note of where it came from, or remove it (`next` null). */
418
+ function folderChanges(
419
+ folder: string,
420
+ current: RepoFile[],
421
+ next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
422
+ ): Change[] {
423
+ const now = new Map(current.map((f) => [f.path, f.oid]));
424
+ const wanted: { path: string; content: string | Uint8Array; oid?: string }[] = next
425
+ ? [
426
+ ...next.files.map((f) => ({
427
+ path: `${folder}/${f.path}`,
428
+ content: f.content,
429
+ oid: next.record.files.find((r) => r.path === f.path)?.oid,
430
+ })),
431
+ { path: `${folder}/${RECORD}`, content: `${JSON.stringify(next.record, null, 2)}\n` },
432
+ ]
433
+ : [];
434
+ return [
435
+ // A file the site has as it is in the store is left as it is.
436
+ ...wanted
437
+ .filter((w) => !w.oid || now.get(w.path) !== w.oid)
438
+ .map((w) => ({ path: w.path, content: w.content, oid: now.get(w.path) ?? null })),
439
+ ...current.filter((f) => !wanted.some((w) => w.path === f.path)).map((f) => ({ path: f.path, content: null, oid: f.oid })),
440
+ ];
441
+ }
442
+
443
+ /** The changes that make an app's folder in the site (`current`, its files now) the store's version, with the note of
444
+ * where it came from, or remove it (`next` null: uninstalling); and the site's file of apps with it among those
445
+ * installed or not (`installed`: their ids now, `file`: the file). In one commit. */
446
+ export function appChanges(
447
+ id: string,
448
+ current: RepoFile[],
449
+ next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
450
+ installed: string[],
451
+ file: RepoFile | undefined,
452
+ ): Change[] {
453
+ const changes = folderChanges(appFolder(id), current, next);
454
+ const ids = [...new Set(installed.filter((x) => x !== id).concat(next ? [id] : []))].sort();
455
+ const text = `${JSON.stringify({ installed: ids }, null, 2)}\n`;
456
+ if (text !== file?.text) changes.push({ path: config.apps.installed, content: text, oid: file?.oid || null });
457
+ return changes;
458
+ }
459
+
460
+ /** The note in an app's (or a theme's) folder of where it came from (store.json), from its files now (`current`). */
461
+ export function recordIn(current: RepoFile[], root: string): StoreRecord | undefined {
462
+ try {
463
+ const record = JSON.parse(current.find((f) => f.path === `${root}/${RECORD}`)?.text ?? '');
464
+ return isText(record?.version) && Array.isArray(record.files) ? record : undefined;
465
+ } catch {
466
+ return undefined;
467
+ }
468
+ }
469
+
470
+ /** The files of an app installed from the store that were changed in the site since (in its folder, `current`, not
471
+ * as its note says), by their paths in the folder: an update writes over them. */
472
+ export function changedFiles(id: string, record: StoreRecord, current: RepoFile[], root = appFolder(id)): string[] {
473
+ const folder = `${root}/`;
474
+ const own = current.map((f) => ({ path: f.path.slice(folder.length), oid: f.oid }));
475
+ return [
476
+ ...own.filter((f) => f.path !== RECORD && record.files.find((r) => r.path === f.path)?.oid !== f.oid).map((f) => f.path),
477
+ ...record.files.filter((r) => !own.some((f) => f.path === r.path)).map((r) => r.path),
478
+ ];
479
+ }