@morphemecms/cms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -3
  3. package/astro.d.mts +2 -0
  4. package/astro.mjs +162 -0
  5. package/bin/morpheme.mjs +38 -0
  6. package/client.d.ts +10 -0
  7. package/defaults/blockTexts.ts +33 -0
  8. package/defaults/collections.ts +774 -0
  9. package/defaults/colors.ts +125 -0
  10. package/defaults/company.ts +20 -0
  11. package/defaults/editor.ts +178 -0
  12. package/defaults/internalLinks.ts +30 -0
  13. package/defaults/opens.ts +18 -0
  14. package/defaults/popup.tsx +93 -0
  15. package/defaults/privacy.ts +79 -0
  16. package/defaults/rules.ts +450 -0
  17. package/defaults/schema.ts +16 -0
  18. package/defaults/sitedata.ts +129 -0
  19. package/defaults/standard.ts +106 -0
  20. package/defaults/standardPages.ts +87 -0
  21. package/defaults/texts.ts +481 -0
  22. package/demo/README.md +26 -0
  23. package/demo/public/apple-touch-icon.png +0 -0
  24. package/demo/public/editor/apple-touch-icon.png +0 -0
  25. package/demo/public/editor/icon-192.png +0 -0
  26. package/demo/public/editor/icon-512.png +0 -0
  27. package/demo/public/editor/icon-maskable-512.png +0 -0
  28. package/demo/public/favicon.ico +0 -0
  29. package/demo/public/favicon.svg +1 -0
  30. package/demo/public/images/uploads/2026/10/andromeda.webp +0 -0
  31. package/demo/public/images/uploads/2026/10/earth-from-space.webp +0 -0
  32. package/demo/public/images/uploads/2026/10/editor-blog.webp +0 -0
  33. package/demo/public/images/uploads/2026/10/editor-colors.webp +0 -0
  34. package/demo/public/images/uploads/2026/10/editor-home.webp +0 -0
  35. package/demo/public/images/uploads/2026/10/editor-page.webp +0 -0
  36. package/demo/public/images/uploads/2026/10/editor-pages.webp +0 -0
  37. package/demo/public/images/uploads/2026/10/editor-seo.webp +0 -0
  38. package/demo/public/images/uploads/2026/10/editor-start.webp +0 -0
  39. package/demo/public/images/uploads/2026/10/editor-themes.webp +0 -0
  40. package/demo/public/images/uploads/2026/10/moon.webp +0 -0
  41. package/demo/public/images/uploads/2026/10/saturn.webp +0 -0
  42. package/demo/public/logo.svg +1 -0
  43. package/demo/src/content/blog/build-a-page-from-blocks.md +19 -0
  44. package/demo/src/content/blog/colors-everyone-can-read.md +19 -0
  45. package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -0
  46. package/demo/src/content/blog/every-save-is-a-version.md +21 -0
  47. package/demo/src/content/blog/schedule-a-post.md +17 -0
  48. package/demo/src/content/blog/secure-from-day-one.md +17 -0
  49. package/demo/src/content/blog/seo-without-the-guesswork.md +23 -0
  50. package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -0
  51. package/demo/src/content/blog/the-app-store.md +21 -0
  52. package/demo/src/content/pages/about.md +22 -0
  53. package/demo/src/content/pages/app-store.md +30 -0
  54. package/demo/src/content/pages/blocks.md +98 -0
  55. package/demo/src/content/pages/blog-page.md +11 -0
  56. package/demo/src/content/pages/contact.md +16 -0
  57. package/demo/src/content/pages/get-started.md +20 -0
  58. package/demo/src/content/pages/home.md +26 -0
  59. package/demo/src/content/pages/page-not-found.md +11 -0
  60. package/demo/src/content/pages/privacy-policy.md +24 -0
  61. package/demo/src/data/blocks.json +25 -0
  62. package/demo/src/data/booking.json +6 -0
  63. package/demo/src/data/categories/apps.json +6 -0
  64. package/demo/src/data/categories/design.json +6 -0
  65. package/demo/src/data/categories/editing.json +6 -0
  66. package/demo/src/data/categories/security.json +6 -0
  67. package/demo/src/data/categories/seo.json +6 -0
  68. package/demo/src/data/colors.json +75 -0
  69. package/demo/src/data/contact.json +8 -0
  70. package/demo/src/data/cookies.json +21 -0
  71. package/demo/src/data/footer.json +27 -0
  72. package/demo/src/data/menu.json +35 -0
  73. package/demo/src/data/mobile-menu.json +25 -0
  74. package/demo/src/data/popups/booking-bar.json +13 -0
  75. package/demo/src/data/popups/try-the-editor.json +16 -0
  76. package/demo/src/data/standard-pages.json +8 -0
  77. package/demo/src/data/testimonials/sample-bakery.json +10 -0
  78. package/demo/src/data/testimonials/sample-clinic.json +10 -0
  79. package/demo/src/data/testimonials/sample-studio.json +10 -0
  80. package/demo/src/site/colors.ts +11 -0
  81. package/demo/src/site/config.ts +19 -0
  82. package/icons/lucide/LICENSE +43 -0
  83. package/icons/lucide/LICENSE-material-design-icons +20 -0
  84. package/icons/lucide/address-book.svg +1 -0
  85. package/icons/lucide/app-window.svg +1 -0
  86. package/icons/lucide/arrow-counter-clockwise.svg +1 -0
  87. package/icons/lucide/arrow-down.svg +1 -0
  88. package/icons/lucide/arrow-left.svg +1 -0
  89. package/icons/lucide/arrow-right.svg +1 -0
  90. package/icons/lucide/arrow-square-out.svg +1 -0
  91. package/icons/lucide/arrow-u-up-left.svg +1 -0
  92. package/icons/lucide/arrow-u-up-right.svg +1 -0
  93. package/icons/lucide/arrow-up-right.svg +1 -0
  94. package/icons/lucide/arrow-up.svg +1 -0
  95. package/icons/lucide/arrows-down-up.svg +1 -0
  96. package/icons/lucide/arrows-in-line-horizontal.svg +1 -0
  97. package/icons/lucide/arrows-out-line-horizontal.svg +1 -0
  98. package/icons/lucide/article.svg +1 -0
  99. package/icons/lucide/bank.svg +1 -0
  100. package/icons/lucide/bell.svg +1 -0
  101. package/icons/lucide/bicycle.svg +1 -0
  102. package/icons/lucide/binoculars.svg +1 -0
  103. package/icons/lucide/book-open-text.svg +1 -0
  104. package/icons/lucide/book-open.svg +1 -0
  105. package/icons/lucide/browser.svg +1 -0
  106. package/icons/lucide/buildings.svg +1 -0
  107. package/icons/lucide/bus.svg +1 -0
  108. package/icons/lucide/calendar-blank.svg +1 -0
  109. package/icons/lucide/calendar-check.svg +1 -0
  110. package/icons/lucide/car.svg +1 -0
  111. package/icons/lucide/cards.svg +1 -0
  112. package/icons/lucide/caret-down.svg +1 -0
  113. package/icons/lucide/caret-left.svg +1 -0
  114. package/icons/lucide/caret-right.svg +1 -0
  115. package/icons/lucide/caret-up.svg +1 -0
  116. package/icons/lucide/certificate.svg +1 -0
  117. package/icons/lucide/chart-line-up.svg +1 -0
  118. package/icons/lucide/chats.svg +1 -0
  119. package/icons/lucide/check-circle.svg +1 -0
  120. package/icons/lucide/check.svg +1 -0
  121. package/icons/lucide/circle-half.svg +1 -0
  122. package/icons/lucide/circle.svg +1 -0
  123. package/icons/lucide/clock-counter-clockwise.svg +1 -0
  124. package/icons/lucide/clock.svg +1 -0
  125. package/icons/lucide/code.svg +1 -0
  126. package/icons/lucide/coffee.svg +1 -0
  127. package/icons/lucide/columns.svg +1 -0
  128. package/icons/lucide/cookie.svg +1 -0
  129. package/icons/lucide/copy.svg +1 -0
  130. package/icons/lucide/credit-card.svg +1 -0
  131. package/icons/lucide/currency-circle-dollar.svg +1 -0
  132. package/icons/lucide/cursor-click.svg +1 -0
  133. package/icons/lucide/database.svg +1 -0
  134. package/icons/lucide/desktop.svg +1 -0
  135. package/icons/lucide/device-mobile.svg +1 -0
  136. package/icons/lucide/door-open.svg +1 -0
  137. package/icons/lucide/download-simple.svg +1 -0
  138. package/icons/lucide/envelope-simple.svg +1 -0
  139. package/icons/lucide/eye-slash.svg +1 -0
  140. package/icons/lucide/eye.svg +1 -0
  141. package/icons/lucide/file-pdf.svg +1 -0
  142. package/icons/lucide/file-text.svg +1 -0
  143. package/icons/lucide/file-video.svg +1 -0
  144. package/icons/lucide/file-zip.svg +1 -0
  145. package/icons/lucide/flower-lotus.svg +1 -0
  146. package/icons/lucide/folder-simple.svg +1 -0
  147. package/icons/lucide/funnel.svg +1 -0
  148. package/icons/lucide/gauge.svg +1 -0
  149. package/icons/lucide/gear.svg +1 -0
  150. package/icons/lucide/github-logo.svg +1 -0
  151. package/icons/lucide/globe.svg +1 -0
  152. package/icons/lucide/google-logo.svg +1 -0
  153. package/icons/lucide/graduation-cap.svg +1 -0
  154. package/icons/lucide/hammer.svg +1 -0
  155. package/icons/lucide/hand-heart.svg +1 -0
  156. package/icons/lucide/handshake.svg +1 -0
  157. package/icons/lucide/hard-drives.svg +1 -0
  158. package/icons/lucide/heart.svg +1 -0
  159. package/icons/lucide/house.svg +1 -0
  160. package/icons/lucide/image-square.svg +1 -0
  161. package/icons/lucide/image.svg +1 -0
  162. package/icons/lucide/info.svg +1 -0
  163. package/icons/lucide/keyboard.svg +1 -0
  164. package/icons/lucide/leaf.svg +1 -0
  165. package/icons/lucide/lifebuoy.svg +1 -0
  166. package/icons/lucide/lightbulb.svg +1 -0
  167. package/icons/lucide/link-break.svg +1 -0
  168. package/icons/lucide/link-simple.svg +1 -0
  169. package/icons/lucide/list-bullets.svg +1 -0
  170. package/icons/lucide/list-checks.svg +1 -0
  171. package/icons/lucide/list-numbers.svg +1 -0
  172. package/icons/lucide/list.svg +1 -0
  173. package/icons/lucide/lock-simple-open.svg +1 -0
  174. package/icons/lucide/lock-simple.svg +1 -0
  175. package/icons/lucide/magnifying-glass.svg +1 -0
  176. package/icons/lucide/map-pin.svg +1 -0
  177. package/icons/lucide/map-trifold.svg +1 -0
  178. package/icons/lucide/medal.svg +1 -0
  179. package/icons/lucide/megaphone.svg +1 -0
  180. package/icons/lucide/minus.svg +1 -0
  181. package/icons/lucide/moon.svg +1 -0
  182. package/icons/lucide/newspaper.svg +1 -0
  183. package/icons/lucide/pack.json +23 -0
  184. package/icons/lucide/paint-brush-broad.svg +1 -0
  185. package/icons/lucide/palette.svg +1 -0
  186. package/icons/lucide/paper-plane-tilt.svg +1 -0
  187. package/icons/lucide/pencil-simple.svg +1 -0
  188. package/icons/lucide/phone.svg +1 -0
  189. package/icons/lucide/play-fill.svg +1 -0
  190. package/icons/lucide/plus.svg +1 -0
  191. package/icons/lucide/prohibit.svg +1 -0
  192. package/icons/lucide/puzzle-piece.svg +1 -0
  193. package/icons/lucide/question.svg +1 -0
  194. package/icons/lucide/quotes.svg +1 -0
  195. package/icons/lucide/rows.svg +1 -0
  196. package/icons/lucide/ruler.svg +1 -0
  197. package/icons/lucide/scan.svg +1 -0
  198. package/icons/lucide/shapes.svg +1 -0
  199. package/icons/lucide/share-network.svg +1 -0
  200. package/icons/lucide/shield-check.svg +1 -0
  201. package/icons/lucide/sidebar-simple.svg +1 -0
  202. package/icons/lucide/sign-out.svg +1 -0
  203. package/icons/lucide/signpost.svg +1 -0
  204. package/icons/lucide/sliders-horizontal.svg +1 -0
  205. package/icons/lucide/smiley.svg +1 -0
  206. package/icons/lucide/sparkle.svg +1 -0
  207. package/icons/lucide/square-half-bottom.svg +1 -0
  208. package/icons/lucide/square.svg +1 -0
  209. package/icons/lucide/squares-four.svg +1 -0
  210. package/icons/lucide/sticker.svg +1 -0
  211. package/icons/lucide/storefront.svg +1 -0
  212. package/icons/lucide/sun.svg +1 -0
  213. package/icons/lucide/swatches.svg +1 -0
  214. package/icons/lucide/tag.svg +1 -0
  215. package/icons/lucide/text-aa.svg +1 -0
  216. package/icons/lucide/text-align-left.svg +1 -0
  217. package/icons/lucide/text-b.svg +1 -0
  218. package/icons/lucide/text-h-one.svg +1 -0
  219. package/icons/lucide/text-h-three.svg +1 -0
  220. package/icons/lucide/text-h-two.svg +1 -0
  221. package/icons/lucide/text-italic.svg +1 -0
  222. package/icons/lucide/text-t.svg +1 -0
  223. package/icons/lucide/ticket.svg +1 -0
  224. package/icons/lucide/toggle-right.svg +1 -0
  225. package/icons/lucide/train.svg +1 -0
  226. package/icons/lucide/trash.svg +1 -0
  227. package/icons/lucide/upload-simple.svg +1 -0
  228. package/icons/lucide/user-circle.svg +1 -0
  229. package/icons/lucide/user.svg +1 -0
  230. package/icons/lucide/users-three.svg +1 -0
  231. package/icons/lucide/users.svg +1 -0
  232. package/icons/lucide/video-camera.svg +1 -0
  233. package/icons/lucide/warning-circle.svg +1 -0
  234. package/icons/lucide/warning.svg +1 -0
  235. package/icons/lucide/wave-sine.svg +1 -0
  236. package/icons/lucide/wheelchair.svg +1 -0
  237. package/icons/lucide/wrench.svg +1 -0
  238. package/icons/lucide/x.svg +1 -0
  239. package/icons/material-symbols/LICENSE +201 -0
  240. package/icons/material-symbols/LICENSE-material-design-icons +20 -0
  241. package/icons/material-symbols/address-book.svg +1 -0
  242. package/icons/material-symbols/app-window.svg +1 -0
  243. package/icons/material-symbols/arrow-counter-clockwise.svg +1 -0
  244. package/icons/material-symbols/arrow-down.svg +1 -0
  245. package/icons/material-symbols/arrow-left.svg +1 -0
  246. package/icons/material-symbols/arrow-right.svg +1 -0
  247. package/icons/material-symbols/arrow-square-out.svg +1 -0
  248. package/icons/material-symbols/arrow-u-up-left.svg +1 -0
  249. package/icons/material-symbols/arrow-u-up-right.svg +1 -0
  250. package/icons/material-symbols/arrow-up-right.svg +1 -0
  251. package/icons/material-symbols/arrow-up.svg +1 -0
  252. package/icons/material-symbols/arrows-down-up.svg +1 -0
  253. package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -0
  254. package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -0
  255. package/icons/material-symbols/article.svg +1 -0
  256. package/icons/material-symbols/bank.svg +1 -0
  257. package/icons/material-symbols/bell.svg +1 -0
  258. package/icons/material-symbols/bicycle.svg +1 -0
  259. package/icons/material-symbols/binoculars.svg +1 -0
  260. package/icons/material-symbols/book-open-text.svg +1 -0
  261. package/icons/material-symbols/book-open.svg +1 -0
  262. package/icons/material-symbols/browser.svg +1 -0
  263. package/icons/material-symbols/buildings.svg +1 -0
  264. package/icons/material-symbols/bus.svg +1 -0
  265. package/icons/material-symbols/calendar-blank.svg +1 -0
  266. package/icons/material-symbols/calendar-check.svg +1 -0
  267. package/icons/material-symbols/car.svg +1 -0
  268. package/icons/material-symbols/cards.svg +1 -0
  269. package/icons/material-symbols/caret-down.svg +1 -0
  270. package/icons/material-symbols/caret-left.svg +1 -0
  271. package/icons/material-symbols/caret-right.svg +1 -0
  272. package/icons/material-symbols/caret-up.svg +1 -0
  273. package/icons/material-symbols/certificate.svg +1 -0
  274. package/icons/material-symbols/chart-line-up.svg +1 -0
  275. package/icons/material-symbols/chats.svg +1 -0
  276. package/icons/material-symbols/check-circle.svg +1 -0
  277. package/icons/material-symbols/check.svg +1 -0
  278. package/icons/material-symbols/circle-half.svg +1 -0
  279. package/icons/material-symbols/circle.svg +1 -0
  280. package/icons/material-symbols/clock-counter-clockwise.svg +1 -0
  281. package/icons/material-symbols/clock.svg +1 -0
  282. package/icons/material-symbols/code.svg +1 -0
  283. package/icons/material-symbols/coffee.svg +1 -0
  284. package/icons/material-symbols/columns.svg +1 -0
  285. package/icons/material-symbols/cookie.svg +1 -0
  286. package/icons/material-symbols/copy.svg +1 -0
  287. package/icons/material-symbols/credit-card.svg +1 -0
  288. package/icons/material-symbols/currency-circle-dollar.svg +1 -0
  289. package/icons/material-symbols/cursor-click.svg +1 -0
  290. package/icons/material-symbols/database.svg +1 -0
  291. package/icons/material-symbols/desktop.svg +1 -0
  292. package/icons/material-symbols/device-mobile.svg +1 -0
  293. package/icons/material-symbols/door-open.svg +1 -0
  294. package/icons/material-symbols/download-simple.svg +1 -0
  295. package/icons/material-symbols/envelope-simple.svg +1 -0
  296. package/icons/material-symbols/eye-slash.svg +1 -0
  297. package/icons/material-symbols/eye.svg +1 -0
  298. package/icons/material-symbols/file-pdf.svg +1 -0
  299. package/icons/material-symbols/file-text.svg +1 -0
  300. package/icons/material-symbols/file-video.svg +1 -0
  301. package/icons/material-symbols/file-zip.svg +1 -0
  302. package/icons/material-symbols/flower-lotus.svg +1 -0
  303. package/icons/material-symbols/folder-simple.svg +1 -0
  304. package/icons/material-symbols/funnel.svg +1 -0
  305. package/icons/material-symbols/gauge.svg +1 -0
  306. package/icons/material-symbols/gear.svg +1 -0
  307. package/icons/material-symbols/github-logo.svg +1 -0
  308. package/icons/material-symbols/globe.svg +1 -0
  309. package/icons/material-symbols/google-logo.svg +1 -0
  310. package/icons/material-symbols/graduation-cap.svg +1 -0
  311. package/icons/material-symbols/hammer.svg +1 -0
  312. package/icons/material-symbols/hand-heart.svg +1 -0
  313. package/icons/material-symbols/handshake.svg +1 -0
  314. package/icons/material-symbols/hard-drives.svg +1 -0
  315. package/icons/material-symbols/heart.svg +1 -0
  316. package/icons/material-symbols/house.svg +1 -0
  317. package/icons/material-symbols/image-square.svg +1 -0
  318. package/icons/material-symbols/image.svg +1 -0
  319. package/icons/material-symbols/info.svg +1 -0
  320. package/icons/material-symbols/keyboard.svg +1 -0
  321. package/icons/material-symbols/leaf.svg +1 -0
  322. package/icons/material-symbols/lifebuoy.svg +1 -0
  323. package/icons/material-symbols/lightbulb.svg +1 -0
  324. package/icons/material-symbols/link-break.svg +1 -0
  325. package/icons/material-symbols/link-simple.svg +1 -0
  326. package/icons/material-symbols/list-bullets.svg +1 -0
  327. package/icons/material-symbols/list-checks.svg +1 -0
  328. package/icons/material-symbols/list-numbers.svg +1 -0
  329. package/icons/material-symbols/list.svg +1 -0
  330. package/icons/material-symbols/lock-simple-open.svg +1 -0
  331. package/icons/material-symbols/lock-simple.svg +1 -0
  332. package/icons/material-symbols/magnifying-glass.svg +1 -0
  333. package/icons/material-symbols/map-pin.svg +1 -0
  334. package/icons/material-symbols/map-trifold.svg +1 -0
  335. package/icons/material-symbols/medal.svg +1 -0
  336. package/icons/material-symbols/megaphone.svg +1 -0
  337. package/icons/material-symbols/minus.svg +1 -0
  338. package/icons/material-symbols/moon.svg +1 -0
  339. package/icons/material-symbols/newspaper.svg +1 -0
  340. package/icons/material-symbols/pack.json +29 -0
  341. package/icons/material-symbols/paint-brush-broad.svg +1 -0
  342. package/icons/material-symbols/palette.svg +1 -0
  343. package/icons/material-symbols/paper-plane-tilt.svg +1 -0
  344. package/icons/material-symbols/pencil-simple.svg +1 -0
  345. package/icons/material-symbols/phone.svg +1 -0
  346. package/icons/material-symbols/play-fill.svg +1 -0
  347. package/icons/material-symbols/plus.svg +1 -0
  348. package/icons/material-symbols/prohibit.svg +1 -0
  349. package/icons/material-symbols/puzzle-piece.svg +1 -0
  350. package/icons/material-symbols/question.svg +1 -0
  351. package/icons/material-symbols/quotes.svg +1 -0
  352. package/icons/material-symbols/rows.svg +1 -0
  353. package/icons/material-symbols/ruler.svg +1 -0
  354. package/icons/material-symbols/scan.svg +1 -0
  355. package/icons/material-symbols/shapes.svg +1 -0
  356. package/icons/material-symbols/share-network.svg +1 -0
  357. package/icons/material-symbols/shield-check.svg +1 -0
  358. package/icons/material-symbols/sidebar-simple.svg +1 -0
  359. package/icons/material-symbols/sign-out.svg +1 -0
  360. package/icons/material-symbols/signpost.svg +1 -0
  361. package/icons/material-symbols/sliders-horizontal.svg +1 -0
  362. package/icons/material-symbols/smiley.svg +1 -0
  363. package/icons/material-symbols/sparkle.svg +1 -0
  364. package/icons/material-symbols/square-half-bottom.svg +1 -0
  365. package/icons/material-symbols/square.svg +1 -0
  366. package/icons/material-symbols/squares-four.svg +1 -0
  367. package/icons/material-symbols/sticker.svg +1 -0
  368. package/icons/material-symbols/storefront.svg +1 -0
  369. package/icons/material-symbols/sun.svg +1 -0
  370. package/icons/material-symbols/swatches.svg +1 -0
  371. package/icons/material-symbols/tag.svg +1 -0
  372. package/icons/material-symbols/text-aa.svg +1 -0
  373. package/icons/material-symbols/text-align-left.svg +1 -0
  374. package/icons/material-symbols/text-b.svg +1 -0
  375. package/icons/material-symbols/text-h-one.svg +1 -0
  376. package/icons/material-symbols/text-h-three.svg +1 -0
  377. package/icons/material-symbols/text-h-two.svg +1 -0
  378. package/icons/material-symbols/text-italic.svg +1 -0
  379. package/icons/material-symbols/text-t.svg +1 -0
  380. package/icons/material-symbols/ticket.svg +1 -0
  381. package/icons/material-symbols/toggle-right.svg +1 -0
  382. package/icons/material-symbols/train.svg +1 -0
  383. package/icons/material-symbols/trash.svg +1 -0
  384. package/icons/material-symbols/upload-simple.svg +1 -0
  385. package/icons/material-symbols/user-circle.svg +1 -0
  386. package/icons/material-symbols/user.svg +1 -0
  387. package/icons/material-symbols/users-three.svg +1 -0
  388. package/icons/material-symbols/users.svg +1 -0
  389. package/icons/material-symbols/video-camera.svg +1 -0
  390. package/icons/material-symbols/warning-circle.svg +1 -0
  391. package/icons/material-symbols/warning.svg +1 -0
  392. package/icons/material-symbols/wave-sine.svg +1 -0
  393. package/icons/material-symbols/wheelchair.svg +1 -0
  394. package/icons/material-symbols/wrench.svg +1 -0
  395. package/icons/material-symbols/x.svg +1 -0
  396. package/icons/phosphor/LICENSE +21 -0
  397. package/icons/phosphor/address-book.svg +1 -0
  398. package/icons/phosphor/app-window.svg +1 -0
  399. package/icons/phosphor/arrow-counter-clockwise.svg +1 -0
  400. package/icons/phosphor/arrow-down.svg +1 -0
  401. package/icons/phosphor/arrow-left.svg +1 -0
  402. package/icons/phosphor/arrow-right.svg +1 -0
  403. package/icons/phosphor/arrow-square-out.svg +1 -0
  404. package/icons/phosphor/arrow-u-up-left.svg +1 -0
  405. package/icons/phosphor/arrow-u-up-right.svg +1 -0
  406. package/icons/phosphor/arrow-up-right.svg +1 -0
  407. package/icons/phosphor/arrow-up.svg +1 -0
  408. package/icons/phosphor/arrows-down-up.svg +1 -0
  409. package/icons/phosphor/arrows-in-line-horizontal.svg +1 -0
  410. package/icons/phosphor/arrows-out-line-horizontal.svg +1 -0
  411. package/icons/phosphor/article.svg +1 -0
  412. package/icons/phosphor/bank.svg +1 -0
  413. package/icons/phosphor/bell.svg +1 -0
  414. package/icons/phosphor/bicycle.svg +1 -0
  415. package/icons/phosphor/binoculars.svg +1 -0
  416. package/icons/phosphor/book-open-text.svg +1 -0
  417. package/icons/phosphor/book-open.svg +1 -0
  418. package/icons/phosphor/browser.svg +1 -0
  419. package/icons/phosphor/buildings.svg +1 -0
  420. package/icons/phosphor/bus.svg +1 -0
  421. package/icons/phosphor/calendar-blank.svg +1 -0
  422. package/icons/phosphor/calendar-check.svg +1 -0
  423. package/icons/phosphor/car.svg +1 -0
  424. package/icons/phosphor/cards.svg +1 -0
  425. package/icons/phosphor/caret-down.svg +1 -0
  426. package/icons/phosphor/caret-left.svg +1 -0
  427. package/icons/phosphor/caret-right.svg +1 -0
  428. package/icons/phosphor/caret-up.svg +1 -0
  429. package/icons/phosphor/certificate.svg +1 -0
  430. package/icons/phosphor/chart-line-up.svg +1 -0
  431. package/icons/phosphor/chats.svg +1 -0
  432. package/icons/phosphor/check-circle.svg +1 -0
  433. package/icons/phosphor/check.svg +1 -0
  434. package/icons/phosphor/circle-half.svg +1 -0
  435. package/icons/phosphor/circle.svg +1 -0
  436. package/icons/phosphor/clock-counter-clockwise.svg +1 -0
  437. package/icons/phosphor/clock.svg +1 -0
  438. package/icons/phosphor/code.svg +1 -0
  439. package/icons/phosphor/coffee.svg +1 -0
  440. package/icons/phosphor/columns.svg +1 -0
  441. package/icons/phosphor/cookie.svg +1 -0
  442. package/icons/phosphor/copy.svg +1 -0
  443. package/icons/phosphor/credit-card.svg +1 -0
  444. package/icons/phosphor/currency-circle-dollar.svg +1 -0
  445. package/icons/phosphor/cursor-click.svg +1 -0
  446. package/icons/phosphor/database.svg +1 -0
  447. package/icons/phosphor/desktop.svg +1 -0
  448. package/icons/phosphor/device-mobile.svg +1 -0
  449. package/icons/phosphor/door-open.svg +1 -0
  450. package/icons/phosphor/download-simple.svg +1 -0
  451. package/icons/phosphor/envelope-simple.svg +1 -0
  452. package/icons/phosphor/eye-slash.svg +1 -0
  453. package/icons/phosphor/eye.svg +1 -0
  454. package/icons/phosphor/file-pdf.svg +1 -0
  455. package/icons/phosphor/file-text.svg +1 -0
  456. package/icons/phosphor/file-video.svg +1 -0
  457. package/icons/phosphor/file-zip.svg +1 -0
  458. package/icons/phosphor/flower-lotus.svg +1 -0
  459. package/icons/phosphor/folder-simple.svg +1 -0
  460. package/icons/phosphor/funnel.svg +1 -0
  461. package/icons/phosphor/gauge.svg +1 -0
  462. package/icons/phosphor/gear.svg +1 -0
  463. package/icons/phosphor/github-logo.svg +1 -0
  464. package/icons/phosphor/globe.svg +1 -0
  465. package/icons/phosphor/google-logo.svg +1 -0
  466. package/icons/phosphor/graduation-cap.svg +1 -0
  467. package/icons/phosphor/hammer.svg +1 -0
  468. package/icons/phosphor/hand-heart.svg +1 -0
  469. package/icons/phosphor/handshake.svg +1 -0
  470. package/icons/phosphor/hard-drives.svg +1 -0
  471. package/icons/phosphor/heart.svg +1 -0
  472. package/icons/phosphor/house.svg +1 -0
  473. package/icons/phosphor/image-square.svg +1 -0
  474. package/icons/phosphor/image.svg +1 -0
  475. package/icons/phosphor/info.svg +1 -0
  476. package/icons/phosphor/keyboard.svg +1 -0
  477. package/icons/phosphor/leaf.svg +1 -0
  478. package/icons/phosphor/lifebuoy.svg +1 -0
  479. package/icons/phosphor/lightbulb.svg +1 -0
  480. package/icons/phosphor/link-break.svg +1 -0
  481. package/icons/phosphor/link-simple.svg +1 -0
  482. package/icons/phosphor/list-bullets.svg +1 -0
  483. package/icons/phosphor/list-checks.svg +1 -0
  484. package/icons/phosphor/list-numbers.svg +1 -0
  485. package/icons/phosphor/list.svg +1 -0
  486. package/icons/phosphor/lock-simple-open.svg +1 -0
  487. package/icons/phosphor/lock-simple.svg +1 -0
  488. package/icons/phosphor/magnifying-glass.svg +1 -0
  489. package/icons/phosphor/map-pin.svg +1 -0
  490. package/icons/phosphor/map-trifold.svg +1 -0
  491. package/icons/phosphor/medal.svg +1 -0
  492. package/icons/phosphor/megaphone.svg +1 -0
  493. package/icons/phosphor/minus.svg +1 -0
  494. package/icons/phosphor/moon.svg +1 -0
  495. package/icons/phosphor/newspaper.svg +1 -0
  496. package/icons/phosphor/pack.json +8 -0
  497. package/icons/phosphor/paint-brush-broad.svg +1 -0
  498. package/icons/phosphor/palette.svg +1 -0
  499. package/icons/phosphor/paper-plane-tilt.svg +1 -0
  500. package/icons/phosphor/pencil-simple.svg +1 -0
  501. package/icons/phosphor/phone.svg +1 -0
  502. package/icons/phosphor/play-fill.svg +1 -0
  503. package/icons/phosphor/plus.svg +1 -0
  504. package/icons/phosphor/prohibit.svg +1 -0
  505. package/icons/phosphor/puzzle-piece.svg +1 -0
  506. package/icons/phosphor/question.svg +1 -0
  507. package/icons/phosphor/quotes.svg +1 -0
  508. package/icons/phosphor/rows.svg +1 -0
  509. package/icons/phosphor/ruler.svg +1 -0
  510. package/icons/phosphor/scan.svg +1 -0
  511. package/icons/phosphor/shapes.svg +1 -0
  512. package/icons/phosphor/share-network.svg +1 -0
  513. package/icons/phosphor/shield-check.svg +1 -0
  514. package/icons/phosphor/sidebar-simple.svg +1 -0
  515. package/icons/phosphor/sign-out.svg +1 -0
  516. package/icons/phosphor/signpost.svg +1 -0
  517. package/icons/phosphor/sliders-horizontal.svg +1 -0
  518. package/icons/phosphor/smiley.svg +1 -0
  519. package/icons/phosphor/sparkle.svg +1 -0
  520. package/icons/phosphor/square-half-bottom.svg +1 -0
  521. package/icons/phosphor/square.svg +1 -0
  522. package/icons/phosphor/squares-four.svg +1 -0
  523. package/icons/phosphor/sticker.svg +1 -0
  524. package/icons/phosphor/storefront.svg +1 -0
  525. package/icons/phosphor/sun.svg +1 -0
  526. package/icons/phosphor/swatches.svg +1 -0
  527. package/icons/phosphor/tag.svg +1 -0
  528. package/icons/phosphor/text-aa.svg +1 -0
  529. package/icons/phosphor/text-align-left.svg +1 -0
  530. package/icons/phosphor/text-b.svg +1 -0
  531. package/icons/phosphor/text-h-one.svg +1 -0
  532. package/icons/phosphor/text-h-three.svg +1 -0
  533. package/icons/phosphor/text-h-two.svg +1 -0
  534. package/icons/phosphor/text-italic.svg +1 -0
  535. package/icons/phosphor/text-t.svg +1 -0
  536. package/icons/phosphor/ticket.svg +1 -0
  537. package/icons/phosphor/toggle-right.svg +1 -0
  538. package/icons/phosphor/train.svg +1 -0
  539. package/icons/phosphor/trash.svg +1 -0
  540. package/icons/phosphor/upload-simple.svg +1 -0
  541. package/icons/phosphor/user-circle.svg +1 -0
  542. package/icons/phosphor/user.svg +1 -0
  543. package/icons/phosphor/users-three.svg +1 -0
  544. package/icons/phosphor/users.svg +1 -0
  545. package/icons/phosphor/video-camera.svg +1 -0
  546. package/icons/phosphor/warning-circle.svg +1 -0
  547. package/icons/phosphor/warning.svg +1 -0
  548. package/icons/phosphor/wave-sine.svg +1 -0
  549. package/icons/phosphor/wheelchair.svg +1 -0
  550. package/icons/phosphor/wrench.svg +1 -0
  551. package/icons/phosphor/x.svg +1 -0
  552. package/lib/apps.d.mts +28 -0
  553. package/lib/apps.mjs +139 -0
  554. package/lib/blocks.d.mts +23 -0
  555. package/lib/blocks.mjs +90 -0
  556. package/lib/colors.ts +86 -0
  557. package/lib/contactBar.ts +18 -0
  558. package/lib/cookieCategories.d.mts +17 -0
  559. package/lib/cookieCategories.mjs +28 -0
  560. package/lib/crop.d.mts +34 -0
  561. package/lib/crop.mjs +83 -0
  562. package/lib/csp.d.mts +22 -0
  563. package/lib/csp.mjs +130 -0
  564. package/lib/cspSources.d.mts +24 -0
  565. package/lib/cspSources.mjs +60 -0
  566. package/lib/dynamic-colors.ts +426 -0
  567. package/lib/git.d.mts +24 -0
  568. package/lib/git.mjs +64 -0
  569. package/lib/hooks.ts +51 -0
  570. package/lib/inline.ts +20 -0
  571. package/lib/license-texts/Apache-2.0.txt +191 -0
  572. package/lib/license-texts/BSD-2-Clause.txt +24 -0
  573. package/lib/license-texts/BSD-3-Clause.txt +28 -0
  574. package/lib/license-texts/ISC.txt +15 -0
  575. package/lib/license-texts/MIT.txt +21 -0
  576. package/lib/licenses.d.mts +190 -0
  577. package/lib/licenses.mjs +683 -0
  578. package/lib/markdown.ts +42 -0
  579. package/lib/motion-schemes.d.mts +2 -0
  580. package/lib/motion-schemes.mjs +5 -0
  581. package/lib/motion.ts +131 -0
  582. package/lib/notice.d.mts +3 -0
  583. package/lib/notice.mjs +34 -0
  584. package/lib/scheduled.d.mts +15 -0
  585. package/lib/scheduled.mjs +37 -0
  586. package/lib/snackbar.ts +149 -0
  587. package/lib/status.d.mts +7 -0
  588. package/lib/status.mjs +18 -0
  589. package/lib/svg.d.mts +22 -0
  590. package/lib/svg.mjs +231 -0
  591. package/lib/text-colors.d.mts +11 -0
  592. package/lib/text-colors.mjs +43 -0
  593. package/lib/video-id.d.mts +14 -0
  594. package/lib/video-id.mjs +72 -0
  595. package/lib/video.d.mts +20 -0
  596. package/lib/video.mjs +91 -0
  597. package/package.json +131 -6
  598. package/pages/404.astro +10 -0
  599. package/pages/[...slug].astro +51 -0
  600. package/pages/_headers.txt +31 -0
  601. package/pages/blog/index.astro +10 -0
  602. package/pages/blog/kategori/[category].astro +18 -0
  603. package/pages/editor/code-images.json.js +25 -0
  604. package/pages/editor/index.astro +75 -0
  605. package/pages/editor/manifest.webmanifest.js +23 -0
  606. package/pages/editor/sw.js +33 -0
  607. package/pages/editor/sw.js.js +4 -0
  608. package/pages/editor/thumbnails.json.js +21 -0
  609. package/pages/editor/version.json.js +22 -0
  610. package/pages/headers.js +4 -0
  611. package/pages/index.astro +10 -0
  612. package/pages/ny.astro +6 -0
  613. package/pages/robots.txt.js +3 -0
  614. package/pages/rss.xml.js +20 -0
  615. package/scripts/app-screenshots.mjs +164 -0
  616. package/scripts/app-workers.mjs +6 -0
  617. package/scripts/bin.mjs +12 -0
  618. package/scripts/built-site.mjs +99 -0
  619. package/scripts/check-consent.mjs +113 -0
  620. package/scripts/check-content.mjs +149 -0
  621. package/scripts/check-demo.mjs +24 -0
  622. package/scripts/check-themes.mjs +45 -0
  623. package/scripts/check-worker.mjs +569 -0
  624. package/scripts/compare-html.mjs +81 -0
  625. package/scripts/demo-site.mjs +42 -0
  626. package/scripts/editor-icons.mjs +56 -0
  627. package/scripts/editor-roundtrip.mjs +134 -0
  628. package/scripts/editor-screens.mjs +101 -0
  629. package/scripts/git-env.mjs +9 -0
  630. package/scripts/icon-packs.mjs +151 -0
  631. package/scripts/link-counts.mjs +22 -0
  632. package/scripts/page-colors.mjs +42 -0
  633. package/scripts/releases.d.mts +10 -0
  634. package/scripts/releases.mjs +80 -0
  635. package/scripts/responsive-images.mjs +94 -0
  636. package/scripts/scheduled-list.mjs +15 -0
  637. package/scripts/theme-screenshots.mjs +88 -0
  638. package/scripts/types.mjs +21 -0
  639. package/scripts/video-thumbnails.mjs +89 -0
  640. package/scripts/worker-modules.mjs +34 -0
  641. package/site/appCookies.d.mts +9 -0
  642. package/site/appCookies.mjs +74 -0
  643. package/site/apps.ts +21 -0
  644. package/site/autolinks.d.mts +11 -0
  645. package/site/autolinks.mjs +200 -0
  646. package/site/blockScripts.ts +31 -0
  647. package/site/categories.ts +16 -0
  648. package/site/consent.ts +26 -0
  649. package/site/content.config.ts +84 -0
  650. package/site/content.ts +104 -0
  651. package/site/cookieList.ts +17 -0
  652. package/site/defineSite.d.mts +142 -0
  653. package/site/defineSite.mjs +136 -0
  654. package/site/directives/interact.d.mts +2 -0
  655. package/site/directives/interact.mjs +31 -0
  656. package/site/editor-version.d.mts +1 -0
  657. package/site/editor-version.mjs +26 -0
  658. package/site/fill.ts +13 -0
  659. package/site/format.ts +11 -0
  660. package/site/home.ts +66 -0
  661. package/site/iconPacks.d.mts +20 -0
  662. package/site/iconPacks.mjs +43 -0
  663. package/site/icons.d.mts +693 -0
  664. package/site/icons.mjs +605 -0
  665. package/site/images.d.mts +43 -0
  666. package/site/images.mjs +157 -0
  667. package/site/motion.ts +13 -0
  668. package/site/noindex.d.mts +6 -0
  669. package/site/noindex.mjs +21 -0
  670. package/site/pageColors.ts +110 -0
  671. package/site/popups.ts +98 -0
  672. package/site/schema.ts +189 -0
  673. package/site/sections.ts +3 -0
  674. package/site/seo.ts +28 -0
  675. package/site/site.ts +76 -0
  676. package/site/siteConfig.d.mts +1 -0
  677. package/site/siteConfig.mjs +8 -0
  678. package/site/siteCsp.d.mts +10 -0
  679. package/site/siteCsp.mjs +75 -0
  680. package/site/siteModules.d.mts +7 -0
  681. package/site/siteModules.mjs +27 -0
  682. package/site/standardPages.ts +59 -0
  683. package/site/testimonials.ts +48 -0
  684. package/site/text.ts +43 -0
  685. package/site/theme.d.mts +83 -0
  686. package/site/theme.mjs +472 -0
  687. package/site/tracking.client.ts +225 -0
  688. package/site/tracking.ts +9 -0
  689. package/src/ActionParts.tsx +205 -0
  690. package/src/Advanced.tsx +152 -0
  691. package/src/App.tsx +904 -0
  692. package/src/AppList.tsx +1345 -0
  693. package/src/BlockDialog.tsx +360 -0
  694. package/src/BlockParts.tsx +652 -0
  695. package/src/BlockSettings.tsx +533 -0
  696. package/src/BrandForm.tsx +164 -0
  697. package/src/Colors.tsx +476 -0
  698. package/src/CommunicationParts.tsx +199 -0
  699. package/src/CompanyForm.tsx +209 -0
  700. package/src/Consents.tsx +175 -0
  701. package/src/ContainmentParts.tsx +185 -0
  702. package/src/Crop.tsx +152 -0
  703. package/src/DataForm.tsx +434 -0
  704. package/src/Deleted.tsx +177 -0
  705. package/src/EntryForm.tsx +740 -0
  706. package/src/FileView.tsx +30 -0
  707. package/src/FormFrame.tsx +237 -0
  708. package/src/Keywords.tsx +225 -0
  709. package/src/Library.tsx +1563 -0
  710. package/src/Licenses.tsx +336 -0
  711. package/src/LinkSheet.tsx +138 -0
  712. package/src/MarkdownEditor.tsx +1075 -0
  713. package/src/Media.tsx +677 -0
  714. package/src/NavigationParts.tsx +350 -0
  715. package/src/NavigationRestParts.tsx +247 -0
  716. package/src/Notifications.tsx +59 -0
  717. package/src/PagePicker.tsx +136 -0
  718. package/src/Parts.tsx +991 -0
  719. package/src/PartsShown.tsx +168 -0
  720. package/src/PickerParts.tsx +183 -0
  721. package/src/Redirects.tsx +450 -0
  722. package/src/Screen.tsx +184 -0
  723. package/src/SectionsField.tsx +65 -0
  724. package/src/SelectionParts.tsx +134 -0
  725. package/src/SeoScores.tsx +184 -0
  726. package/src/Sheet.tsx +35 -0
  727. package/src/ThemeStore.tsx +376 -0
  728. package/src/Tokens.tsx +346 -0
  729. package/src/Versions.tsx +214 -0
  730. package/src/addresses.ts +75 -0
  731. package/src/allIcons.ts +7 -0
  732. package/src/appFiles.ts +29 -0
  733. package/src/apps.ts +281 -0
  734. package/src/ask.tsx +123 -0
  735. package/src/blocks.ts +251 -0
  736. package/src/brand.ts +153 -0
  737. package/src/cache.ts +42 -0
  738. package/src/check.ts +285 -0
  739. package/src/checkSite.ts +256 -0
  740. package/src/consents.ts +26 -0
  741. package/src/cookieList.ts +70 -0
  742. package/src/countries.ts +59 -0
  743. package/src/deploy.ts +155 -0
  744. package/src/editor.css +602 -0
  745. package/src/examples.ts +85 -0
  746. package/src/fields.tsx +942 -0
  747. package/src/frontmatter.d.mts +6 -0
  748. package/src/frontmatter.mjs +42 -0
  749. package/src/github.ts +376 -0
  750. package/src/i18n/da-DK/advanced.ts +28 -0
  751. package/src/i18n/da-DK/app.ts +71 -0
  752. package/src/i18n/da-DK/apps.ts +90 -0
  753. package/src/i18n/da-DK/blockSettings.ts +38 -0
  754. package/src/i18n/da-DK/blocks.ts +408 -0
  755. package/src/i18n/da-DK/check.ts +25 -0
  756. package/src/i18n/da-DK/colors.ts +64 -0
  757. package/src/i18n/da-DK/common.ts +118 -0
  758. package/src/i18n/da-DK/company.ts +24 -0
  759. package/src/i18n/da-DK/consents.ts +23 -0
  760. package/src/i18n/da-DK/forms.ts +116 -0
  761. package/src/i18n/da-DK/history.ts +35 -0
  762. package/src/i18n/da-DK/index.ts +49 -0
  763. package/src/i18n/da-DK/keywords.ts +19 -0
  764. package/src/i18n/da-DK/library.ts +135 -0
  765. package/src/i18n/da-DK/licenses.ts +25 -0
  766. package/src/i18n/da-DK/media.ts +79 -0
  767. package/src/i18n/da-DK/parts.ts +696 -0
  768. package/src/i18n/da-DK/preview.ts +13 -0
  769. package/src/i18n/da-DK/redirects.ts +51 -0
  770. package/src/i18n/da-DK/seo.ts +20 -0
  771. package/src/i18n/da-DK/text.ts +35 -0
  772. package/src/i18n/da-DK/theme.ts +41 -0
  773. package/src/i18n/en-US/advanced.ts +32 -0
  774. package/src/i18n/en-US/app.ts +84 -0
  775. package/src/i18n/en-US/apps.ts +134 -0
  776. package/src/i18n/en-US/blockSettings.ts +48 -0
  777. package/src/i18n/en-US/blocks.ts +441 -0
  778. package/src/i18n/en-US/check.ts +31 -0
  779. package/src/i18n/en-US/colors.ts +82 -0
  780. package/src/i18n/en-US/common.ts +144 -0
  781. package/src/i18n/en-US/company.ts +25 -0
  782. package/src/i18n/en-US/consents.ts +25 -0
  783. package/src/i18n/en-US/forms.ts +145 -0
  784. package/src/i18n/en-US/history.ts +43 -0
  785. package/src/i18n/en-US/index.ts +51 -0
  786. package/src/i18n/en-US/keywords.ts +24 -0
  787. package/src/i18n/en-US/library.ts +176 -0
  788. package/src/i18n/en-US/licenses.ts +36 -0
  789. package/src/i18n/en-US/media.ts +80 -0
  790. package/src/i18n/en-US/parts.ts +718 -0
  791. package/src/i18n/en-US/preview.ts +15 -0
  792. package/src/i18n/en-US/redirects.ts +66 -0
  793. package/src/i18n/en-US/seo.ts +26 -0
  794. package/src/i18n/en-US/text.ts +41 -0
  795. package/src/i18n/en-US/theme.ts +41 -0
  796. package/src/i18n/index.ts +124 -0
  797. package/src/icon.tsx +160 -0
  798. package/src/iconPacks.ts +89 -0
  799. package/src/images.ts +163 -0
  800. package/src/licenses.ts +79 -0
  801. package/src/markdown.d.mts +12 -0
  802. package/src/markdown.mjs +204 -0
  803. package/src/nav.ts +68 -0
  804. package/src/notFound.ts +28 -0
  805. package/src/notifications.ts +223 -0
  806. package/src/pagePreview.tsx +204 -0
  807. package/src/r2Caching.ts +55 -0
  808. package/src/redirects.ts +149 -0
  809. package/src/registry.tsx +188 -0
  810. package/src/schema.ts +417 -0
  811. package/src/sections.d.mts +45 -0
  812. package/src/sections.mjs +303 -0
  813. package/src/seo.ts +333 -0
  814. package/src/shared.ts +44 -0
  815. package/src/sidebar.ts +31 -0
  816. package/src/site.ts +317 -0
  817. package/src/standard/Block.astro +19 -0
  818. package/src/standard/BlockSite.astro +18 -0
  819. package/src/standard/accordion/index.tsx +47 -0
  820. package/src/standard/accordion/load.ts +23 -0
  821. package/src/standard/accordion/shared.ts +15 -0
  822. package/src/standard/accordion/view.ts +32 -0
  823. package/src/standard/buttons/fields.ts +98 -0
  824. package/src/standard/buttons/index.tsx +23 -0
  825. package/src/standard/buttons/load.ts +34 -0
  826. package/src/standard/buttons/shared.ts +12 -0
  827. package/src/standard/buttons/view.ts +30 -0
  828. package/src/standard/cards/index.tsx +71 -0
  829. package/src/standard/cards/load.ts +17 -0
  830. package/src/standard/cards/shared.ts +27 -0
  831. package/src/standard/cards/view.ts +29 -0
  832. package/src/standard/checklist/index.tsx +27 -0
  833. package/src/standard/checklist/load.ts +5 -0
  834. package/src/standard/checklist/view.ts +13 -0
  835. package/src/standard/cookies/index.tsx +17 -0
  836. package/src/standard/cookies/list.ts +56 -0
  837. package/src/standard/cookies/load.ts +11 -0
  838. package/src/standard/cookies/shared.ts +20 -0
  839. package/src/standard/cookies/view.ts +28 -0
  840. package/src/standard/faq/index.tsx +29 -0
  841. package/src/standard/faq/load.ts +14 -0
  842. package/src/standard/faq/view.ts +21 -0
  843. package/src/standard/head.ts +23 -0
  844. package/src/standard/hero/fields.ts +48 -0
  845. package/src/standard/hero/index.tsx +57 -0
  846. package/src/standard/hero/load.ts +23 -0
  847. package/src/standard/hero/view.ts +38 -0
  848. package/src/standard/links/index.tsx +41 -0
  849. package/src/standard/links/load.ts +17 -0
  850. package/src/standard/links/view.ts +25 -0
  851. package/src/standard/list/index.tsx +50 -0
  852. package/src/standard/list/load.ts +5 -0
  853. package/src/standard/list/view.ts +18 -0
  854. package/src/standard/load.ts +7 -0
  855. package/src/standard/notice/index.tsx +30 -0
  856. package/src/standard/notice/load.ts +16 -0
  857. package/src/standard/notice/view.ts +10 -0
  858. package/src/standard/points/index.tsx +42 -0
  859. package/src/standard/points/load.ts +5 -0
  860. package/src/standard/points/view.ts +12 -0
  861. package/src/standard/posts/index.tsx +132 -0
  862. package/src/standard/posts/load.ts +36 -0
  863. package/src/standard/posts/shared.ts +21 -0
  864. package/src/standard/posts/view.ts +63 -0
  865. package/src/standard/pricecards/index.tsx +57 -0
  866. package/src/standard/pricecards/load.ts +11 -0
  867. package/src/standard/pricecards/view.ts +50 -0
  868. package/src/standard/sharedTexts.ts +6 -0
  869. package/src/standard/site.ts +124 -0
  870. package/src/standard/siteLooks.ts +19 -0
  871. package/src/standard/sites.ts +13 -0
  872. package/src/standard/steps/index.tsx +28 -0
  873. package/src/standard/steps/load.ts +5 -0
  874. package/src/standard/steps/view.ts +11 -0
  875. package/src/standard/testimonials/index.tsx +87 -0
  876. package/src/standard/testimonials/list.ts +43 -0
  877. package/src/standard/testimonials/load.ts +58 -0
  878. package/src/standard/testimonials/shared.ts +13 -0
  879. package/src/standard/testimonials/view.ts +76 -0
  880. package/src/standard/themeBlocks.ts +13 -0
  881. package/src/standard/video/index.tsx +23 -0
  882. package/src/standard/video/load.ts +10 -0
  883. package/src/standard/video/view.ts +42 -0
  884. package/src/standard/view.tsx +105 -0
  885. package/src/standard/views.ts +37 -0
  886. package/src/store.ts +499 -0
  887. package/src/theme.ts +90 -0
  888. package/src/themeUi.ts +97 -0
  889. package/src/themes.ts +91 -0
  890. package/src/ui.tsx +3402 -0
  891. package/src/virtual.d.ts +5 -0
  892. package/src/when.ts +22 -0
  893. package/starter/.githooks/pre-push +4 -0
  894. package/starter/.nvmrc +1 -0
  895. package/starter/.prettierignore +11 -0
  896. package/starter/.prettierrc.json +7 -0
  897. package/starter/README.md +26 -0
  898. package/starter/astro.config.mjs +5 -0
  899. package/starter/package.json +67 -0
  900. package/starter/public/_redirects +1 -0
  901. package/starter/public/apple-touch-icon.png +0 -0
  902. package/starter/public/editor/apple-touch-icon.png +0 -0
  903. package/starter/public/editor/icon-192.png +0 -0
  904. package/starter/public/editor/icon-512.png +0 -0
  905. package/starter/public/editor/icon-maskable-512.png +0 -0
  906. package/starter/public/favicon.ico +0 -0
  907. package/starter/public/favicon.svg +1 -0
  908. package/starter/public/logo.svg +1 -0
  909. package/starter/src/content/pages/blog.md +8 -0
  910. package/starter/src/content/pages/forside.md +9 -0
  911. package/starter/src/content/pages/kontakt.md +7 -0
  912. package/starter/src/content/pages/privatlivspolitik.md +9 -0
  913. package/starter/src/content/pages/siden-findes-ikke.md +7 -0
  914. package/starter/src/content.config.ts +2 -0
  915. package/starter/src/data/apps.json +1 -0
  916. package/starter/src/data/block-settings.json +3 -0
  917. package/starter/src/data/blocks.json +1 -0
  918. package/starter/src/data/booking.json +6 -0
  919. package/starter/src/data/colors.json +70 -0
  920. package/starter/src/data/contact.json +8 -0
  921. package/starter/src/data/cookies.json +21 -0
  922. package/starter/src/data/csp.json +1 -0
  923. package/starter/src/data/footer.json +15 -0
  924. package/starter/src/data/internal-links.json +4 -0
  925. package/starter/src/data/menu.json +4 -0
  926. package/starter/src/data/mobile-menu.json +4 -0
  927. package/starter/src/data/old-addresses.json +1 -0
  928. package/starter/src/data/redirects.json +1 -0
  929. package/starter/src/data/standard-pages.json +8 -0
  930. package/starter/src/data/theme.json +1 -0
  931. package/starter/src/env.d.ts +2 -0
  932. package/starter/src/site/config.ts +13 -0
  933. package/starter/src/site/worker.js +14 -0
  934. package/starter/tsconfig.json +17 -0
  935. package/starter/worker/apps.js +6 -0
  936. package/starter/wrangler.jsonc +39 -0
  937. package/theme/LICENSE +21 -0
  938. package/theme/blocks/ArrowLink.tsx +14 -0
  939. package/theme/blocks/BlockHead.tsx +26 -0
  940. package/theme/blocks/Panel.tsx +32 -0
  941. package/theme/blocks/Person.tsx +26 -0
  942. package/theme/blocks/accordion/PageRow.tsx +25 -0
  943. package/theme/blocks/accordion/View.tsx +17 -0
  944. package/theme/blocks/buttons/Buttons.tsx +28 -0
  945. package/theme/blocks/buttons/View.tsx +12 -0
  946. package/theme/blocks/cards/LinkCards.tsx +72 -0
  947. package/theme/blocks/cards/View.tsx +20 -0
  948. package/theme/blocks/checklist/Checks.tsx +15 -0
  949. package/theme/blocks/checklist/View.tsx +14 -0
  950. package/theme/blocks/cookies/View.tsx +36 -0
  951. package/theme/blocks/faq/Accordion.tsx +34 -0
  952. package/theme/blocks/faq/View.tsx +16 -0
  953. package/theme/blocks/faq/script.ts +56 -0
  954. package/theme/blocks/hero/View.tsx +44 -0
  955. package/theme/blocks/links/LinkGroups.tsx +24 -0
  956. package/theme/blocks/links/View.tsx +17 -0
  957. package/theme/blocks/list/Rows.tsx +24 -0
  958. package/theme/blocks/list/View.tsx +14 -0
  959. package/theme/blocks/notice/View.tsx +13 -0
  960. package/theme/blocks/points/PointList.tsx +34 -0
  961. package/theme/blocks/points/View.tsx +14 -0
  962. package/theme/blocks/posts/View.tsx +26 -0
  963. package/theme/blocks/pricecards/View.tsx +40 -0
  964. package/theme/blocks/steps/StepList.tsx +23 -0
  965. package/theme/blocks/steps/View.tsx +14 -0
  966. package/theme/blocks/testimonials/List.tsx +46 -0
  967. package/theme/blocks/testimonials/Slider.tsx +44 -0
  968. package/theme/blocks/testimonials/Testimonial.tsx +124 -0
  969. package/theme/blocks/testimonials/View.tsx +21 -0
  970. package/theme/blocks/testimonials/script.ts +52 -0
  971. package/theme/blocks/video/View.tsx +41 -0
  972. package/theme/components/ActionBar.astro +63 -0
  973. package/theme/components/AppSlot.astro +21 -0
  974. package/theme/components/ArrowLink.tsx +21 -0
  975. package/theme/components/BlogIndex.astro +48 -0
  976. package/theme/components/BlogPosts.astro +18 -0
  977. package/theme/components/BlogPosts.tsx +45 -0
  978. package/theme/components/BookButton.tsx +22 -0
  979. package/theme/components/Container.astro +11 -0
  980. package/theme/components/CookieConsent.astro +121 -0
  981. package/theme/components/EndBlock.astro +21 -0
  982. package/theme/components/FontPreload.astro +6 -0
  983. package/theme/components/Footer.astro +85 -0
  984. package/theme/components/Header.astro +186 -0
  985. package/theme/components/Icon.astro +16 -0
  986. package/theme/components/Img.astro +29 -0
  987. package/theme/components/Img.tsx +33 -0
  988. package/theme/components/Inline.astro +11 -0
  989. package/theme/components/Logo.astro +23 -0
  990. package/theme/components/LogoMark.astro +10 -0
  991. package/theme/components/Markdown.astro +17 -0
  992. package/theme/components/MobileMenu.tsx +224 -0
  993. package/theme/components/MobileNav.astro +33 -0
  994. package/theme/components/PageHeader.astro +22 -0
  995. package/theme/components/PageSection.astro +24 -0
  996. package/theme/components/PhoneButton.tsx +11 -0
  997. package/theme/components/Popup.astro +145 -0
  998. package/theme/components/PopupButton.tsx +24 -0
  999. package/theme/components/PopupView.tsx +48 -0
  1000. package/theme/components/PostList.tsx +58 -0
  1001. package/theme/components/PostMeta.tsx +17 -0
  1002. package/theme/components/RichText.astro +28 -0
  1003. package/theme/components/Section.astro +29 -0
  1004. package/theme/components/SectionHeading.tsx +8 -0
  1005. package/theme/components/TextSection.astro +16 -0
  1006. package/theme/components/home/PageBlock.astro +19 -0
  1007. package/theme/components/home/Text.astro +13 -0
  1008. package/theme/components/loadPopup.ts +22 -0
  1009. package/theme/components/loadPosts.ts +16 -0
  1010. package/theme/components/posts.ts +15 -0
  1011. package/theme/editor/looks.mjs +7 -0
  1012. package/theme/editor/previews.tsx +56 -0
  1013. package/theme/layouts/Base.astro +273 -0
  1014. package/theme/layouts/PageLayout.astro +126 -0
  1015. package/theme/layouts/PostLayout.astro +137 -0
  1016. package/theme/screenshots/1-front.jpg +0 -0
  1017. package/theme/screenshots/2-front-dark.jpg +0 -0
  1018. package/theme/screenshots/3-blog.jpg +0 -0
  1019. package/theme/screenshots/4-contact.jpg +0 -0
  1020. package/theme/scripts/motion.ts +29 -0
  1021. package/theme/styles/base.css +320 -0
  1022. package/theme/styles/blocks.css +1808 -0
  1023. package/theme/styles/components.css +206 -0
  1024. package/theme/styles/global.css +1302 -0
  1025. package/theme/styles/ui.css +6544 -0
  1026. package/theme/templates/Blog.astro +38 -0
  1027. package/theme/templates/Category.astro +19 -0
  1028. package/theme/templates/Home.astro +36 -0
  1029. package/theme/templates/New.astro +68 -0
  1030. package/theme/templates/NotFound.astro +117 -0
  1031. package/theme/theme.json +28 -0
  1032. package/theme/ui/Badge.tsx +45 -0
  1033. package/theme/ui/BottomSheet.tsx +148 -0
  1034. package/theme/ui/Button.tsx +79 -0
  1035. package/theme/ui/ButtonGroup.tsx +31 -0
  1036. package/theme/ui/Card.tsx +45 -0
  1037. package/theme/ui/Carousel.tsx +124 -0
  1038. package/theme/ui/Checkbox.tsx +59 -0
  1039. package/theme/ui/Chip.tsx +101 -0
  1040. package/theme/ui/ChipSet.tsx +27 -0
  1041. package/theme/ui/DatePicker.tsx +149 -0
  1042. package/theme/ui/DatePickerContainer.tsx +316 -0
  1043. package/theme/ui/Dialog.tsx +100 -0
  1044. package/theme/ui/Divider.tsx +27 -0
  1045. package/theme/ui/ExposedDropdownMenu.tsx +77 -0
  1046. package/theme/ui/Fab.tsx +88 -0
  1047. package/theme/ui/FabMenu.tsx +67 -0
  1048. package/theme/ui/IconButton.tsx +78 -0
  1049. package/theme/ui/List.tsx +287 -0
  1050. package/theme/ui/LoadingIndicator.tsx +29 -0
  1051. package/theme/ui/Menu.tsx +278 -0
  1052. package/theme/ui/NavigationBar.tsx +62 -0
  1053. package/theme/ui/NavigationDrawer.tsx +80 -0
  1054. package/theme/ui/NavigationRail.tsx +154 -0
  1055. package/theme/ui/ProgressIndicator.tsx +162 -0
  1056. package/theme/ui/RadioButton.tsx +50 -0
  1057. package/theme/ui/Search.tsx +129 -0
  1058. package/theme/ui/SegmentedButton.tsx +49 -0
  1059. package/theme/ui/SideSheet.tsx +54 -0
  1060. package/theme/ui/Slider.tsx +155 -0
  1061. package/theme/ui/Snackbar.tsx +67 -0
  1062. package/theme/ui/SplitButton.tsx +67 -0
  1063. package/theme/ui/Switch.tsx +60 -0
  1064. package/theme/ui/Tabs.tsx +114 -0
  1065. package/theme/ui/TextField.tsx +278 -0
  1066. package/theme/ui/TimePicker.tsx +128 -0
  1067. package/theme/ui/TimePickerContainer.tsx +234 -0
  1068. package/theme/ui/Toolbar.tsx +67 -0
  1069. package/theme/ui/Tooltip.tsx +58 -0
  1070. package/theme/ui/TopAppBar.tsx +77 -0
  1071. package/theme/ui/bottomSheet.script.ts +8 -0
  1072. package/theme/ui/bottomSheet.ts +211 -0
  1073. package/theme/ui/carousel.script.ts +28 -0
  1074. package/theme/ui/carouselLayout.ts +229 -0
  1075. package/theme/ui/checkbox.script.ts +8 -0
  1076. package/theme/ui/datePicker.script.ts +302 -0
  1077. package/theme/ui/dates.ts +185 -0
  1078. package/theme/ui/dialog.ts +48 -0
  1079. package/theme/ui/dropdownMenu.script.ts +166 -0
  1080. package/theme/ui/fabMenu.script.ts +57 -0
  1081. package/theme/ui/navigationRail.script.ts +47 -0
  1082. package/theme/ui/ripple.script.ts +159 -0
  1083. package/theme/ui/search.script.ts +92 -0
  1084. package/theme/ui/sideSheet.ts +48 -0
  1085. package/theme/ui/slider.script.ts +12 -0
  1086. package/theme/ui/sliderUpdate.ts +52 -0
  1087. package/theme/ui/snackbar.script.ts +16 -0
  1088. package/theme/ui/splitButton.script.ts +63 -0
  1089. package/theme/ui/tabs.script.ts +48 -0
  1090. package/theme/ui/textField.script.ts +17 -0
  1091. package/theme/ui/textFieldUpdate.ts +44 -0
  1092. package/theme/ui/timePicker.script.ts +248 -0
  1093. package/theme/ui/toolbar.script.ts +43 -0
  1094. package/theme/ui/tooltip.script.ts +68 -0
  1095. package/worker/consent.js +141 -0
  1096. package/worker/editor-github.js +167 -0
  1097. package/worker/editor-host.js +74 -0
  1098. package/worker/files.js +401 -0
  1099. package/worker/github.js +36 -0
  1100. package/worker/index.js +184 -0
  1101. package/worker/not-found.js +97 -0
  1102. package/worker/oauth.js +95 -0
  1103. package/worker/publish.js +52 -0
  1104. package/worker/push.js +270 -0
  1105. package/worker/r2-status.js +50 -0
  1106. package/worker/session.js +58 -0
@@ -0,0 +1,1345 @@
1
+ // "Indstillinger" › "Apps" (#apps): the apps installed (src/apps/, apps.ts: the site's own and those from the app store,
2
+ // with one from the store not yet built as "Installeres …"), each with who made it under its name and, from the store,
3
+ // that it has a newer version, with search (by name, who made it and what it does), the order (A–Å, Å–A, kept in this
4
+ // browser) and headings by the first letter, as the lists under "Indhold" have. "Indstillinger" › "Appstore"
5
+ // (#indstillinger/appstore): all apps, those in the app store (store.ts) and the site's own, installed or not, with search (by name, what they
6
+ // do, who made them and the words they are found by), the same order and a funnel by category. An app's page
7
+ // (#apps/<app>) is the same for every app and without examples: its name, who made it, whether it is installed, its
8
+ // version (from the store) and what it does, "Installer" (or "Opdater til …"), its pictures (each opening it whole),
9
+ // what it does more fully ("Om appen"), a row to its settings (installed: on the screen under "Indstillinger" with its
10
+ // kind, "Analytics" or "Promovering", where their fields are saved with "Gem ændringer" in their file,
11
+ // config.apps.settings; its page only installs it), what it adds (its blocks, the lists it brings: "Priser", each opening
12
+ // its list, and its kinds of the site's lists: a Mailchimp popup; installed, each with its switch at the end of its row
13
+ // and opening a sheet with where it is used and the same switch, as under "Blokindstillinger", and a kind's button to a
14
+ // new one, while it is on), what it needs of the site ("Rettigheder", needsOf in store.ts: its code on every page,
15
+ // the addresses it loads from and sends forms to, its part of the Worker, a step in the build), the cookies it sets
16
+ // (and whether the cookie banner asks first), who made it (a row to their page, #apps/udvikler/<name>, with all their
17
+ // apps), and "Afinstaller"; installing, updating and uninstalling are each saved at once in its own commit,
18
+ // as a block turned on or off under "Blokindstillinger". One of the site's own is installed or uninstalled in the site's
19
+ // file of apps; one from the store is copied into the site, or deleted from it, with it (appChanges in store.ts), and
20
+ // its page before it is installed is drawn from the store's catalog (StoreAppPage). Installed, its blocks and kinds are
21
+ // under "Blokindstillinger" (under its name), its blocks under "Indsæt" and its kinds under "+" of their list. An app in
22
+ // use (one of its blocks on a page, an entry of its kind or in its list) cannot be uninstalled, as a block on a page
23
+ // cannot be turned off; its page says where it is used, so uninstalling never changes the site.
24
+ import { useEffect, useRef, useState, type ReactNode } from 'react';
25
+ import {
26
+ ArrowsDownUp,
27
+ Bell,
28
+ CheckCircle,
29
+ Code,
30
+ Cookie,
31
+ Gear,
32
+ Globe,
33
+ Hammer,
34
+ HardDrives,
35
+ Palette,
36
+ PaperPlaneTilt,
37
+ PuzzlePiece,
38
+ ShieldCheck,
39
+ Swatches,
40
+ UserCircle,
41
+ type Icon,
42
+ } from '@phosphor-icons/react';
43
+ import Screen from './Screen';
44
+ import {
45
+ Button,
46
+ InputChip,
47
+ Choice,
48
+ EmptyNote,
49
+ Filters,
50
+ Group,
51
+ GroupTitle,
52
+ ImageCarousel,
53
+ LeadingIcon,
54
+ LinkButton,
55
+ listClass,
56
+ SupportingText,
57
+ SearchRow,
58
+ BottomSheet,
59
+ SheetBody,
60
+ SheetTitle,
61
+ Switch,
62
+ SwitchRow,
63
+ useKept,
64
+ ListItem,
65
+ } from './ui';
66
+ import { BlockSwitch, blockSwitchTexts, KindSwitch, kindSwitchTexts, pagesWith, useOnOff } from './BlockSettings';
67
+ import { ConflictError, type Change, type RepoFile } from './github';
68
+ import { categoryOf } from '../lib/cookieCategories.mjs';
69
+ import { cookieChanges, inCookieList } from './cookieList';
70
+ import type { Env } from './App';
71
+ import { ask, choose } from './ask';
72
+ import { installedApps, kindKey, settingsFile, type SiteApp } from './apps';
73
+ import { AnyIcon } from './icon';
74
+ import {
75
+ appCategories,
76
+ appChanges,
77
+ appFolder,
78
+ breaking,
79
+ changedFiles,
80
+ notesSince,
81
+ recordIn,
82
+ DEVELOPER_ROUTE,
83
+ hasStore,
84
+ newer,
85
+ STORE_ROUTE,
86
+ STORE_THEMES,
87
+ type AppCategory,
88
+ type AppCookie,
89
+ type AppNeeds,
90
+ type ShownApp,
91
+ type StoreAdd,
92
+ type StoreRecord,
93
+ } from './store';
94
+ import { apps, offBlocks, siteBlocks } from './registry';
95
+ import { asWritten, backFrom, collections, companyParts, partScreens } from './site';
96
+ import { byText, slugify, type Part } from './schema';
97
+ import { headed, letterOf } from './Sheet';
98
+ import { config } from '@site/editor';
99
+ import { allThemes, ThemeRow, themeInstalled, useThemeCatalog } from './ThemeStore';
100
+ import { siteT, siteText, t } from './i18n';
101
+ import { OwnerLicenses, StoreLicenseRows } from './Licenses';
102
+ import { appType, setOn, told, useNotificationChoices } from './notifications';
103
+ import { kindAndName } from './deploy';
104
+
105
+ export const appHref = (app: SiteApp) => `#apps/${app.id}`;
106
+
107
+ /** The screen with an app's settings, by its kind, if any: one under "Indstillinger" ("Analytics", partScreens) or a
108
+ * part of "Virksomheden" with parts ("Betaling", companyParts); its address and its name. */
109
+ export function settingsHome(app: SiteApp): { route: string; label: string } | undefined {
110
+ const kind = app.category;
111
+ if (!kind) return undefined;
112
+ const screen = partScreens.find((s) => s.apps?.includes(kind));
113
+ if (screen) return { route: screen.id, label: screen.label };
114
+ const company = companyParts.find((p) => p.parts && p.apps?.includes(kind));
115
+ return company && { route: `virksomheden/${company.id}`, label: company.label };
116
+ }
117
+
118
+ /** Where an app's settings are: on the screen with its kind (#maaling/google-analytics, #virksomheden/betaling/mobilepay),
119
+ * else on a screen of their own (#apps/<id>/indstillinger); null without settings. */
120
+ export function settingsRoute(app: SiteApp): string | null {
121
+ if (!app.settings) return null;
122
+ const home = settingsHome(app);
123
+ return home ? `${home.route}/${app.id}` : `apps/${app.id}/indstillinger`;
124
+ }
125
+
126
+ /** An app's settings as a part, a form of its own (CompanyForm.tsx): its fields in its file, under its name, going back
127
+ * to the screen with its category, or to its page. */
128
+ export function appSettingsPart(app: SiteApp): Part | null {
129
+ if (!app.settings) return null;
130
+ const home = settingsHome(app);
131
+ return {
132
+ id: app.id,
133
+ label: app.label,
134
+ icon: app.icon,
135
+ file: settingsFile(app.id),
136
+ fields: app.settings.fields,
137
+ defaults: app.settings.values,
138
+ commit: siteT.apps.settingsOf(asWritten(app).label),
139
+ back: home ? { href: `#${home.route}`, label: home.label } : { href: appHref(app), label: app.label },
140
+ };
141
+ }
142
+
143
+ /** The settings of the apps installed of a screen's kinds (PartScreen.apps, or Part.apps of a part with parts), after
144
+ * its own parts. */
145
+ export function appPartsIn(screen: { apps?: AppCategory[] }, files: RepoFile[]): Part[] {
146
+ if (!screen.apps) return [];
147
+ const installed = installedApps(files);
148
+ return apps
149
+ .filter((a) => !!a.category && screen.apps!.includes(a.category) && installed.includes(a.id))
150
+ .flatMap((a) => appSettingsPart(a) ?? [])
151
+ .sort((a, b) => byText(a.label, b.label));
152
+ }
153
+
154
+ /** The entries of an app's kind in a list (a Mailchimp pop-up), by their title, each opening it. */
155
+ export function entriesOf(app: SiteApp, list: string, files: RepoFile[]): { href: string; title: string }[] {
156
+ const c = collections.find((x) => x.id === list);
157
+ if (c?.kind !== 'folder' || !c.kinds) return [];
158
+ return files
159
+ .filter((f) => f.path.startsWith(`${c.folder}/`) && f.path.endsWith('.json'))
160
+ .flatMap((f) => {
161
+ try {
162
+ const data = JSON.parse(f.text) as Record<string, unknown>;
163
+ return data[c.kinds!] === app.id ? [{ href: `#${f.path}`, title: c.summary(data) || f.path }] : [];
164
+ } catch {
165
+ return [];
166
+ }
167
+ });
168
+ }
169
+
170
+ /** The lists an app brings ("Priser"), each with how many entries it has, opening it. */
171
+ export const listsOf = (app: SiteApp, files: RepoFile[]) =>
172
+ (app.lists ?? []).map((l) => ({
173
+ key: l.id,
174
+ icon: l.icon,
175
+ label: l.label,
176
+ href: `#liste/${l.id}`,
177
+ count: files.filter((f) => f.path.startsWith(`${l.folder}/`) && f.path.endsWith('.json')).length,
178
+ }));
179
+
180
+ /** Where an app is in use: the pages and posts with one of its blocks (also a hidden one), the entries
181
+ * of its kinds (a Mailchimp popup), by their title, and the lists it brings that have entries ("Priser"), each
182
+ * with the icon of what is used there (the first of its blocks on a page) and opening it. */
183
+ export function usesOf(app: SiteApp, files: RepoFile[]): { href: string; title: string; icon: Icon }[] {
184
+ const pages = new Map<string, { title: string; icon: Icon }>();
185
+ for (const b of siteBlocks.filter((x) => x.app === app.id))
186
+ for (const p of pagesWith(b.type, files)) if (!pages.has(p.path)) pages.set(p.path, { title: p.title, icon: b.icon });
187
+ const entries = Object.entries(app.kinds ?? {}).flatMap(([list, kind]) =>
188
+ entriesOf(app, list, files).map((e) => ({ ...e, icon: kind.icon })),
189
+ );
190
+ const lists = listsOf(app, files).flatMap((l) => (l.count ? [{ href: l.href, title: l.label, icon: l.icon }] : []));
191
+ return [...[...pages].map(([path, p]) => ({ href: `#${path}`, ...p })), ...entries, ...lists];
192
+ }
193
+
194
+ /** An app's kinds of the site's lists (a pop-up), each by its own name ("Mailchimp popup", "Calendly bookingark") and
195
+ * opening the list, with the list's id, its name for one and for more. */
196
+ export function kindsOf(app: SiteApp) {
197
+ return Object.entries(app.kinds ?? {}).flatMap(([list, kind]) => {
198
+ const c = collections.find((x) => x.id === list);
199
+ return c
200
+ ? [
201
+ {
202
+ key: list,
203
+ icon: kind.icon,
204
+ label: kind.label,
205
+ hint: kind.hint,
206
+ href: `#liste/${c.id}`,
207
+ one: c.singular,
208
+ more: c.label,
209
+ create: c.create,
210
+ },
211
+ ]
212
+ : [];
213
+ });
214
+ }
215
+
216
+ const statusOf = (installed: boolean) => (installed ? t.apps.installed : t.apps.notInstalled);
217
+
218
+ type Sort = 'az' | 'za';
219
+ const SORTS: Sort[] = ['az', 'za'];
220
+ // The order picked last time, in this browser (the same for the apps and the store).
221
+ const SORT_KEY = 'editor-sort-apps';
222
+
223
+ /** The order of a list of apps, A–Å or Å–A, kept in this browser. */
224
+ function useSort(): [Sort, (next: Sort) => void] {
225
+ const [sort, setSort] = useState<Sort>(() => {
226
+ try {
227
+ const saved = localStorage.getItem(SORT_KEY);
228
+ if (SORTS.some((s) => s === saved)) return saved as Sort;
229
+ } catch {}
230
+ return 'az';
231
+ });
232
+ function pickSort(next: Sort) {
233
+ setSort(next);
234
+ try {
235
+ localStorage.setItem(SORT_KEY, next);
236
+ } catch {}
237
+ }
238
+ return [sort, pickSort];
239
+ }
240
+
241
+ const sortChoice = (sort: Sort, pickSort: (next: Sort) => void) => (
242
+ <Choice
243
+ title={t.library.sort}
244
+ icon={ArrowsDownUp}
245
+ value={sort}
246
+ options={SORTS.map((value) => ({ value, label: t.library.sorts[value] }))}
247
+ onChange={pickSort}
248
+ />
249
+ );
250
+
251
+ /** The store's catalog (store.ts) for a screen: null while it is read, 'failed' when it could not be; `skip`: not read
252
+ * (the page of one of the site's own apps). */
253
+ function useCatalog(env: Env, skip = false): ShownApp[] | null | 'failed' {
254
+ const [catalog, setCatalog] = useState<ShownApp[] | null | 'failed'>(null);
255
+ useEffect(() => {
256
+ if (skip) return;
257
+ let on = true;
258
+ env.store.catalog().then(
259
+ (apps) => on && setCatalog(apps),
260
+ () => on && setCatalog('failed'),
261
+ );
262
+ return () => {
263
+ on = false;
264
+ };
265
+ }, [skip]);
266
+ return catalog;
267
+ }
268
+
269
+ // The apps updated while the editor is open, by their id, to their version: their page and the list say so until the
270
+ // site's build has it (and the editor reloads with it, deploy.ts keepCurrent).
271
+ const updated = new Map<string, string>();
272
+ const versionOf = (app: SiteApp) => updated.get(app.id) ?? app.store?.version ?? '';
273
+
274
+ /** The newer version of an app from the store, when the store has one. */
275
+ const updateOf = (app: SiteApp, catalog: ShownApp[] | null | 'failed') => {
276
+ const found = app.store && Array.isArray(catalog) ? catalog.find((a) => a.id === app.id) : undefined;
277
+ return found && newer(found.version, versionOf(app)) ? found : undefined;
278
+ };
279
+
280
+ /** An icon of the store's catalog (Phosphor's name) in front of a row. */
281
+ const storeIcon = (name?: string) => <LeadingIcon>{name && <AnyIcon name={name} size={24} />}</LeadingIcon>;
282
+
283
+ export function Apps({ env, files }: { env: Env; files: RepoFile[] }) {
284
+ const installed = installedApps(files);
285
+ const catalog = useCatalog(env);
286
+ const store = Array.isArray(catalog) ? catalog : [];
287
+ const [query, setQuery] = useKept('apps-soeg', '');
288
+ const [sort, pickSort] = useSort();
289
+
290
+ // The apps installed: the site's own and those from the store; and one from the store in the file of apps that the
291
+ // site's build does not have yet, from the catalog (by its id until it is read). One that is not installed is under
292
+ // "Appstore".
293
+ const rows = [
294
+ ...apps
295
+ .filter((a) => installed.includes(a.id))
296
+ .map((a) => ({
297
+ id: a.id,
298
+ label: a.label,
299
+ publisher: a.publisher,
300
+ hint: a.hint,
301
+ icon: <LeadingIcon icon={a.icon} />,
302
+ status: updateOf(a, catalog) ? t.apps.updateFound : '',
303
+ })),
304
+ ...installed
305
+ .filter((id) => !apps.some((a) => a.id === id))
306
+ .map((id) => {
307
+ const found = store.find((a) => a.id === id);
308
+ return {
309
+ id,
310
+ label: found?.label ?? id,
311
+ publisher: found?.publisher ?? '',
312
+ hint: found?.hint ?? '',
313
+ icon: storeIcon(found?.icon),
314
+ status: t.apps.pending,
315
+ };
316
+ }),
317
+ ];
318
+ // By the name, who made it and what it does.
319
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
320
+ const shown = rows
321
+ .filter((a) => words.every((w) => `${a.label} ${a.publisher} ${a.hint}`.toLowerCase().includes(w)))
322
+ .sort((a, b) => (sort === 'za' ? -1 : 1) * byText(a.label, b.label));
323
+ // Under the first letter of their names (headed, from HEADINGS_FROM apps).
324
+ const groups = headed(shown, (a) => letterOf(a.label));
325
+
326
+ return (
327
+ <Screen title={t.common.parts.apps} wide back={backFrom('apps')}>
328
+ <SearchRow
329
+ value={query}
330
+ onChange={setQuery}
331
+ label={t.common.searchIn(t.common.parts.apps)}
332
+ sort={sortChoice(sort, pickSort)}
333
+ className="mb-6"
334
+ />
335
+ {groups.length ? (
336
+ groups.map((g) => (
337
+ <Group key={g.key ?? 'all'} title={g.key ?? undefined}>
338
+ {g.items.map((a) => (
339
+ // Who made it under its name.
340
+ <ListItem
341
+ key={a.id}
342
+ href={`#apps/${a.id}`}
343
+ leading={a.icon}
344
+ headline={a.label}
345
+ supporting={a.publisher}
346
+ count={a.status}
347
+ arrow
348
+ />
349
+ ))}
350
+ </Group>
351
+ ))
352
+ ) : (
353
+ <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
354
+ )}
355
+ </Screen>
356
+ );
357
+ }
358
+
359
+ const backToStore = { href: `#${STORE_ROUTE}`, label: t.apps.find };
360
+
361
+ /** All apps, as "Appstore" lists them: those in the store's catalog, and the site's own (not from the store), drawn
362
+ * from the site's code; each with all its words (for the search) and its icon. */
363
+ function allApps(catalog: ShownApp[] | null | 'failed') {
364
+ const store = Array.isArray(catalog) ? catalog : [];
365
+ return [
366
+ ...store.map((a) => ({ ...a, glyph: storeIcon(a.icon) })),
367
+ ...apps
368
+ .filter((a) => !a.store && !store.some((s) => s.id === a.id))
369
+ .map((a) => ({
370
+ id: a.id,
371
+ label: a.label,
372
+ hint: a.hint,
373
+ publisher: a.publisher,
374
+ category: a.category,
375
+ words: [a.label, a.hint, a.publisher, ...(a.keywords ?? [])].join(' ').toLowerCase(),
376
+ glyph: <LeadingIcon icon={a.icon} />,
377
+ })),
378
+ ];
379
+ }
380
+
381
+ /** The address of a developer's page, by their name. */
382
+ const developerHref = (name: string) => `#${DEVELOPER_ROUTE}/${slugify(name)}`;
383
+
384
+ /** An app's row in "Appstore" and on a developer's page: its icon, its name, what it does, and whether it is installed,
385
+ * opening its page. */
386
+ function AppRow({ app, installed }: { app: ReturnType<typeof allApps>[number]; installed: boolean }) {
387
+ return (
388
+ <ListItem
389
+ href={`#apps/${app.id}`}
390
+ leading={app.glyph}
391
+ headline={app.label}
392
+ supporting={app.hint}
393
+ count={installed && t.apps.installed}
394
+ arrow
395
+ />
396
+ );
397
+ }
398
+
399
+ /** A developer's page (#apps/udvikler/<name>): their name, and all their apps and themes (in the store and the site's
400
+ * own), each opening its page. */
401
+ export function DeveloperPage({ slug, env, files }: { slug: string; env: Env; files: RepoFile[] }) {
402
+ const installed = installedApps(files);
403
+ const catalog = useCatalog(env);
404
+ const themeCatalog = useThemeCatalog(env);
405
+ const theirs = allApps(catalog)
406
+ .filter((a) => slugify(a.publisher) === slug)
407
+ .sort((a, b) => byText(a.label, b.label));
408
+ const theirThemes = allThemes(themeCatalog)
409
+ .filter((x) => slugify(x.author) === slug)
410
+ .sort((a, b) => byText(a.name, b.name));
411
+ const name = theirs[0]?.publisher ?? theirThemes[0]?.author;
412
+ return (
413
+ <Screen title={name ?? t.apps.developer} wide back={backToStore}>
414
+ {name && (
415
+ <div className="mb-8 flex items-center gap-3 px-4">
416
+ <LeadingIcon icon={UserCircle} />
417
+ <span className="min-w-0 flex-1">
418
+ <span className="block truncate font-medium">{name}</span>
419
+ <span className="block truncate text-sm text-muted">
420
+ {[theirs.length && t.apps.appCount(theirs.length), theirThemes.length && t.library.themeCount(theirThemes.length)]
421
+ .filter(Boolean)
422
+ .join(' · ')}
423
+ </span>
424
+ </span>
425
+ </div>
426
+ )}
427
+ {theirs.length > 0 && (
428
+ <Group title={t.apps.developerApps}>
429
+ {theirs.map((a) => (
430
+ <AppRow key={a.id} app={a} installed={installed.includes(a.id)} />
431
+ ))}
432
+ </Group>
433
+ )}
434
+ {theirThemes.length > 0 && (
435
+ <Group title={t.common.parts.themes}>
436
+ {theirThemes.map((x) => (
437
+ <ThemeRow key={x.id} of={x} />
438
+ ))}
439
+ </Group>
440
+ )}
441
+ {!name &&
442
+ (catalog === null || themeCatalog === null ? (
443
+ <EmptyNote busy>{t.apps.loadingStore}</EmptyNote>
444
+ ) : (
445
+ <EmptyNote>{t.apps.noDeveloper}</EmptyNote>
446
+ ))}
447
+ </Screen>
448
+ );
449
+ }
450
+
451
+ /** What every app's page says about it, from its folder or the store's catalog: its pictures (each opening it whole),
452
+ * what it does more fully, what it needs of the site (needsOf), the cookies it sets (`consent`: the cookie banner asks
453
+ * first), and who made it, with a row to their page. `children`: what stands between its description and what it
454
+ * needs (its settings and what it adds). */
455
+ function AppAbout({
456
+ pictures,
457
+ description,
458
+ news = [],
459
+ needs,
460
+ notifies = [],
461
+ cookies,
462
+ consent,
463
+ publisher,
464
+ licenses,
465
+ children,
466
+ }: {
467
+ pictures: { src: string; label: string }[];
468
+ description?: string;
469
+ news?: { version: string; text: string }[];
470
+ needs?: AppNeeds;
471
+ /** The kinds of notifications it sends (AppNotifications), under "Notifikationer": their names, and installed their
472
+ * types, each with its switch, the same as under "Konto" › "Notifikationer". */
473
+ notifies?: { label: string; type?: string }[];
474
+ cookies: AppCookie[];
475
+ consent: boolean;
476
+ publisher: string;
477
+ /** Its licences, under "Licenser" (OwnerLicenses, or StoreLicenseRows from the catalog). */
478
+ licenses?: ReactNode;
479
+ children?: ReactNode;
480
+ }) {
481
+ const row = (glyph: Icon, text: string, more?: string) => <ListItem key={text} icon={glyph} headline={text} supporting={more} />;
482
+ const needed = needs
483
+ ? [
484
+ needs.pages && row(Code, t.apps.needsPages),
485
+ needs.loads.length > 0 && row(Globe, t.apps.needsLoads, needs.loads.join(', ')),
486
+ needs.sends.length > 0 && row(PaperPlaneTilt, t.apps.needsSends, needs.sends.join(', ')),
487
+ needs.server && row(HardDrives, t.apps.needsServer),
488
+ needs.build && row(Hammer, t.apps.needsBuild),
489
+ ...(needs.changes ?? []).map((hook) => row(Palette, t.apps.changes[hook])),
490
+ ].filter((x) => !!x)
491
+ : [];
492
+ const lasts = (c: AppCookie) => c.lasts && t.apps.cookieLasts(c.lasts);
493
+ // A named cookie's category (lib/cookieCategories.mjs), first under its name.
494
+ const category = (c: AppCookie) => t.blocks.cookies.entries.categories[categoryOf(c)];
495
+ const { off } = useNotificationChoices();
496
+ return (
497
+ <>
498
+ {pictures.length > 0 && (
499
+ <section className="mb-8">
500
+ <GroupTitle>{t.apps.screenshots}</GroupTitle>
501
+ <ImageCarousel label={t.apps.screenshots} images={pictures} />
502
+ </section>
503
+ )}
504
+ {description && (
505
+ <section className="mb-8">
506
+ <GroupTitle>{t.apps.about}</GroupTitle>
507
+ {description.split(/\n\s*\n/).map((p) => (
508
+ <p key={p} className="mb-3 px-4 last:mb-0">
509
+ {p}
510
+ </p>
511
+ ))}
512
+ </section>
513
+ )}
514
+ {news.length > 0 && (
515
+ <section className="mb-8">
516
+ <GroupTitle>{t.apps.news}</GroupTitle>
517
+ {news.map((n) => (
518
+ <div key={n.version} className="mb-3 px-4 last:mb-0">
519
+ <h3 className="font-medium">{t.apps.version(n.version)}</h3>
520
+ {n.text.split(/\n\s*\n/).map((p) => (
521
+ <p key={p} className="text-muted">
522
+ {p}
523
+ </p>
524
+ ))}
525
+ </div>
526
+ ))}
527
+ </section>
528
+ )}
529
+ {children}
530
+ {needs && <Group title={t.apps.needs}>{needed.length ? needed : row(CheckCircle, t.apps.needsNothing)}</Group>}
531
+ {notifies.some((n) => n.type) ? (
532
+ <section className="mb-8">
533
+ <GroupTitle>{t.apps.notifications}</GroupTitle>
534
+ <div className="space-y-5 px-4">
535
+ {notifies.map((n) => (
536
+ <SwitchRow
537
+ key={n.type}
538
+ label={n.label}
539
+ texts={[t.library.on, t.library.off]}
540
+ selected={!off.includes(n.type!)}
541
+ onChange={(yes) => setOn(n.type!, yes)}
542
+ />
543
+ ))}
544
+ </div>
545
+ </section>
546
+ ) : (
547
+ <Group title={t.apps.notifications}>
548
+ {notifies.length ? notifies.map((n) => row(Bell, n.label)) : row(Bell, t.apps.notifiesNone)}
549
+ </Group>
550
+ )}
551
+ <Group title={t.apps.cookies}>
552
+ {consent && row(ShieldCheck, t.apps.cookiesAsk)}
553
+ {cookies.map((c) =>
554
+ c.name
555
+ ? row(Cookie, c.name, [category(c), c.purpose, lasts(c)].filter(Boolean).join(' · '))
556
+ : row(Cookie, c.purpose, lasts(c) || undefined),
557
+ )}
558
+ {!consent && !cookies.length && row(Cookie, t.apps.noCookies)}
559
+ </Group>
560
+ {licenses}
561
+ <DeveloperRow name={publisher} />
562
+ </>
563
+ );
564
+ }
565
+
566
+ /** Who made an app or a theme, under "Udvikler", as a row to their page with all they have made. */
567
+ export function DeveloperRow({ name }: { name: string }) {
568
+ return (
569
+ <Group title={t.apps.developer}>
570
+ <ListItem href={developerHref(name)} icon={UserCircle} headline={name} arrow />
571
+ </Group>
572
+ );
573
+ }
574
+
575
+ /** "Appstore" (#indstillinger/appstore, under "Indstillinger"): in a tab of its own each in the top app bar, all apps,
576
+ * those in the app store and the site's own, each with what it does and whether it is installed, and all themes
577
+ * (#indstillinger/appstore/temaer, ThemeStore.tsx), those in the store
578
+ * and those installed, each with its version and who made it; each opening its page. With search (by name, what it
579
+ * does, who made it and the words it is found by, in both languages), the order and the filters by category (apps)
580
+ * and by whether it is installed, each picked one as a chip. */
581
+ export function StorePage({ env, files, tab }: { env: Env; files: RepoFile[]; tab: 'apps' | 'themes' }) {
582
+ const installed = installedApps(files);
583
+ const catalog = useCatalog(env);
584
+ const themeCatalog = useThemeCatalog(env);
585
+ const [query, setQuery] = useKept('butik-soeg', '');
586
+ const [sort, pickSort] = useSort();
587
+ const [category, setCategory] = useKept<AppCategory | ''>('butik-kategori', '');
588
+ const [state, setState] = useKept<'' | 'installed' | 'notInstalled'>('butik-status', '');
589
+ const apps = tab === 'apps';
590
+ const read = apps ? catalog : themeCatalog;
591
+
592
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
593
+ const order = (a: string, b: string) => (sort === 'za' ? -1 : 1) * byText(a, b);
594
+ const appGroups = headed(
595
+ allApps(catalog)
596
+ .filter((a) => words.every((w) => a.words.includes(w)))
597
+ .filter((a) => !category || a.category === category)
598
+ .filter((a) => !state || installed.includes(a.id) === (state === 'installed'))
599
+ .sort((a, b) => order(a.label, b.label)),
600
+ (a) => letterOf(a.label),
601
+ );
602
+ const themeGroups = headed(
603
+ allThemes(themeCatalog)
604
+ .filter((x) => words.every((w) => x.words.includes(w)))
605
+ .filter((x) => !state || themeInstalled(x.id) === (state === 'installed'))
606
+ .sort((a, b) => order(a.name, b.name)),
607
+ (x) => letterOf(x.name),
608
+ );
609
+ const filters = [
610
+ ...(apps
611
+ ? [
612
+ {
613
+ title: t.apps.categoryFilter,
614
+ value: category,
615
+ options: [
616
+ { value: '', label: t.apps.allCategories },
617
+ ...appCategories.map((value) => ({ value, label: t.apps.categories[value] })),
618
+ ],
619
+ onChange: (v: string) => setCategory(v as AppCategory | ''),
620
+ },
621
+ ]
622
+ : []),
623
+ {
624
+ title: t.apps.stateFilter,
625
+ value: state,
626
+ options: [
627
+ { value: '', label: t.apps.allStates },
628
+ { value: 'installed', label: t.apps.installed },
629
+ { value: 'notInstalled', label: t.apps.notInstalled },
630
+ ],
631
+ onChange: (v: string) => setState(v as typeof state),
632
+ },
633
+ ];
634
+ // Each filter picked, as a chip that removes it.
635
+ const chips = [
636
+ apps && category && { key: 'category', label: t.apps.categories[category], clear: () => setCategory('') },
637
+ state && { key: 'state', label: t.apps[state], clear: () => setState('') },
638
+ ].filter((c) => !!c);
639
+ const empty = apps ? !appGroups.length : !themeGroups.length;
640
+
641
+ return (
642
+ <Screen
643
+ title={t.apps.find}
644
+ wide
645
+ back={backFrom('appstore')}
646
+ tabs={{
647
+ label: t.apps.find,
648
+ value: tab,
649
+ options: [
650
+ { value: 'apps', label: t.common.parts.apps, icon: PuzzlePiece },
651
+ { value: 'themes', label: t.common.parts.themes, icon: Swatches },
652
+ ],
653
+ onChange: (v) => (location.hash = v === 'apps' ? STORE_ROUTE : STORE_THEMES),
654
+ }}
655
+ >
656
+ <SearchRow
657
+ value={query}
658
+ onChange={setQuery}
659
+ label={apps ? t.apps.searchStore : t.library.searchThemes}
660
+ sort={sortChoice(sort, pickSort)}
661
+ filter={<Filters title={t.library.filter} selected={chips.length > 0} groups={filters} />}
662
+ className="mb-6"
663
+ />
664
+ {chips.length > 0 && (
665
+ <div className="-mt-2 mb-6 flex flex-wrap gap-2">
666
+ {chips.map((c) => (
667
+ <InputChip key={c.key} removeLabel={t.library.removeFilter(c.label)} onRemove={c.clear}>
668
+ {c.label}
669
+ </InputChip>
670
+ ))}
671
+ </div>
672
+ )}
673
+ {/* The site's own are shown also when the store cannot be read. */}
674
+ {!hasStore ? (
675
+ <EmptyNote>{t.apps.noStore}</EmptyNote>
676
+ ) : read === null ? (
677
+ <EmptyNote busy>{t.apps.loadingStore}</EmptyNote>
678
+ ) : read === 'failed' ? (
679
+ <EmptyNote>{t.apps.storeFailed}</EmptyNote>
680
+ ) : null}
681
+ {read === null ? null : empty ? (
682
+ <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
683
+ ) : apps ? (
684
+ appGroups.map((g) => (
685
+ <Group key={g.key ?? 'all'} title={g.key ?? undefined}>
686
+ {g.items.map((a) => (
687
+ <AppRow key={a.id} app={a} installed={installed.includes(a.id)} />
688
+ ))}
689
+ </Group>
690
+ ))
691
+ ) : (
692
+ themeGroups.map((g) => (
693
+ <Group key={g.key ?? 'all'} title={g.key ?? undefined}>
694
+ {g.items.map((x) => (
695
+ <ThemeRow key={x.id} of={x} />
696
+ ))}
697
+ </Group>
698
+ ))
699
+ )}
700
+ </Screen>
701
+ );
702
+ }
703
+
704
+ const recordOf = (app: ShownApp): StoreRecord => ({ version: app.version, ref: app.ref, files: app.files });
705
+
706
+ /** The cookies an app sets (its cookies.json among its files from the store), in the site's language; none when it
707
+ * has no such file. */
708
+ function cookiesIn(files: { path: string; content: string | Uint8Array }[]): AppCookie[] {
709
+ const file = files.find((f) => f.path === 'cookies.json');
710
+ try {
711
+ const list = file && JSON.parse(typeof file.content === 'string' ? file.content : new TextDecoder().decode(file.content));
712
+ return Array.isArray(list) ? list : [];
713
+ } catch {
714
+ return [];
715
+ }
716
+ }
717
+
718
+ /** Saves the changes to an app's folder (appChanges) and to the cookie list (cookieChanges) in one commit, and gives
719
+ * the editor the site's files it changed: the file of apps and the cookie list. `change`: what it changed (deploy.ts). */
720
+ async function saveApp(env: Env, message: string, change: string, changes: Change[], apps: RepoFile | undefined) {
721
+ const oids = await env.commit(message, changes, change);
722
+ const site = changes.filter((c) => c.path === config.apps.installed || inCookieList(c.path));
723
+ const saved = site.flatMap((c) => (typeof c.content === 'string' ? [{ path: c.path, oid: oids[c.path], text: c.content }] : []));
724
+ const removed = site.filter((c) => c.content === null).map((c) => c.path);
725
+ // Only taken out of the list (an update): the file of apps, as it is, with them.
726
+ const [first, ...more] = saved.length ? saved : apps ? [apps] : [];
727
+ if (first) env.onSaved(first, { files: more, removed });
728
+ }
729
+
730
+ // Whether the apps were updated on their own while the editor is open (autoUpdate): once.
731
+ let autoUpdated = false;
732
+
733
+ /** Updates the apps installed from the store on their own, once while the editor is open (App.tsx): each with a newer
734
+ * version in the store, in its own commit, with a message linking to its page, where what is new stands. One whose
735
+ * files were changed in the site is left for "Opdater til …" on its page, which says they are written over. A version
736
+ * with a higher first number (breaking) is not updated to: a message says it is there, once for each such version. */
737
+ export async function autoUpdate(env: Env, files: RepoFile[]) {
738
+ if (autoUpdated) return;
739
+ autoUpdated = true;
740
+ const installed = installedApps(files);
741
+ const file = files.find((f) => f.path === config.apps.installed);
742
+ const catalog = await env.store.catalog().catch(() => [] as ShownApp[]);
743
+ for (const app of apps.filter((a) => a.store && installed.includes(a.id))) {
744
+ const next = catalog.find((a) => a.id === app.id);
745
+ if (!next || !newer(next.version, versionOf(app))) continue;
746
+ try {
747
+ // As the site has it now (another tab, or an update the site's build does not have yet, may have come first).
748
+ const current = await env.folder(appFolder(app.id));
749
+ const record = recordIn(current, appFolder(app.id)) ?? app.store!;
750
+ if (!newer(next.version, record.version)) continue;
751
+ if (breaking(next.version, record.version)) {
752
+ if (!told('updates', `${app.id}@${next.version}`))
753
+ env.notify({
754
+ text: t.apps.breakingFound(next.label, next.version),
755
+ edit: { text: t.apps.whatsNew, to: `apps/${app.id}` },
756
+ kind: 'updates',
757
+ });
758
+ continue;
759
+ }
760
+ if (changedFiles(app.id, record, current).length) continue;
761
+ const wanted = { files: await env.store.files(next), record: recordOf(next) };
762
+ const changes = [...appChanges(app.id, current, wanted, installed, file), ...cookieChanges(app.id, cookiesIn(wanted.files), files)];
763
+ const change = t.app.changes.updated(kindAndName(t.app.changes.app, next.label), next.version);
764
+ await saveApp(env, siteT.apps.commit.update(next.written, next.version), change, changes, file);
765
+ updated.set(app.id, next.version);
766
+ env.notify({
767
+ text: t.apps.autoUpdated(next.label, next.version),
768
+ edit: { text: t.apps.whatsNew, to: `apps/${app.id}` },
769
+ kind: 'updates',
770
+ });
771
+ } catch (e) {
772
+ // Tried again the next time the editor opens.
773
+ console.warn(`${app.id}:`, e);
774
+ }
775
+ }
776
+ }
777
+
778
+ /** Installing, updating and uninstalling an app from the store, each saved at once in its own commit: its folder
779
+ * copied from the store, or deleted, with the site's file of apps (appChanges in store.ts). Each says true when it
780
+ * was saved. */
781
+ function useStoreActions(env: Env, files: RepoFile[]) {
782
+ const [saving, setSaving] = useState(false);
783
+ const installed = installedApps(files);
784
+ const file = files.find((f) => f.path === config.apps.installed);
785
+
786
+ async function change(
787
+ id: string,
788
+ // `kind`: the notification it is when it is saved (an update is "Apps opdateret", as one on its own).
789
+ text: { busy: string; done: string; commit: string; change: string; kind?: 'saved' | 'updates' },
790
+ next: () => Promise<Parameters<typeof appChanges>[2]>,
791
+ // Whether to go on, with the app's files in the site now (an update asks, when some were changed).
792
+ go: (current: RepoFile[]) => Promise<boolean> = async () => true,
793
+ ): Promise<boolean> {
794
+ setSaving(true);
795
+ env.notify({ text: text.busy });
796
+ try {
797
+ const [current, wanted] = await Promise.all([env.folder(appFolder(id)), next()]);
798
+ if (!(await go(current))) {
799
+ env.notify(null);
800
+ return false;
801
+ }
802
+ const cookies = cookieChanges(id, wanted ? cookiesIn(wanted.files) : null, files);
803
+ await saveApp(env, text.commit, text.change, [...appChanges(id, current, wanted, installed, file), ...cookies], file);
804
+ env.notify({ text: text.done, kind: text.kind ?? 'saved' });
805
+ return true;
806
+ } catch (e) {
807
+ env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
808
+ return false;
809
+ } finally {
810
+ setSaving(false);
811
+ }
812
+ }
813
+
814
+ return {
815
+ saving,
816
+ install: (app: ShownApp) =>
817
+ change(
818
+ app.id,
819
+ {
820
+ busy: t.apps.installing,
821
+ done: t.apps.isInstalledSoon(app.label),
822
+ commit: siteT.apps.commit.install(app.written),
823
+ change: t.app.changes.installed(kindAndName(t.app.changes.app, app.label)),
824
+ },
825
+ async () => ({ files: await env.store.files(app), record: recordOf(app) }),
826
+ ),
827
+ async update(app: ShownApp, record: StoreRecord) {
828
+ const done = await change(
829
+ app.id,
830
+ {
831
+ busy: t.apps.updating,
832
+ done: t.apps.isUpdatedSoon(app.label, app.version),
833
+ commit: siteT.apps.commit.update(app.written, app.version),
834
+ change: t.app.changes.updated(kindAndName(t.app.changes.app, app.label), app.version),
835
+ kind: 'updates',
836
+ },
837
+ async () => ({ files: await env.store.files(app), record: recordOf(app) }),
838
+ async (current) => {
839
+ env.notify(null);
840
+ return askUpdate(
841
+ app.label,
842
+ record.version,
843
+ app.version,
844
+ notesSince(app.releaseNotes, record.version, app.version),
845
+ changedFiles(app.id, record, current),
846
+ );
847
+ },
848
+ );
849
+ if (done) updated.set(app.id, app.version);
850
+ return done;
851
+ },
852
+ uninstall: (id: string, label: string, written: string) =>
853
+ change(
854
+ id,
855
+ {
856
+ busy: t.apps.uninstalling,
857
+ done: t.apps.isUninstalled(label),
858
+ commit: siteT.apps.commit.uninstall(written),
859
+ change: t.app.changes.uninstalled(kindAndName(t.app.changes.app, label)),
860
+ },
861
+ async () => null,
862
+ ),
863
+ };
864
+ }
865
+
866
+ /** Before an app or a theme is updated from `from` to `version`: what is new in it since (`notes`, its release notes),
867
+ * that it can break something (a higher first number, breaking), and, when files in its folder were changed in the site
868
+ * (`changed`), that they are written over. Without any of them, it is updated at once. */
869
+ export async function askUpdate(
870
+ name: string,
871
+ from: string,
872
+ version: string,
873
+ notes: { version: string; text: string }[],
874
+ changed: string[],
875
+ ) {
876
+ const big = breaking(version, from);
877
+ if (!notes.length && !changed.length && !big) return true;
878
+ const answer = await choose({
879
+ title: t.apps.askUpdate(name, version),
880
+ notes: notes.map((n) => ({ title: t.apps.version(n.version), text: n.text })),
881
+ text: [big && t.apps.breaking, changed.length && t.apps.overwrites].filter(Boolean).join(' '),
882
+ choices: [{ value: 'yes', label: t.apps.updateNow }],
883
+ });
884
+ return !!answer;
885
+ }
886
+
887
+ /** What is new in the last versions up to the one shown (an app's release notes), the newest first, on its page. */
888
+ const newsOf = (notes: Record<string, string> | undefined, version: string) => notesSince(notes, '0', version).slice(0, 3);
889
+
890
+ /** The top of an app's page: its icon, its name, who made it, whether it is installed, its version (from the store),
891
+ * and what it does. */
892
+ function AppHead({
893
+ icon,
894
+ label,
895
+ publisher,
896
+ status,
897
+ version,
898
+ hint,
899
+ }: {
900
+ icon: ReactNode;
901
+ label: string;
902
+ publisher: string;
903
+ status: string;
904
+ version?: string;
905
+ hint: string;
906
+ }) {
907
+ return (
908
+ <div className="mb-8 px-4">
909
+ <div className="flex items-center gap-3">
910
+ {icon}
911
+ <span className="min-w-0 flex-1">
912
+ <span className="block truncate font-medium">{label}</span>
913
+ <span className="block truncate text-sm text-muted">{t.apps.publisher(publisher)}</span>
914
+ <span className="block truncate text-sm text-muted">{status}</span>
915
+ {version && <span className="block truncate text-sm text-muted">{t.apps.version(version)}</span>}
916
+ </span>
917
+ </div>
918
+ <p className="mt-3 text-muted">{hint}</p>
919
+ </div>
920
+ );
921
+ }
922
+
923
+ /** An app's pictures from the store (readScreenshots), once its entry in the catalog is read; none until then. */
924
+ function useStorePictures(env: Env, app: ShownApp | undefined) {
925
+ const [pictures, setPictures] = useState<{ src: string; label: string }[]>([]);
926
+ useEffect(() => {
927
+ if (!app?.screenshots?.length) return;
928
+ let on = true;
929
+ let made: string[] = [];
930
+ env.store.screenshots(app).then((found) => {
931
+ made = found.map((x) => x.src);
932
+ if (on) setPictures(found);
933
+ else made.forEach((src) => URL.revokeObjectURL(src));
934
+ });
935
+ return () => {
936
+ on = false;
937
+ made.forEach((src) => URL.revokeObjectURL(src));
938
+ };
939
+ }, [app?.id, app?.version]);
940
+ return pictures;
941
+ }
942
+
943
+ /** The page of an app in the store that is not installed, or installed but not yet built (#apps/<app>), from the
944
+ * catalog: as an app's page in the site, what it adds without switches, and "Installer" (or "Afinstaller"). */
945
+ export function StoreAppPage({ id, env, files }: { id: string; env: Env; files: RepoFile[] }) {
946
+ const catalog = useCatalog(env);
947
+ const { saving, install, uninstall } = useStoreActions(env, files);
948
+ const pending = installedApps(files).includes(id);
949
+ const app = Array.isArray(catalog) ? catalog.find((a) => a.id === id) : undefined;
950
+ const pictures = useStorePictures(env, app);
951
+
952
+ const row = (a: StoreAdd, key: string) => <ListItem key={key} leading={storeIcon(a.icon)} headline={a.label} supporting={a.hint} />;
953
+ // Its kinds under the name of the list they are added to ("Promovering").
954
+ const kinds = app?.adds.kinds ?? [];
955
+ const listName = (list: string) => collections.find((c) => c.id === list)?.label ?? list;
956
+
957
+ return (
958
+ <Screen title={app?.label ?? (pending ? id : t.apps.find)} wide back={pending ? backToApps : backToStore}>
959
+ {app && (
960
+ <>
961
+ <AppHead
962
+ icon={storeIcon(app.icon)}
963
+ label={app.label}
964
+ publisher={app.publisher}
965
+ status={pending ? t.apps.pending : t.apps.notInstalled}
966
+ version={app.version}
967
+ hint={app.hint}
968
+ />
969
+ {!pending && (
970
+ <div className="mb-8">
971
+ <Button wide disabled={saving} onClick={() => install(app)}>
972
+ {t.apps.install}
973
+ </Button>
974
+ </div>
975
+ )}
976
+ <AppAbout
977
+ pictures={pictures}
978
+ description={app.description}
979
+ news={newsOf(app.releaseNotes, app.version)}
980
+ needs={app.needs}
981
+ notifies={app.notifies?.map((label) => ({ label }))}
982
+ cookies={app.cookies ?? []}
983
+ consent={!!app.consent}
984
+ publisher={app.publisher}
985
+ licenses={<StoreLicenseRows licenses={app.licenses} name={app.label} author={app.publisher} version={app.version} />}
986
+ >
987
+ {app.adds.blocks.length > 0 && <Group title={t.apps.blocks}>{app.adds.blocks.map((a, i) => row(a, `${i}`))}</Group>}
988
+ {app.adds.lists.length > 0 && <Group title={t.apps.lists}>{app.adds.lists.map((a, i) => row(a, `${i}`))}</Group>}
989
+ {[...new Set(kinds.map((k) => k.list))].map((list) => (
990
+ <Group key={list} title={listName(list)}>
991
+ {kinds.filter((k) => k.list === list).map((k, i) => row(k, `${i}`))}
992
+ </Group>
993
+ ))}
994
+ </AppAbout>
995
+ </>
996
+ )}
997
+ {!app &&
998
+ (catalog === null ? (
999
+ <EmptyNote busy>{t.apps.loadingStore}</EmptyNote>
1000
+ ) : (
1001
+ <EmptyNote>{catalog === 'failed' ? t.apps.storeFailed : t.apps.notInStore}</EmptyNote>
1002
+ ))}
1003
+ {/* Installed but not yet built, it can be uninstalled again, also without the catalog. */}
1004
+ {pending ? (
1005
+ <div>
1006
+ <Button
1007
+ kind="outlined"
1008
+
1009
+ wide
1010
+ disabled={saving}
1011
+ onClick={async () => {
1012
+ const label = app?.label ?? id;
1013
+ if (await ask(t.apps.askUninstall(label), t.apps.uninstall)) uninstall(id, label, app?.written ?? id);
1014
+ }}
1015
+ >
1016
+ {t.apps.uninstall}
1017
+ </Button>
1018
+ <SupportingText className="mt-2">{t.apps.pendingHint}</SupportingText>
1019
+ </div>
1020
+ ) : null}
1021
+ </Screen>
1022
+ );
1023
+ }
1024
+
1025
+ /** What an app adds but its lists, each on its page under "Blokke" or the name of its list ("Promovering") with a sheet: its blocks, and its kinds of the site's
1026
+ * lists (a Mailchimp popup), each with where it is used and its switch; a kind with a button to a new one. */
1027
+ function addsOf(app: SiteApp, files: RepoFile[]) {
1028
+ return [
1029
+ ...siteBlocks
1030
+ .filter((b) => b.app === app.id)
1031
+ .map((b) => ({
1032
+ key: b.type,
1033
+ icon: b.icon,
1034
+ label: b.label,
1035
+ hint: b.hint,
1036
+ block: b,
1037
+ create: undefined,
1038
+ uses: pagesWith(b.type, files).map((p) => ({ href: `#${p.path}`, title: p.title })),
1039
+ })),
1040
+ ...kindsOf(app).map((k) => ({
1041
+ ...k,
1042
+ block: undefined,
1043
+ create: k.create ? { href: `#ny/${k.key}/${app.id}`, label: k.create } : undefined,
1044
+ uses: entriesOf(app, k.key, files),
1045
+ })),
1046
+ ];
1047
+ }
1048
+
1049
+ type Add = ReturnType<typeof addsOf>[number];
1050
+
1051
+ /** The row of a block (or a kind of a list) an installed app adds: tapped, it opens its sheet; at its end the switch
1052
+ * that turns it on or off, as the one in the sheet ("Aktiveret"), saved at once. Used, it cannot be turned off, and
1053
+ * the switch says why. The only switch in a row (CLAUDE.md). */
1054
+ function SwitchedRow({
1055
+ app,
1056
+ add,
1057
+ env,
1058
+ files,
1059
+ busy,
1060
+ onBusy,
1061
+ onOpen,
1062
+ }: {
1063
+ app: SiteApp;
1064
+ add: Add;
1065
+ env: Env;
1066
+ files: RepoFile[];
1067
+ busy: boolean;
1068
+ onBusy: (busy: boolean) => void;
1069
+ onOpen: () => void;
1070
+ }) {
1071
+ const count = add.uses.length;
1072
+ const id = add.block ? add.block.type : kindKey(add.key, app.id);
1073
+ const texts = add.block ? blockSwitchTexts(add.block, count) : kindSwitchTexts(app, add.key, count);
1074
+ const { on, saving, turn } = useOnOff(id, count, texts, env, files);
1075
+ return (
1076
+ <ListItem
1077
+ aria-haspopup="dialog"
1078
+ onClick={onOpen}
1079
+ icon={add.icon}
1080
+ headline={add.label}
1081
+ supporting={add.hint}
1082
+ actions={
1083
+ <span className="shrink-0 pr-4">
1084
+ <Switch
1085
+ label={t.apps.activeOf(add.label)}
1086
+ selected={on}
1087
+ texts={[t.blockSettings.on, t.blockSettings.off]}
1088
+ disabled={busy || saving}
1089
+ onChange={async (next) => {
1090
+ onBusy(true);
1091
+ try {
1092
+ await turn(next);
1093
+ } finally {
1094
+ onBusy(false);
1095
+ }
1096
+ }}
1097
+ />
1098
+ </span>
1099
+ }
1100
+ />
1101
+ );
1102
+ }
1103
+
1104
+ const backToApps = { href: '#apps', label: t.common.parts.apps };
1105
+
1106
+ type AppProps = { app: SiteApp; env: Env; files: RepoFile[] };
1107
+
1108
+ /** An app's page, the same for every app: it installs the app; its settings are on a screen of their own (settingsRoute). */
1109
+ export function AppPage({ app, env, files }: AppProps) {
1110
+ // One that is not installed is found under "Appstore".
1111
+ return (
1112
+ <Screen title={app.label} wide back={installedApps(files).includes(app.id) ? backToApps : backToStore}>
1113
+ <AppContent app={app} env={env} files={files} />
1114
+ </Screen>
1115
+ );
1116
+ }
1117
+
1118
+ /** A screen of an app's own under its page (#apps/<id>/<key>, AppDefinition.screens), while it is installed: its title,
1119
+ * back to the app's page, and what the app shows on it. */
1120
+ export function AppScreenPage({ app, id, env }: { app: SiteApp; id: string; env: Env }) {
1121
+ const screen = app.screens![id];
1122
+ return (
1123
+ <Screen title={siteText(screen.title)} wide back={{ href: `#apps/${app.id}`, label: app.label }}>
1124
+ <div data-app-page={app.id}>
1125
+ <screen.page api={(path, init) => env.appApi(app.id, path, init)} />
1126
+ </div>
1127
+ </Screen>
1128
+ );
1129
+ }
1130
+
1131
+ /** What every app's page has, in this order: its name, who made it, whether it is installed, its version (from the
1132
+ * store) and what it does; "Installer" (or, from the store, "Opdater" when it has a newer version); its pictures and
1133
+ * what it does more fully; a row to its settings (installed), with where they are; what it adds (its blocks, its kinds
1134
+ * of lists); what it needs, its cookies and who made it (AppAbout); where it is used; and "Afinstaller". */
1135
+ function AppContent({ app, env, files }: AppProps) {
1136
+ const notify = env.notify;
1137
+ const dialog = useRef<HTMLDialogElement>(null);
1138
+ const [toggling, setToggling] = useState(false);
1139
+ const store = useStoreActions(env, files);
1140
+ const saving = toggling || store.saving;
1141
+ const catalog = useCatalog(env);
1142
+ const update = updateOf(app, catalog);
1143
+ // Its release notes as the store has them: also those of a version the site's build does not have yet.
1144
+ const inStore = Array.isArray(catalog) ? catalog.find((a) => a.id === app.id) : undefined;
1145
+ const [openKey, setOpenKey] = useState<string | null>(null);
1146
+ // While one row's switch saves, the others wait.
1147
+ const [switching, setSwitching] = useState(false);
1148
+ const installed = installedApps(files);
1149
+ const on = installed.includes(app.id);
1150
+ // Where it is used: while it is, it cannot be uninstalled.
1151
+ const uses = on ? usesOf(app, files) : [];
1152
+ const stuck = uses.length > 0;
1153
+ const adds = addsOf(app, files);
1154
+ const lists = listsOf(app, files);
1155
+ const open = adds.find((a) => a.key === openKey);
1156
+ const settings = on && settingsRoute(app);
1157
+
1158
+ // Installed or uninstalled, saved at once in its own commit. One in use cannot be uninstalled. One from the store
1159
+ // is deleted from the site (it can be installed again from the store).
1160
+ async function install(yes: boolean) {
1161
+ if (!yes && stuck) return notify({ text: t.apps.stuck, error: true });
1162
+ if (!yes && !(await ask(t.apps.askUninstall(app.label), t.apps.uninstall))) return;
1163
+ if (!yes && app.store) return store.uninstall(app.id, app.label, asWritten(app).label);
1164
+ const file = files.find((f) => f.path === config.apps.installed);
1165
+ const next = installed.filter((id) => id !== app.id).concat(yes ? [app.id] : []);
1166
+ const text = `${JSON.stringify({ installed: [...new Set(next)].sort() }, null, 2)}\n`;
1167
+ if (text === file?.text) return;
1168
+ setToggling(true);
1169
+ notify({ text: yes ? t.apps.installing : t.apps.uninstalling });
1170
+ try {
1171
+ // With its cookies added to the cookie list, or those it added taken out.
1172
+ const changes = [
1173
+ { path: config.apps.installed, content: text, oid: file?.oid || null },
1174
+ ...cookieChanges(app.id, yes ? (asWritten(app).cookieList ?? []) : null, files),
1175
+ ];
1176
+ const change = t.app.changes[yes ? 'installed' : 'uninstalled'](kindAndName(t.app.changes.app, app.label));
1177
+ await saveApp(env, siteT.apps.commit[yes ? 'install' : 'uninstall'](asWritten(app).label), change, changes, file);
1178
+ notify({ text: yes ? t.apps.isInstalled(app.label) : t.apps.isUninstalled(app.label), kind: 'saved' });
1179
+ } catch (e) {
1180
+ notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
1181
+ } finally {
1182
+ setToggling(false);
1183
+ }
1184
+ }
1185
+
1186
+ // What it adds under a heading. Installed, each row opens its sheet and has its switch at its end.
1187
+ const addsGroup = (title: string, items: typeof adds) =>
1188
+ items.length > 0 && (
1189
+ <Group key={title} title={title}>
1190
+ {items.map((a) =>
1191
+ on ? (
1192
+ <SwitchedRow
1193
+ key={a.key}
1194
+ app={app}
1195
+ add={a}
1196
+ env={env}
1197
+ files={files}
1198
+ busy={switching}
1199
+ onBusy={setSwitching}
1200
+ onOpen={() => {
1201
+ setOpenKey(a.key);
1202
+ dialog.current?.showModal();
1203
+ }}
1204
+ />
1205
+ ) : (
1206
+ <ListItem key={a.key} icon={a.icon} headline={a.label} supporting={a.hint} />
1207
+ ),
1208
+ )}
1209
+ </Group>
1210
+ );
1211
+
1212
+ return (
1213
+ <div>
1214
+ <AppHead
1215
+ icon={<LeadingIcon icon={app.icon} selected={on} />}
1216
+ label={app.label}
1217
+ publisher={app.publisher}
1218
+ status={on && update ? t.apps.updateFound : statusOf(on)}
1219
+ version={app.store && versionOf(app)}
1220
+ hint={app.hint}
1221
+ />
1222
+ {(!on || (update && app.store)) && (
1223
+ <div className="mb-8">
1224
+ {on ? (
1225
+ <Button wide disabled={saving} onClick={() => store.update(update!, app.store!)}>
1226
+ {t.apps.update(update!.version)}
1227
+ </Button>
1228
+ ) : (
1229
+ <Button wide disabled={saving} onClick={() => install(true)}>
1230
+ {t.apps.install}
1231
+ </Button>
1232
+ )}
1233
+ </div>
1234
+ )}
1235
+ <AppAbout
1236
+ pictures={app.pictures}
1237
+ description={app.description}
1238
+ news={app.store ? newsOf(inStore?.releaseNotes ?? app.releaseNotes, update?.version ?? versionOf(app)) : []}
1239
+ needs={app.needs}
1240
+ notifies={Object.entries(app.notifications?.types ?? {}).map(([id, label]) => ({
1241
+ label: siteText(label),
1242
+ ...(on ? { type: appType(app.id, id) } : {}),
1243
+ }))}
1244
+ cookies={app.cookieList ?? []}
1245
+ consent={!!app.cookies}
1246
+ publisher={app.publisher}
1247
+ licenses={
1248
+ <OwnerLicenses
1249
+ id={`app/${app.id}`}
1250
+ name={app.label}
1251
+ author={app.publisher}
1252
+ version={app.store && versionOf(app)}
1253
+ declared={app}
1254
+ />
1255
+ }
1256
+ >
1257
+ {settings && (
1258
+ <Group>
1259
+ <ListItem href={`#${settings}`} icon={Gear} headline={t.apps.settings} count={settingsHome(app)?.label} arrow />
1260
+ </Group>
1261
+ )}
1262
+ {/* What the app shows itself (its `page`), while it is installed. */}
1263
+ {on && app.page && (
1264
+ <div data-app-page={app.id}>
1265
+ <app.page api={(path, init) => env.appApi(app.id, path, init)} />
1266
+ </div>
1267
+ )}
1268
+ {/* What it adds: its blocks under "Blokke", the lists it brings under "Lister" (as on "Lister", each opening its
1269
+ list), and its kinds under the name of the list they are added to ("Promovering"). */}
1270
+ {addsGroup(
1271
+ t.apps.blocks,
1272
+ adds.filter((a) => a.block),
1273
+ )}
1274
+ {lists.length > 0 && (
1275
+ <Group title={t.apps.lists}>
1276
+ {lists.map((l) => (
1277
+ // Installed, a row opens its list.
1278
+ <ListItem key={l.key} href={on ? l.href : undefined} icon={l.icon} headline={l.label} count={l.count} arrow={on} />
1279
+ ))}
1280
+ </Group>
1281
+ )}
1282
+ {[...new Set(adds.flatMap((a) => (a.block ? [] : [a.more])))].map((title) =>
1283
+ addsGroup(
1284
+ title,
1285
+ adds.filter((a) => !a.block && a.more === title),
1286
+ ),
1287
+ )}
1288
+ </AppAbout>
1289
+ {stuck && (
1290
+ <Group title={t.apps.used}>
1291
+ {uses.map((u) => (
1292
+ <ListItem key={u.href} href={u.href} icon={u.icon} headline={u.title} arrow />
1293
+ ))}
1294
+ </Group>
1295
+ )}
1296
+ {/* As wide as the fields and the rows above. */}
1297
+ {on && (
1298
+ <div>
1299
+ <Button kind="outlined" wide disabled={saving || stuck} onClick={() => install(false)}>
1300
+ {t.apps.uninstall}
1301
+ </Button>
1302
+ <SupportingText className="mt-2">{stuck ? t.apps.stuck : t.apps.uninstallHint}</SupportingText>
1303
+ </div>
1304
+ )}
1305
+ <BottomSheet
1306
+ ref={dialog}
1307
+ aria-label={open?.label}
1308
+ onClose={(e) => e.target === e.currentTarget && setOpenKey(null)}
1309
+ onClick={(e) => e.target === dialog.current && dialog.current?.close()}
1310
+ >
1311
+ <SheetTitle title={open?.label ?? ''} onClose={() => dialog.current?.close()} />
1312
+ {open && (
1313
+ <SheetBody>
1314
+ <div className="flex items-start gap-3">
1315
+ <LeadingIcon icon={open.icon} />
1316
+ <p className="pt-1.5 text-muted">{open.hint ?? open.label}</p>
1317
+ </div>
1318
+ {open.block ? (
1319
+ <BlockSwitch block={open.block} env={env} files={files} />
1320
+ ) : (
1321
+ <KindSwitch app={app} list={open.key} env={env} files={files} />
1322
+ )}
1323
+ {open.create && !offBlocks(files).includes(kindKey(open.key, app.id)) && (
1324
+ <LinkButton href={open.create.href} wide onClick={() => dialog.current?.close()}>
1325
+ {open.create.label}
1326
+ </LinkButton>
1327
+ )}
1328
+ <div>
1329
+ <p className="mb-1 font-medium">{t.apps.used}</p>
1330
+ {open.uses.length ? (
1331
+ <ul className={listClass}>
1332
+ {open.uses.map((u) => (
1333
+ <ListItem key={u.href} href={u.href} onClick={() => dialog.current?.close()} headline={u.title} />
1334
+ ))}
1335
+ </ul>
1336
+ ) : (
1337
+ <p className="text-muted">{t.apps.notUsed}</p>
1338
+ )}
1339
+ </div>
1340
+ </SheetBody>
1341
+ )}
1342
+ </BottomSheet>
1343
+ </div>
1344
+ );
1345
+ }