@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (908) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +51 -51
  4. package/astro.mjs +162 -162
  5. package/bin/morpheme.mjs +38 -38
  6. package/client.d.ts +10 -10
  7. package/defaults/blockTexts.ts +33 -33
  8. package/defaults/collections.ts +774 -774
  9. package/defaults/colors.ts +125 -125
  10. package/defaults/company.ts +20 -20
  11. package/defaults/editor.ts +178 -178
  12. package/defaults/internalLinks.ts +30 -30
  13. package/defaults/opens.ts +18 -18
  14. package/defaults/popup.tsx +93 -93
  15. package/defaults/privacy.ts +79 -79
  16. package/defaults/rules.ts +450 -450
  17. package/defaults/schema.ts +16 -16
  18. package/defaults/sitedata.ts +129 -129
  19. package/defaults/standard.ts +106 -106
  20. package/defaults/standardPages.ts +87 -87
  21. package/defaults/texts.ts +481 -481
  22. package/demo/README.md +26 -26
  23. package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
  24. package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
  25. package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
  26. package/demo/src/content/blog/every-save-is-a-version.md +21 -21
  27. package/demo/src/content/blog/schedule-a-post.md +17 -17
  28. package/demo/src/content/blog/secure-from-day-one.md +17 -17
  29. package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
  30. package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
  31. package/demo/src/content/blog/the-app-store.md +21 -21
  32. package/demo/src/content/pages/about.md +22 -22
  33. package/demo/src/content/pages/app-store.md +30 -30
  34. package/demo/src/content/pages/blocks.md +98 -98
  35. package/demo/src/content/pages/blog-page.md +11 -11
  36. package/demo/src/content/pages/contact.md +16 -16
  37. package/demo/src/content/pages/get-started.md +20 -20
  38. package/demo/src/content/pages/home.md +26 -26
  39. package/demo/src/content/pages/page-not-found.md +11 -11
  40. package/demo/src/content/pages/privacy-policy.md +24 -24
  41. package/demo/src/data/blocks.json +25 -25
  42. package/demo/src/data/booking.json +6 -6
  43. package/demo/src/data/categories/apps.json +6 -6
  44. package/demo/src/data/categories/design.json +6 -6
  45. package/demo/src/data/categories/editing.json +6 -6
  46. package/demo/src/data/categories/security.json +6 -6
  47. package/demo/src/data/categories/seo.json +6 -6
  48. package/demo/src/data/colors.json +75 -75
  49. package/demo/src/data/contact.json +8 -8
  50. package/demo/src/data/cookies.json +21 -21
  51. package/demo/src/data/footer.json +27 -27
  52. package/demo/src/data/menu.json +35 -35
  53. package/demo/src/data/mobile-menu.json +25 -25
  54. package/demo/src/data/popups/booking-bar.json +13 -13
  55. package/demo/src/data/popups/try-the-editor.json +16 -16
  56. package/demo/src/data/standard-pages.json +8 -8
  57. package/demo/src/data/testimonials/sample-bakery.json +10 -10
  58. package/demo/src/data/testimonials/sample-clinic.json +10 -10
  59. package/demo/src/data/testimonials/sample-studio.json +10 -10
  60. package/demo/src/site/colors.ts +11 -11
  61. package/demo/src/site/config.ts +19 -19
  62. package/icons/lucide/LICENSE +43 -43
  63. package/icons/lucide/address-book.svg +1 -1
  64. package/icons/lucide/app-window.svg +1 -1
  65. package/icons/lucide/arrow-counter-clockwise.svg +1 -1
  66. package/icons/lucide/arrow-down.svg +1 -1
  67. package/icons/lucide/arrow-left.svg +1 -1
  68. package/icons/lucide/arrow-right.svg +1 -1
  69. package/icons/lucide/arrow-square-out.svg +1 -1
  70. package/icons/lucide/arrow-u-up-left.svg +1 -1
  71. package/icons/lucide/arrow-u-up-right.svg +1 -1
  72. package/icons/lucide/arrow-up-right.svg +1 -1
  73. package/icons/lucide/arrow-up.svg +1 -1
  74. package/icons/lucide/arrows-down-up.svg +1 -1
  75. package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
  76. package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
  77. package/icons/lucide/article.svg +1 -1
  78. package/icons/lucide/bank.svg +1 -1
  79. package/icons/lucide/bell.svg +1 -1
  80. package/icons/lucide/bicycle.svg +1 -1
  81. package/icons/lucide/binoculars.svg +1 -1
  82. package/icons/lucide/book-open-text.svg +1 -1
  83. package/icons/lucide/book-open.svg +1 -1
  84. package/icons/lucide/browser.svg +1 -1
  85. package/icons/lucide/buildings.svg +1 -1
  86. package/icons/lucide/bus.svg +1 -1
  87. package/icons/lucide/calendar-blank.svg +1 -1
  88. package/icons/lucide/calendar-check.svg +1 -1
  89. package/icons/lucide/car.svg +1 -1
  90. package/icons/lucide/cards.svg +1 -1
  91. package/icons/lucide/caret-down.svg +1 -1
  92. package/icons/lucide/caret-left.svg +1 -1
  93. package/icons/lucide/caret-right.svg +1 -1
  94. package/icons/lucide/caret-up.svg +1 -1
  95. package/icons/lucide/certificate.svg +1 -1
  96. package/icons/lucide/chart-line-up.svg +1 -1
  97. package/icons/lucide/chats.svg +1 -1
  98. package/icons/lucide/check-circle.svg +1 -1
  99. package/icons/lucide/check.svg +1 -1
  100. package/icons/lucide/circle-half.svg +1 -1
  101. package/icons/lucide/circle.svg +1 -1
  102. package/icons/lucide/clock-counter-clockwise.svg +1 -1
  103. package/icons/lucide/clock.svg +1 -1
  104. package/icons/lucide/code.svg +1 -1
  105. package/icons/lucide/coffee.svg +1 -1
  106. package/icons/lucide/columns.svg +1 -1
  107. package/icons/lucide/cookie.svg +1 -1
  108. package/icons/lucide/copy.svg +1 -1
  109. package/icons/lucide/credit-card.svg +1 -1
  110. package/icons/lucide/currency-circle-dollar.svg +1 -1
  111. package/icons/lucide/cursor-click.svg +1 -1
  112. package/icons/lucide/database.svg +1 -1
  113. package/icons/lucide/desktop.svg +1 -1
  114. package/icons/lucide/device-mobile.svg +1 -1
  115. package/icons/lucide/door-open.svg +1 -1
  116. package/icons/lucide/download-simple.svg +1 -1
  117. package/icons/lucide/envelope-simple.svg +1 -1
  118. package/icons/lucide/eye-slash.svg +1 -1
  119. package/icons/lucide/eye.svg +1 -1
  120. package/icons/lucide/file-pdf.svg +1 -1
  121. package/icons/lucide/file-text.svg +1 -1
  122. package/icons/lucide/file-video.svg +1 -1
  123. package/icons/lucide/file-zip.svg +1 -1
  124. package/icons/lucide/flower-lotus.svg +1 -1
  125. package/icons/lucide/folder-simple.svg +1 -1
  126. package/icons/lucide/funnel.svg +1 -1
  127. package/icons/lucide/gauge.svg +1 -1
  128. package/icons/lucide/gear.svg +1 -1
  129. package/icons/lucide/github-logo.svg +1 -1
  130. package/icons/lucide/globe.svg +1 -1
  131. package/icons/lucide/google-logo.svg +1 -1
  132. package/icons/lucide/graduation-cap.svg +1 -1
  133. package/icons/lucide/hammer.svg +1 -1
  134. package/icons/lucide/hand-heart.svg +1 -1
  135. package/icons/lucide/handshake.svg +1 -1
  136. package/icons/lucide/hard-drives.svg +1 -1
  137. package/icons/lucide/heart.svg +1 -1
  138. package/icons/lucide/house.svg +1 -1
  139. package/icons/lucide/image-square.svg +1 -1
  140. package/icons/lucide/image.svg +1 -1
  141. package/icons/lucide/info.svg +1 -1
  142. package/icons/lucide/keyboard.svg +1 -1
  143. package/icons/lucide/leaf.svg +1 -1
  144. package/icons/lucide/lifebuoy.svg +1 -1
  145. package/icons/lucide/lightbulb.svg +1 -1
  146. package/icons/lucide/link-break.svg +1 -1
  147. package/icons/lucide/link-simple.svg +1 -1
  148. package/icons/lucide/list-bullets.svg +1 -1
  149. package/icons/lucide/list-checks.svg +1 -1
  150. package/icons/lucide/list-numbers.svg +1 -1
  151. package/icons/lucide/list.svg +1 -1
  152. package/icons/lucide/lock-simple-open.svg +1 -1
  153. package/icons/lucide/lock-simple.svg +1 -1
  154. package/icons/lucide/magnifying-glass.svg +1 -1
  155. package/icons/lucide/map-pin.svg +1 -1
  156. package/icons/lucide/map-trifold.svg +1 -1
  157. package/icons/lucide/medal.svg +1 -1
  158. package/icons/lucide/megaphone.svg +1 -1
  159. package/icons/lucide/minus.svg +1 -1
  160. package/icons/lucide/moon.svg +1 -1
  161. package/icons/lucide/newspaper.svg +1 -1
  162. package/icons/lucide/pack.json +23 -23
  163. package/icons/lucide/paint-brush-broad.svg +1 -1
  164. package/icons/lucide/palette.svg +1 -1
  165. package/icons/lucide/paper-plane-tilt.svg +1 -1
  166. package/icons/lucide/pencil-simple.svg +1 -1
  167. package/icons/lucide/phone.svg +1 -1
  168. package/icons/lucide/play-fill.svg +1 -1
  169. package/icons/lucide/plus.svg +1 -1
  170. package/icons/lucide/prohibit.svg +1 -1
  171. package/icons/lucide/puzzle-piece.svg +1 -1
  172. package/icons/lucide/question.svg +1 -1
  173. package/icons/lucide/quotes.svg +1 -1
  174. package/icons/lucide/rows.svg +1 -1
  175. package/icons/lucide/ruler.svg +1 -1
  176. package/icons/lucide/scan.svg +1 -1
  177. package/icons/lucide/shapes.svg +1 -1
  178. package/icons/lucide/share-network.svg +1 -1
  179. package/icons/lucide/shield-check.svg +1 -1
  180. package/icons/lucide/sidebar-simple.svg +1 -1
  181. package/icons/lucide/sign-out.svg +1 -1
  182. package/icons/lucide/signpost.svg +1 -1
  183. package/icons/lucide/sliders-horizontal.svg +1 -1
  184. package/icons/lucide/smiley.svg +1 -1
  185. package/icons/lucide/sparkle.svg +1 -1
  186. package/icons/lucide/square-half-bottom.svg +1 -1
  187. package/icons/lucide/square.svg +1 -1
  188. package/icons/lucide/squares-four.svg +1 -1
  189. package/icons/lucide/sticker.svg +1 -1
  190. package/icons/lucide/storefront.svg +1 -1
  191. package/icons/lucide/sun.svg +1 -1
  192. package/icons/lucide/swatches.svg +1 -1
  193. package/icons/lucide/tag.svg +1 -1
  194. package/icons/lucide/text-aa.svg +1 -1
  195. package/icons/lucide/text-align-left.svg +1 -1
  196. package/icons/lucide/text-b.svg +1 -1
  197. package/icons/lucide/text-h-one.svg +1 -1
  198. package/icons/lucide/text-h-three.svg +1 -1
  199. package/icons/lucide/text-h-two.svg +1 -1
  200. package/icons/lucide/text-italic.svg +1 -1
  201. package/icons/lucide/text-t.svg +1 -1
  202. package/icons/lucide/ticket.svg +1 -1
  203. package/icons/lucide/toggle-right.svg +1 -1
  204. package/icons/lucide/train.svg +1 -1
  205. package/icons/lucide/trash.svg +1 -1
  206. package/icons/lucide/upload-simple.svg +1 -1
  207. package/icons/lucide/user-circle.svg +1 -1
  208. package/icons/lucide/user.svg +1 -1
  209. package/icons/lucide/users-three.svg +1 -1
  210. package/icons/lucide/users.svg +1 -1
  211. package/icons/lucide/video-camera.svg +1 -1
  212. package/icons/lucide/warning-circle.svg +1 -1
  213. package/icons/lucide/warning.svg +1 -1
  214. package/icons/lucide/wave-sine.svg +1 -1
  215. package/icons/lucide/wheelchair.svg +1 -1
  216. package/icons/lucide/wrench.svg +1 -1
  217. package/icons/lucide/x.svg +1 -1
  218. package/icons/material-symbols/LICENSE +201 -201
  219. package/icons/material-symbols/address-book.svg +1 -1
  220. package/icons/material-symbols/app-window.svg +1 -1
  221. package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
  222. package/icons/material-symbols/arrow-down.svg +1 -1
  223. package/icons/material-symbols/arrow-left.svg +1 -1
  224. package/icons/material-symbols/arrow-right.svg +1 -1
  225. package/icons/material-symbols/arrow-square-out.svg +1 -1
  226. package/icons/material-symbols/arrow-u-up-left.svg +1 -1
  227. package/icons/material-symbols/arrow-u-up-right.svg +1 -1
  228. package/icons/material-symbols/arrow-up-right.svg +1 -1
  229. package/icons/material-symbols/arrow-up.svg +1 -1
  230. package/icons/material-symbols/arrows-down-up.svg +1 -1
  231. package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
  232. package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
  233. package/icons/material-symbols/article.svg +1 -1
  234. package/icons/material-symbols/bank.svg +1 -1
  235. package/icons/material-symbols/bell.svg +1 -1
  236. package/icons/material-symbols/bicycle.svg +1 -1
  237. package/icons/material-symbols/binoculars.svg +1 -1
  238. package/icons/material-symbols/book-open-text.svg +1 -1
  239. package/icons/material-symbols/book-open.svg +1 -1
  240. package/icons/material-symbols/browser.svg +1 -1
  241. package/icons/material-symbols/buildings.svg +1 -1
  242. package/icons/material-symbols/bus.svg +1 -1
  243. package/icons/material-symbols/calendar-blank.svg +1 -1
  244. package/icons/material-symbols/calendar-check.svg +1 -1
  245. package/icons/material-symbols/car.svg +1 -1
  246. package/icons/material-symbols/cards.svg +1 -1
  247. package/icons/material-symbols/caret-down.svg +1 -1
  248. package/icons/material-symbols/caret-left.svg +1 -1
  249. package/icons/material-symbols/caret-right.svg +1 -1
  250. package/icons/material-symbols/caret-up.svg +1 -1
  251. package/icons/material-symbols/certificate.svg +1 -1
  252. package/icons/material-symbols/chart-line-up.svg +1 -1
  253. package/icons/material-symbols/chats.svg +1 -1
  254. package/icons/material-symbols/check-circle.svg +1 -1
  255. package/icons/material-symbols/check.svg +1 -1
  256. package/icons/material-symbols/circle-half.svg +1 -1
  257. package/icons/material-symbols/circle.svg +1 -1
  258. package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
  259. package/icons/material-symbols/clock.svg +1 -1
  260. package/icons/material-symbols/code.svg +1 -1
  261. package/icons/material-symbols/coffee.svg +1 -1
  262. package/icons/material-symbols/columns.svg +1 -1
  263. package/icons/material-symbols/cookie.svg +1 -1
  264. package/icons/material-symbols/copy.svg +1 -1
  265. package/icons/material-symbols/credit-card.svg +1 -1
  266. package/icons/material-symbols/currency-circle-dollar.svg +1 -1
  267. package/icons/material-symbols/cursor-click.svg +1 -1
  268. package/icons/material-symbols/database.svg +1 -1
  269. package/icons/material-symbols/desktop.svg +1 -1
  270. package/icons/material-symbols/device-mobile.svg +1 -1
  271. package/icons/material-symbols/door-open.svg +1 -1
  272. package/icons/material-symbols/download-simple.svg +1 -1
  273. package/icons/material-symbols/envelope-simple.svg +1 -1
  274. package/icons/material-symbols/eye-slash.svg +1 -1
  275. package/icons/material-symbols/eye.svg +1 -1
  276. package/icons/material-symbols/file-pdf.svg +1 -1
  277. package/icons/material-symbols/file-text.svg +1 -1
  278. package/icons/material-symbols/file-video.svg +1 -1
  279. package/icons/material-symbols/file-zip.svg +1 -1
  280. package/icons/material-symbols/flower-lotus.svg +1 -1
  281. package/icons/material-symbols/folder-simple.svg +1 -1
  282. package/icons/material-symbols/funnel.svg +1 -1
  283. package/icons/material-symbols/gauge.svg +1 -1
  284. package/icons/material-symbols/gear.svg +1 -1
  285. package/icons/material-symbols/github-logo.svg +1 -1
  286. package/icons/material-symbols/globe.svg +1 -1
  287. package/icons/material-symbols/google-logo.svg +1 -1
  288. package/icons/material-symbols/graduation-cap.svg +1 -1
  289. package/icons/material-symbols/hammer.svg +1 -1
  290. package/icons/material-symbols/hand-heart.svg +1 -1
  291. package/icons/material-symbols/handshake.svg +1 -1
  292. package/icons/material-symbols/hard-drives.svg +1 -1
  293. package/icons/material-symbols/heart.svg +1 -1
  294. package/icons/material-symbols/house.svg +1 -1
  295. package/icons/material-symbols/image-square.svg +1 -1
  296. package/icons/material-symbols/image.svg +1 -1
  297. package/icons/material-symbols/info.svg +1 -1
  298. package/icons/material-symbols/keyboard.svg +1 -1
  299. package/icons/material-symbols/leaf.svg +1 -1
  300. package/icons/material-symbols/lifebuoy.svg +1 -1
  301. package/icons/material-symbols/lightbulb.svg +1 -1
  302. package/icons/material-symbols/link-break.svg +1 -1
  303. package/icons/material-symbols/link-simple.svg +1 -1
  304. package/icons/material-symbols/list-bullets.svg +1 -1
  305. package/icons/material-symbols/list-checks.svg +1 -1
  306. package/icons/material-symbols/list-numbers.svg +1 -1
  307. package/icons/material-symbols/list.svg +1 -1
  308. package/icons/material-symbols/lock-simple-open.svg +1 -1
  309. package/icons/material-symbols/lock-simple.svg +1 -1
  310. package/icons/material-symbols/magnifying-glass.svg +1 -1
  311. package/icons/material-symbols/map-pin.svg +1 -1
  312. package/icons/material-symbols/map-trifold.svg +1 -1
  313. package/icons/material-symbols/medal.svg +1 -1
  314. package/icons/material-symbols/megaphone.svg +1 -1
  315. package/icons/material-symbols/minus.svg +1 -1
  316. package/icons/material-symbols/moon.svg +1 -1
  317. package/icons/material-symbols/newspaper.svg +1 -1
  318. package/icons/material-symbols/pack.json +29 -29
  319. package/icons/material-symbols/paint-brush-broad.svg +1 -1
  320. package/icons/material-symbols/palette.svg +1 -1
  321. package/icons/material-symbols/paper-plane-tilt.svg +1 -1
  322. package/icons/material-symbols/pencil-simple.svg +1 -1
  323. package/icons/material-symbols/phone.svg +1 -1
  324. package/icons/material-symbols/play-fill.svg +1 -1
  325. package/icons/material-symbols/plus.svg +1 -1
  326. package/icons/material-symbols/prohibit.svg +1 -1
  327. package/icons/material-symbols/puzzle-piece.svg +1 -1
  328. package/icons/material-symbols/question.svg +1 -1
  329. package/icons/material-symbols/quotes.svg +1 -1
  330. package/icons/material-symbols/rows.svg +1 -1
  331. package/icons/material-symbols/ruler.svg +1 -1
  332. package/icons/material-symbols/scan.svg +1 -1
  333. package/icons/material-symbols/shapes.svg +1 -1
  334. package/icons/material-symbols/share-network.svg +1 -1
  335. package/icons/material-symbols/shield-check.svg +1 -1
  336. package/icons/material-symbols/sidebar-simple.svg +1 -1
  337. package/icons/material-symbols/sign-out.svg +1 -1
  338. package/icons/material-symbols/signpost.svg +1 -1
  339. package/icons/material-symbols/sliders-horizontal.svg +1 -1
  340. package/icons/material-symbols/smiley.svg +1 -1
  341. package/icons/material-symbols/sparkle.svg +1 -1
  342. package/icons/material-symbols/square-half-bottom.svg +1 -1
  343. package/icons/material-symbols/square.svg +1 -1
  344. package/icons/material-symbols/squares-four.svg +1 -1
  345. package/icons/material-symbols/sticker.svg +1 -1
  346. package/icons/material-symbols/storefront.svg +1 -1
  347. package/icons/material-symbols/sun.svg +1 -1
  348. package/icons/material-symbols/swatches.svg +1 -1
  349. package/icons/material-symbols/tag.svg +1 -1
  350. package/icons/material-symbols/text-aa.svg +1 -1
  351. package/icons/material-symbols/text-align-left.svg +1 -1
  352. package/icons/material-symbols/text-b.svg +1 -1
  353. package/icons/material-symbols/text-h-one.svg +1 -1
  354. package/icons/material-symbols/text-h-three.svg +1 -1
  355. package/icons/material-symbols/text-h-two.svg +1 -1
  356. package/icons/material-symbols/text-italic.svg +1 -1
  357. package/icons/material-symbols/text-t.svg +1 -1
  358. package/icons/material-symbols/ticket.svg +1 -1
  359. package/icons/material-symbols/toggle-right.svg +1 -1
  360. package/icons/material-symbols/train.svg +1 -1
  361. package/icons/material-symbols/trash.svg +1 -1
  362. package/icons/material-symbols/upload-simple.svg +1 -1
  363. package/icons/material-symbols/user-circle.svg +1 -1
  364. package/icons/material-symbols/user.svg +1 -1
  365. package/icons/material-symbols/users-three.svg +1 -1
  366. package/icons/material-symbols/users.svg +1 -1
  367. package/icons/material-symbols/video-camera.svg +1 -1
  368. package/icons/material-symbols/warning-circle.svg +1 -1
  369. package/icons/material-symbols/warning.svg +1 -1
  370. package/icons/material-symbols/wave-sine.svg +1 -1
  371. package/icons/material-symbols/wheelchair.svg +1 -1
  372. package/icons/material-symbols/wrench.svg +1 -1
  373. package/icons/material-symbols/x.svg +1 -1
  374. package/icons/phosphor/pack.json +8 -8
  375. package/lib/apps.mjs +142 -139
  376. package/lib/apps.test.d.mts +1 -0
  377. package/lib/apps.test.mjs +50 -0
  378. package/lib/blocks.mjs +90 -90
  379. package/lib/caseCollisions.d.mts +4 -0
  380. package/lib/caseCollisions.mjs +62 -0
  381. package/lib/caseCollisions.test.d.mts +1 -0
  382. package/lib/caseCollisions.test.mjs +74 -0
  383. package/lib/colors.ts +86 -86
  384. package/lib/contactBar.ts +18 -18
  385. package/lib/contracts.d.mts +15 -0
  386. package/lib/contracts.mjs +17 -0
  387. package/lib/cookieCategories.mjs +28 -28
  388. package/lib/crop.mjs +83 -83
  389. package/lib/csp.mjs +130 -130
  390. package/lib/cspSources.mjs +60 -60
  391. package/lib/dynamic-colors.ts +426 -426
  392. package/lib/git.mjs +64 -64
  393. package/lib/hooks.ts +51 -51
  394. package/lib/inline.ts +20 -20
  395. package/lib/license-texts/Apache-2.0.txt +190 -190
  396. package/lib/license-texts/BSD-2-Clause.txt +24 -24
  397. package/lib/license-texts/BSD-3-Clause.txt +28 -28
  398. package/lib/license-texts/ISC.txt +15 -15
  399. package/lib/license-texts/MIT.txt +21 -21
  400. package/lib/licenses.d.mts +1 -1
  401. package/lib/licenses.mjs +683 -683
  402. package/lib/markdown.ts +42 -42
  403. package/lib/motion-schemes.mjs +5 -5
  404. package/lib/motion.ts +131 -131
  405. package/lib/notice.mjs +34 -34
  406. package/lib/scheduled.mjs +37 -37
  407. package/lib/snackbar.ts +149 -149
  408. package/lib/status.mjs +18 -18
  409. package/lib/storeEntries.d.mts +18 -0
  410. package/lib/storeEntries.mjs +64 -0
  411. package/lib/storeEntries.test.d.mts +1 -0
  412. package/lib/storeEntries.test.mjs +71 -0
  413. package/lib/svg.mjs +231 -231
  414. package/lib/text-colors.mjs +43 -43
  415. package/lib/video-id.mjs +72 -72
  416. package/lib/video.mjs +91 -91
  417. package/package.json +136 -131
  418. package/pages/404.astro +10 -10
  419. package/pages/[...slug].astro +51 -51
  420. package/pages/_headers.txt +31 -31
  421. package/pages/blog/index.astro +10 -10
  422. package/pages/blog/kategori/[category].astro +18 -18
  423. package/pages/editor/code-images.json.js +25 -25
  424. package/pages/editor/index.astro +75 -75
  425. package/pages/editor/manifest.webmanifest.js +23 -23
  426. package/pages/editor/sw.js +33 -33
  427. package/pages/editor/sw.js.js +4 -4
  428. package/pages/editor/thumbnails.json.js +21 -21
  429. package/pages/editor/version.json.js +22 -22
  430. package/pages/headers.js +4 -4
  431. package/pages/index.astro +10 -10
  432. package/pages/ny.astro +6 -6
  433. package/pages/robots.txt.js +3 -3
  434. package/pages/rss.xml.js +20 -20
  435. package/scripts/app-screenshots.mjs +164 -164
  436. package/scripts/app-workers.mjs +6 -6
  437. package/scripts/bin.mjs +12 -12
  438. package/scripts/built-site.mjs +134 -99
  439. package/scripts/check-consent.mjs +113 -113
  440. package/scripts/check-content.mjs +151 -149
  441. package/scripts/check-demo.mjs +24 -24
  442. package/scripts/check-store.mjs +160 -0
  443. package/scripts/check-themes.mjs +45 -45
  444. package/scripts/check-worker.mjs +569 -569
  445. package/scripts/compare-html.mjs +81 -81
  446. package/scripts/declarations.mjs +10 -0
  447. package/scripts/declarations.test.mjs +32 -0
  448. package/scripts/demo-site.mjs +42 -42
  449. package/scripts/editor-icons.mjs +56 -56
  450. package/scripts/editor-roundtrip.mjs +134 -134
  451. package/scripts/editor-screens.mjs +101 -101
  452. package/scripts/git-env.mjs +9 -9
  453. package/scripts/icon-packs.mjs +151 -151
  454. package/scripts/link-counts.mjs +22 -22
  455. package/scripts/page-colors.mjs +42 -42
  456. package/scripts/releases.mjs +80 -80
  457. package/scripts/responsive-images.mjs +94 -94
  458. package/scripts/scheduled-list.mjs +15 -15
  459. package/scripts/theme-screenshots.mjs +88 -88
  460. package/scripts/types.mjs +17 -21
  461. package/scripts/video-thumbnails.mjs +89 -89
  462. package/scripts/worker-modules.mjs +34 -34
  463. package/site/appCookies.mjs +74 -74
  464. package/site/apps.ts +21 -21
  465. package/site/autolinks.mjs +200 -200
  466. package/site/blockScripts.ts +31 -31
  467. package/site/categories.ts +16 -16
  468. package/site/consent.ts +26 -26
  469. package/site/content.config.ts +84 -84
  470. package/site/content.ts +104 -104
  471. package/site/cookieList.ts +17 -17
  472. package/site/defineSite.d.mts +2 -14
  473. package/site/defineSite.mjs +131 -136
  474. package/site/directives/interact.mjs +31 -31
  475. package/site/editor-version.mjs +26 -26
  476. package/site/fill.ts +13 -13
  477. package/site/format.ts +11 -11
  478. package/site/home.ts +66 -66
  479. package/site/iconPacks.mjs +43 -43
  480. package/site/icons.mjs +605 -605
  481. package/site/images.mjs +157 -157
  482. package/site/motion.ts +13 -13
  483. package/site/noindex.mjs +21 -21
  484. package/site/pageColors.ts +110 -110
  485. package/site/popups.ts +98 -98
  486. package/site/schema.ts +189 -189
  487. package/site/sections.ts +3 -3
  488. package/site/seo.ts +28 -28
  489. package/site/site.ts +76 -76
  490. package/site/siteConfig.mjs +8 -8
  491. package/site/siteCsp.mjs +75 -75
  492. package/site/siteModules.mjs +27 -27
  493. package/site/standardPages.ts +59 -59
  494. package/site/storeTheme.d.mts +6 -0
  495. package/site/storeTheme.mjs +29 -0
  496. package/site/storeTheme.test.d.mts +1 -0
  497. package/site/storeTheme.test.mjs +53 -0
  498. package/site/testimonials.ts +48 -48
  499. package/site/text.ts +43 -43
  500. package/site/theme.mjs +483 -472
  501. package/site/tracking.client.ts +225 -225
  502. package/site/tracking.ts +9 -9
  503. package/src/ActionParts.tsx +205 -205
  504. package/src/Advanced.tsx +152 -152
  505. package/src/App.tsx +901 -904
  506. package/src/AppList.tsx +1362 -1345
  507. package/src/BlockDialog.tsx +360 -360
  508. package/src/BlockParts.tsx +652 -652
  509. package/src/BlockSettings.tsx +533 -533
  510. package/src/BrandForm.tsx +164 -164
  511. package/src/Colors.tsx +476 -476
  512. package/src/CommunicationParts.tsx +199 -199
  513. package/src/CompanyForm.tsx +209 -209
  514. package/src/Consents.tsx +175 -175
  515. package/src/ContainmentParts.tsx +185 -185
  516. package/src/Crop.tsx +152 -152
  517. package/src/DataForm.tsx +434 -434
  518. package/src/Deleted.tsx +177 -177
  519. package/src/EntryForm.tsx +740 -740
  520. package/src/FileView.tsx +30 -30
  521. package/src/FormFrame.tsx +237 -237
  522. package/src/Keywords.tsx +225 -225
  523. package/src/Library.tsx +1563 -1563
  524. package/src/Licenses.tsx +336 -336
  525. package/src/LinkSheet.tsx +138 -138
  526. package/src/MarkdownEditor.tsx +1075 -1075
  527. package/src/Media.tsx +677 -677
  528. package/src/NavigationParts.tsx +350 -350
  529. package/src/NavigationRestParts.tsx +247 -247
  530. package/src/Notifications.tsx +59 -59
  531. package/src/PagePicker.tsx +136 -136
  532. package/src/Parts.tsx +991 -991
  533. package/src/PartsShown.tsx +168 -168
  534. package/src/PickerParts.tsx +183 -183
  535. package/src/Redirects.tsx +450 -450
  536. package/src/Screen.tsx +184 -184
  537. package/src/SectionsField.tsx +65 -65
  538. package/src/SelectionParts.tsx +134 -134
  539. package/src/SeoScores.tsx +184 -184
  540. package/src/Sheet.tsx +35 -35
  541. package/src/ThemeStore.tsx +376 -376
  542. package/src/Tokens.tsx +346 -346
  543. package/src/Versions.tsx +214 -214
  544. package/src/addresses.ts +75 -75
  545. package/src/allIcons.ts +7 -7
  546. package/src/appFiles.ts +29 -29
  547. package/src/apps.ts +281 -281
  548. package/src/ask.tsx +123 -123
  549. package/src/blocks.ts +251 -251
  550. package/src/brand.ts +153 -153
  551. package/src/cache.ts +42 -42
  552. package/src/check.ts +285 -285
  553. package/src/checkSite.ts +256 -256
  554. package/src/{consents.ts → consentData.ts} +26 -26
  555. package/src/cookieList.ts +70 -70
  556. package/src/countries.ts +59 -59
  557. package/src/deploy.ts +155 -155
  558. package/src/editor.css +602 -602
  559. package/src/examples.ts +85 -85
  560. package/src/fields.tsx +942 -942
  561. package/src/frontmatter.mjs +42 -42
  562. package/src/github.ts +376 -376
  563. package/src/i18n/da-DK/advanced.ts +28 -28
  564. package/src/i18n/da-DK/app.ts +71 -71
  565. package/src/i18n/da-DK/apps.ts +90 -90
  566. package/src/i18n/da-DK/blockSettings.ts +38 -38
  567. package/src/i18n/da-DK/blocks.ts +408 -408
  568. package/src/i18n/da-DK/check.ts +25 -25
  569. package/src/i18n/da-DK/colors.ts +64 -64
  570. package/src/i18n/da-DK/common.ts +118 -118
  571. package/src/i18n/da-DK/company.ts +24 -24
  572. package/src/i18n/da-DK/consents.ts +23 -23
  573. package/src/i18n/da-DK/forms.ts +116 -116
  574. package/src/i18n/da-DK/history.ts +35 -35
  575. package/src/i18n/da-DK/index.ts +49 -49
  576. package/src/i18n/da-DK/keywords.ts +19 -19
  577. package/src/i18n/da-DK/library.ts +135 -135
  578. package/src/i18n/da-DK/licenses.ts +25 -25
  579. package/src/i18n/da-DK/media.ts +79 -79
  580. package/src/i18n/da-DK/parts.ts +696 -696
  581. package/src/i18n/da-DK/preview.ts +13 -13
  582. package/src/i18n/da-DK/redirects.ts +51 -51
  583. package/src/i18n/da-DK/seo.ts +20 -20
  584. package/src/i18n/da-DK/text.ts +35 -35
  585. package/src/i18n/da-DK/theme.ts +41 -41
  586. package/src/i18n/en-US/advanced.ts +32 -32
  587. package/src/i18n/en-US/app.ts +84 -84
  588. package/src/i18n/en-US/apps.ts +134 -134
  589. package/src/i18n/en-US/blockSettings.ts +48 -48
  590. package/src/i18n/en-US/blocks.ts +441 -441
  591. package/src/i18n/en-US/check.ts +31 -31
  592. package/src/i18n/en-US/colors.ts +82 -82
  593. package/src/i18n/en-US/common.ts +144 -144
  594. package/src/i18n/en-US/company.ts +25 -25
  595. package/src/i18n/en-US/consents.ts +25 -25
  596. package/src/i18n/en-US/forms.ts +145 -145
  597. package/src/i18n/en-US/history.ts +43 -43
  598. package/src/i18n/en-US/index.ts +51 -51
  599. package/src/i18n/en-US/keywords.ts +24 -24
  600. package/src/i18n/en-US/library.ts +176 -176
  601. package/src/i18n/en-US/licenses.ts +36 -36
  602. package/src/i18n/en-US/media.ts +80 -80
  603. package/src/i18n/en-US/parts.ts +718 -718
  604. package/src/i18n/en-US/preview.ts +15 -15
  605. package/src/i18n/en-US/redirects.ts +66 -66
  606. package/src/i18n/en-US/seo.ts +26 -26
  607. package/src/i18n/en-US/text.ts +41 -41
  608. package/src/i18n/en-US/theme.ts +41 -41
  609. package/src/i18n/index.ts +124 -124
  610. package/src/icon.tsx +160 -160
  611. package/src/iconPacks.ts +89 -89
  612. package/src/images.ts +163 -163
  613. package/src/{licenses.ts → licenseData.ts} +79 -79
  614. package/src/markdown.mjs +204 -204
  615. package/src/nav.ts +68 -68
  616. package/src/notFound.ts +28 -28
  617. package/src/{notifications.ts → notificationData.ts} +223 -223
  618. package/src/pagePreview.tsx +204 -204
  619. package/src/r2Caching.ts +55 -55
  620. package/src/{redirects.ts → redirectData.ts} +149 -149
  621. package/src/registry.tsx +188 -188
  622. package/src/schema.ts +417 -417
  623. package/src/sections.mjs +303 -303
  624. package/src/seo.ts +333 -333
  625. package/src/shared.ts +44 -44
  626. package/src/sidebar.ts +31 -31
  627. package/src/site.ts +317 -317
  628. package/src/standard/Block.astro +19 -19
  629. package/src/standard/BlockSite.astro +18 -18
  630. package/src/standard/accordion/index.tsx +47 -47
  631. package/src/standard/accordion/load.ts +23 -23
  632. package/src/standard/accordion/shared.ts +15 -15
  633. package/src/standard/accordion/view.ts +32 -32
  634. package/src/standard/buttons/fields.ts +98 -98
  635. package/src/standard/buttons/index.tsx +23 -23
  636. package/src/standard/buttons/load.ts +34 -34
  637. package/src/standard/buttons/shared.ts +12 -12
  638. package/src/standard/buttons/view.ts +30 -30
  639. package/src/standard/cards/index.tsx +71 -71
  640. package/src/standard/cards/load.ts +17 -17
  641. package/src/standard/cards/shared.ts +27 -27
  642. package/src/standard/cards/view.ts +29 -29
  643. package/src/standard/checklist/index.tsx +27 -27
  644. package/src/standard/checklist/load.ts +5 -5
  645. package/src/standard/checklist/view.ts +13 -13
  646. package/src/standard/cookies/index.tsx +17 -17
  647. package/src/standard/cookies/list.ts +56 -56
  648. package/src/standard/cookies/load.ts +11 -11
  649. package/src/standard/cookies/shared.ts +20 -20
  650. package/src/standard/cookies/view.ts +28 -28
  651. package/src/standard/faq/index.tsx +29 -29
  652. package/src/standard/faq/load.ts +14 -14
  653. package/src/standard/faq/view.ts +21 -21
  654. package/src/standard/head.ts +23 -23
  655. package/src/standard/hero/fields.ts +48 -48
  656. package/src/standard/hero/index.tsx +57 -57
  657. package/src/standard/hero/load.ts +23 -23
  658. package/src/standard/hero/view.ts +38 -38
  659. package/src/standard/links/index.tsx +41 -41
  660. package/src/standard/links/load.ts +17 -17
  661. package/src/standard/links/view.ts +25 -25
  662. package/src/standard/list/index.tsx +50 -50
  663. package/src/standard/list/load.ts +5 -5
  664. package/src/standard/list/view.ts +18 -18
  665. package/src/standard/load.ts +7 -7
  666. package/src/standard/notice/index.tsx +30 -30
  667. package/src/standard/notice/load.ts +16 -16
  668. package/src/standard/notice/view.ts +10 -10
  669. package/src/standard/points/index.tsx +42 -42
  670. package/src/standard/points/load.ts +5 -5
  671. package/src/standard/points/view.ts +12 -12
  672. package/src/standard/posts/index.tsx +132 -132
  673. package/src/standard/posts/load.ts +36 -36
  674. package/src/standard/posts/shared.ts +21 -21
  675. package/src/standard/posts/view.ts +63 -63
  676. package/src/standard/pricecards/index.tsx +57 -57
  677. package/src/standard/pricecards/load.ts +11 -11
  678. package/src/standard/pricecards/view.ts +50 -50
  679. package/src/standard/sharedTexts.ts +6 -6
  680. package/src/standard/site.ts +124 -124
  681. package/src/standard/siteLooks.ts +19 -19
  682. package/src/standard/sites.ts +13 -13
  683. package/src/standard/steps/index.tsx +28 -28
  684. package/src/standard/steps/load.ts +5 -5
  685. package/src/standard/steps/view.ts +11 -11
  686. package/src/standard/testimonials/index.tsx +87 -87
  687. package/src/standard/testimonials/list.ts +43 -43
  688. package/src/standard/testimonials/load.ts +58 -58
  689. package/src/standard/testimonials/shared.ts +13 -13
  690. package/src/standard/testimonials/view.ts +76 -76
  691. package/src/standard/themeBlocks.ts +13 -13
  692. package/src/standard/video/index.tsx +23 -23
  693. package/src/standard/video/load.ts +10 -10
  694. package/src/standard/video/view.ts +42 -42
  695. package/src/standard/view.tsx +105 -105
  696. package/src/standard/views.ts +37 -37
  697. package/src/store.ts +479 -499
  698. package/src/theme.ts +90 -90
  699. package/src/themeUi.ts +97 -97
  700. package/src/themes.ts +91 -91
  701. package/src/ui.tsx +3402 -3402
  702. package/src/virtual.d.ts +5 -5
  703. package/src/when.ts +22 -22
  704. package/starter/.githooks/pre-push +4 -4
  705. package/starter/.nvmrc +1 -1
  706. package/starter/.prettierignore +11 -11
  707. package/starter/.prettierrc.json +7 -7
  708. package/starter/README.md +27 -26
  709. package/starter/astro.config.mjs +5 -5
  710. package/starter/package.json +67 -67
  711. package/starter/public/_redirects +1 -1
  712. package/starter/public/favicon.svg +1 -1
  713. package/starter/public/logo.svg +1 -1
  714. package/starter/src/content/pages/blog.md +8 -8
  715. package/starter/src/content/pages/forside.md +9 -9
  716. package/starter/src/content/pages/kontakt.md +7 -7
  717. package/starter/src/content/pages/privatlivspolitik.md +9 -9
  718. package/starter/src/content/pages/siden-findes-ikke.md +7 -7
  719. package/starter/src/content.config.ts +2 -2
  720. package/starter/src/data/apps.json +1 -1
  721. package/starter/src/data/block-settings.json +3 -3
  722. package/starter/src/data/blocks.json +1 -1
  723. package/starter/src/data/booking.json +6 -6
  724. package/starter/src/data/colors.json +70 -70
  725. package/starter/src/data/contact.json +8 -8
  726. package/starter/src/data/cookies.json +21 -21
  727. package/starter/src/data/csp.json +1 -1
  728. package/starter/src/data/footer.json +15 -15
  729. package/starter/src/data/internal-links.json +4 -4
  730. package/starter/src/data/menu.json +4 -4
  731. package/starter/src/data/mobile-menu.json +4 -4
  732. package/starter/src/data/old-addresses.json +1 -1
  733. package/starter/src/data/redirects.json +1 -1
  734. package/starter/src/data/standard-pages.json +8 -8
  735. package/starter/src/data/theme.json +1 -1
  736. package/starter/src/env.d.ts +2 -2
  737. package/starter/src/site/config.ts +13 -13
  738. package/starter/src/site/worker.js +14 -14
  739. package/starter/tsconfig.json +17 -17
  740. package/starter/worker/apps.js +6 -6
  741. package/starter/wrangler.jsonc +39 -39
  742. package/theme/LICENSE +21 -21
  743. package/theme/blocks/ArrowLink.tsx +14 -14
  744. package/theme/blocks/BlockHead.tsx +26 -26
  745. package/theme/blocks/Panel.tsx +32 -32
  746. package/theme/blocks/Person.tsx +26 -26
  747. package/theme/blocks/accordion/PageRow.tsx +25 -25
  748. package/theme/blocks/accordion/View.tsx +17 -17
  749. package/theme/blocks/buttons/Buttons.tsx +28 -28
  750. package/theme/blocks/buttons/View.tsx +12 -12
  751. package/theme/blocks/cards/LinkCards.tsx +72 -72
  752. package/theme/blocks/cards/View.tsx +20 -20
  753. package/theme/blocks/checklist/Checks.tsx +15 -15
  754. package/theme/blocks/checklist/View.tsx +14 -14
  755. package/theme/blocks/cookies/View.tsx +36 -36
  756. package/theme/blocks/faq/Accordion.tsx +34 -34
  757. package/theme/blocks/faq/View.tsx +16 -16
  758. package/theme/blocks/faq/script.ts +56 -56
  759. package/theme/blocks/hero/View.tsx +44 -44
  760. package/theme/blocks/links/LinkGroups.tsx +24 -24
  761. package/theme/blocks/links/View.tsx +17 -17
  762. package/theme/blocks/list/Rows.tsx +24 -24
  763. package/theme/blocks/list/View.tsx +14 -14
  764. package/theme/blocks/notice/View.tsx +13 -13
  765. package/theme/blocks/points/PointList.tsx +34 -34
  766. package/theme/blocks/points/View.tsx +14 -14
  767. package/theme/blocks/posts/View.tsx +26 -26
  768. package/theme/blocks/pricecards/View.tsx +40 -40
  769. package/theme/blocks/steps/StepList.tsx +23 -23
  770. package/theme/blocks/steps/View.tsx +14 -14
  771. package/theme/blocks/testimonials/List.tsx +46 -46
  772. package/theme/blocks/testimonials/Slider.tsx +44 -44
  773. package/theme/blocks/testimonials/Testimonial.tsx +124 -124
  774. package/theme/blocks/testimonials/View.tsx +21 -21
  775. package/theme/blocks/testimonials/script.ts +52 -52
  776. package/theme/blocks/video/View.tsx +41 -41
  777. package/theme/components/ActionBar.astro +63 -63
  778. package/theme/components/AppSlot.astro +21 -21
  779. package/theme/components/ArrowLink.tsx +21 -21
  780. package/theme/components/BlogIndex.astro +48 -48
  781. package/theme/components/BlogPosts.astro +18 -18
  782. package/theme/components/BlogPosts.tsx +45 -45
  783. package/theme/components/BookButton.tsx +22 -22
  784. package/theme/components/Container.astro +11 -11
  785. package/theme/components/CookieConsent.astro +121 -121
  786. package/theme/components/EndBlock.astro +21 -21
  787. package/theme/components/FontPreload.astro +6 -6
  788. package/theme/components/Footer.astro +85 -85
  789. package/theme/components/Header.astro +186 -186
  790. package/theme/components/Icon.astro +16 -16
  791. package/theme/components/Img.astro +29 -29
  792. package/theme/components/Img.tsx +33 -33
  793. package/theme/components/Inline.astro +11 -11
  794. package/theme/components/Logo.astro +23 -23
  795. package/theme/components/LogoMark.astro +10 -10
  796. package/theme/components/Markdown.astro +17 -17
  797. package/theme/components/MobileMenu.tsx +224 -224
  798. package/theme/components/MobileNav.astro +33 -33
  799. package/theme/components/PageHeader.astro +22 -22
  800. package/theme/components/PageSection.astro +24 -24
  801. package/theme/components/PhoneButton.tsx +11 -11
  802. package/theme/components/Popup.astro +145 -145
  803. package/theme/components/PopupButton.tsx +24 -24
  804. package/theme/components/PopupView.tsx +48 -48
  805. package/theme/components/PostList.tsx +58 -58
  806. package/theme/components/PostMeta.tsx +17 -17
  807. package/theme/components/RichText.astro +28 -28
  808. package/theme/components/Section.astro +29 -29
  809. package/theme/components/SectionHeading.tsx +8 -8
  810. package/theme/components/TextSection.astro +16 -16
  811. package/theme/components/home/PageBlock.astro +19 -19
  812. package/theme/components/home/Text.astro +13 -13
  813. package/theme/components/loadPopup.ts +22 -22
  814. package/theme/components/loadPosts.ts +16 -16
  815. package/theme/components/posts.ts +15 -15
  816. package/theme/editor/looks.mjs +7 -7
  817. package/theme/editor/previews.tsx +56 -56
  818. package/theme/layouts/Base.astro +273 -273
  819. package/theme/layouts/PageLayout.astro +126 -126
  820. package/theme/layouts/PostLayout.astro +137 -137
  821. package/theme/scripts/motion.ts +29 -29
  822. package/theme/styles/base.css +320 -320
  823. package/theme/styles/blocks.css +1808 -1808
  824. package/theme/styles/components.css +206 -206
  825. package/theme/styles/global.css +1302 -1302
  826. package/theme/styles/ui.css +6544 -6544
  827. package/theme/templates/Blog.astro +38 -38
  828. package/theme/templates/Category.astro +19 -19
  829. package/theme/templates/Home.astro +36 -36
  830. package/theme/templates/New.astro +68 -68
  831. package/theme/templates/NotFound.astro +117 -117
  832. package/theme/theme.json +28 -28
  833. package/theme/ui/Badge.tsx +45 -45
  834. package/theme/ui/BottomSheet.tsx +148 -148
  835. package/theme/ui/Button.tsx +79 -79
  836. package/theme/ui/ButtonGroup.tsx +31 -31
  837. package/theme/ui/Card.tsx +45 -45
  838. package/theme/ui/Carousel.tsx +124 -124
  839. package/theme/ui/Checkbox.tsx +59 -59
  840. package/theme/ui/Chip.tsx +101 -101
  841. package/theme/ui/ChipSet.tsx +27 -27
  842. package/theme/ui/DatePicker.tsx +149 -149
  843. package/theme/ui/DatePickerContainer.tsx +316 -316
  844. package/theme/ui/Dialog.tsx +100 -100
  845. package/theme/ui/Divider.tsx +27 -27
  846. package/theme/ui/ExposedDropdownMenu.tsx +77 -77
  847. package/theme/ui/Fab.tsx +88 -88
  848. package/theme/ui/FabMenu.tsx +67 -67
  849. package/theme/ui/IconButton.tsx +78 -78
  850. package/theme/ui/List.tsx +287 -287
  851. package/theme/ui/LoadingIndicator.tsx +29 -29
  852. package/theme/ui/Menu.tsx +278 -278
  853. package/theme/ui/NavigationBar.tsx +62 -62
  854. package/theme/ui/NavigationDrawer.tsx +80 -80
  855. package/theme/ui/NavigationRail.tsx +154 -154
  856. package/theme/ui/ProgressIndicator.tsx +162 -162
  857. package/theme/ui/RadioButton.tsx +50 -50
  858. package/theme/ui/Search.tsx +129 -129
  859. package/theme/ui/SegmentedButton.tsx +49 -49
  860. package/theme/ui/SideSheet.tsx +54 -54
  861. package/theme/ui/Slider.tsx +155 -155
  862. package/theme/ui/Snackbar.tsx +67 -67
  863. package/theme/ui/SplitButton.tsx +67 -67
  864. package/theme/ui/Switch.tsx +60 -60
  865. package/theme/ui/Tabs.tsx +114 -114
  866. package/theme/ui/TextField.tsx +278 -278
  867. package/theme/ui/TimePicker.tsx +128 -128
  868. package/theme/ui/TimePickerContainer.tsx +234 -234
  869. package/theme/ui/Toolbar.tsx +67 -67
  870. package/theme/ui/Tooltip.tsx +58 -58
  871. package/theme/ui/TopAppBar.tsx +77 -77
  872. package/theme/ui/bottomSheet.script.ts +8 -8
  873. package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
  874. package/theme/ui/carousel.script.ts +28 -28
  875. package/theme/ui/carouselLayout.ts +229 -229
  876. package/theme/ui/checkbox.script.ts +8 -8
  877. package/theme/ui/datePicker.script.ts +302 -302
  878. package/theme/ui/dates.ts +185 -185
  879. package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
  880. package/theme/ui/dropdownMenu.script.ts +166 -166
  881. package/theme/ui/fabMenu.script.ts +57 -57
  882. package/theme/ui/navigationRail.script.ts +47 -47
  883. package/theme/ui/ripple.script.ts +159 -159
  884. package/theme/ui/search.script.ts +92 -92
  885. package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
  886. package/theme/ui/slider.script.ts +12 -12
  887. package/theme/ui/sliderUpdate.ts +52 -52
  888. package/theme/ui/snackbar.script.ts +16 -16
  889. package/theme/ui/splitButton.script.ts +63 -63
  890. package/theme/ui/tabs.script.ts +48 -48
  891. package/theme/ui/textField.script.ts +17 -17
  892. package/theme/ui/textFieldUpdate.ts +44 -44
  893. package/theme/ui/timePicker.script.ts +248 -248
  894. package/theme/ui/toolbar.script.ts +43 -43
  895. package/theme/ui/tooltip.script.ts +68 -68
  896. package/worker/consent.js +141 -141
  897. package/worker/editor-github.js +167 -167
  898. package/worker/editor-host.js +74 -74
  899. package/worker/files.js +401 -401
  900. package/worker/github.js +36 -36
  901. package/worker/index.js +188 -184
  902. package/worker/not-found.js +97 -97
  903. package/worker/oauth.js +95 -95
  904. package/worker/publish.js +52 -52
  905. package/worker/push.js +270 -270
  906. package/worker/r2-status.js +50 -50
  907. package/worker/session.js +58 -58
  908. package/worker/store.js +46 -0
package/defaults/rules.ts CHANGED
@@ -1,450 +1,450 @@
1
- // The site's own rules for the editor: the lists the fields that pick something choose from (a page, a
2
- // post, a tag, a testimonial), the prices and testimonials the blocks show, and why an entry cannot be
3
- // deleted (something on the site uses it). The messages are shown in the editor's language (siteText,
4
- // texts.ts).
5
- import type { RepoFile } from '../src/github';
6
- import type { Option, PageState, TestimonialRow } from '../src/fields';
7
- import type { Status } from '../src/FormFrame';
8
- import { blocksIn } from '../lib/blocks.mjs';
9
- import { readFields, splitFrontmatter } from '../src/frontmatter.mjs';
10
- import { siteText } from '../src/i18n';
11
- import { byText, fixedName, slugOf, type Collection, type Relation } from '../src/schema';
12
- import { collections } from '@site/collections';
13
- import { config } from '@site/config';
14
- import { opensOf } from '@site/opens';
15
- import { differentPages, standardPages, standardPaths } from '@site/standardPages';
16
-
17
- const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
18
- // A page's address: `path`, or /<file name>/ for pages made in the editor.
19
- const pagePath = (f: RepoFile) => String(fm(f).path ?? `/${slugOf(f.path, config.folders.pages)}/`);
20
- const isPage = (f: RepoFile) => f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md');
21
- // The addresses of the standard pages, by their key ("Indstillinger" › "Udseende" › "Standardsider", standardPages.ts).
22
- const standardOf = (files: { path: string; text: string }[]) => standardPaths(files.find((f) => f.path === config.standardPages)?.text);
23
- // The page that is a standard page (the front page: 'front'), if it is there.
24
- const standardFile = (files: RepoFile[], key: string) => {
25
- const path = standardOf(files)[key];
26
- return path ? files.find((f) => isPage(f) && pagePath(f) === path) : undefined;
27
- };
28
- // Where a page is shown: the front page at /, the blog page at /blog/, the others at their own address.
29
- const shownAt = (path: string, files: { path: string; text: string }[]) => {
30
- const paths = standardOf(files);
31
- return standardPages.find((p) => p.at && paths[p.key] === path)?.at ?? path;
32
- };
33
- const json = (file: RepoFile) => JSON.parse(file.text) as Record<string, unknown>;
34
- const byLabel = (a: Option, b: Option) => byText(a.label, b.label);
35
- // A page's or post's status (packages/editor/lib/status.mjs), for the filter of the sheet it is picked in (usePickTools).
36
- const stateOf = (f: RepoFile): PageState => {
37
- const s = fm(f).status;
38
- return s === 'draft' || s === 'unlisted' || s === 'scheduled' ? s : 'published';
39
- };
40
- // The addresses the menu (config.menu) links to, at every level, without a part of a page (#…).
41
- type MenuEntry = { href: string; children?: MenuEntry[] };
42
- function menuAddresses(files: RepoFile[]): Set<string> {
43
- let entries: MenuEntry[] = [];
44
- try {
45
- entries = JSON.parse(files.find((f) => f.path === config.menu)?.text ?? '{}').items ?? [];
46
- } catch {}
47
- const all = (e: MenuEntry[]): string[] => e.flatMap((x) => [String(x.href ?? '').replace(/#.*/, ''), ...all(x.children ?? [])]);
48
- return new Set(all(entries));
49
- }
50
-
51
- // The choices for the fields that pick a page, a post, a category or a testimonial.
52
- export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
53
- const inFolder = (folder: string, ext: string) => files.filter((f) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext));
54
- // A standard page is in the menu by its own address or where it is shown (the front page at /, the blog page at /blog/).
55
- const menu = menuAddresses(files);
56
- const page = (f: RepoFile): Option => ({
57
- value: pagePath(f),
58
- label: String(fm(f).title),
59
- state: stateOf(f),
60
- inMenu: menu.has(pagePath(f)) || menu.has(shownAt(pagePath(f), files)),
61
- });
62
- return {
63
- // Pages are stored by their address: `path`, or /<file name>/ for pages made in the editor.
64
- pages: inFolder(config.folders.pages, '.md').map(page).sort(byLabel),
65
- // The pages that can be a standard page ("Indstillinger" › "Udseende" › "Standardsider"): those on the site, not a
66
- // draft, one waiting for its time or a new version of a page (/ny/).
67
- standardPages: inFolder(config.folders.pages, '.md')
68
- .filter((f) => !['draft', 'scheduled'].includes(String(fm(f).status ?? '')) && !pagePath(f).startsWith('/ny/'))
69
- .map(page)
70
- .sort(byLabel),
71
- // The front page (/) and the blog (/blog/), whichever pages they are, the blog by its page's title.
72
- fixed: [
73
- { value: '/', label: siteText('Forside'), inMenu: menu.has('/') },
74
- {
75
- value: '/blog/',
76
- label: String((standardFile(files, 'blog') && fm(standardFile(files, 'blog')!).title) || siteText('Blog')),
77
- inMenu: menu.has('/blog/'),
78
- },
79
- ],
80
- blog: inFolder(config.folders.blog, '.md')
81
- .map((f) => ({
82
- value: slugOf(f.path, config.folders.blog),
83
- label: String(fm(f).title),
84
- state: stateOf(f),
85
- date: String(fm(f).date ?? ''),
86
- }))
87
- .sort(byLabel),
88
- categories: inFolder('src/data/categories', '.json')
89
- .map((f) => ({ value: slugOf(f.path, 'src/data/categories'), label: String(json(f).name) }))
90
- .sort(byLabel),
91
- testimonials: inFolder('src/data/testimonials', '.json')
92
- .map((f) => ({ value: slugOf(f.path, 'src/data/testimonials'), label: String(json(f).name) }))
93
- .sort(byLabel),
94
- // The promotions turned on that a button can open ("Knapper": "Åbn promovering", opens.ts), by what opens with
95
- // the promotion's name ("Bookingarket (Calendly bookingark)"), where they are shown (a button offers those on its
96
- // page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book en tid").
97
- promotions: inFolder('src/data/popups', '.json')
98
- .flatMap((f) => {
99
- const d = json(f);
100
- const opens = opensOf(d.type ? String(d.type) : undefined);
101
- if (!opens || d.enabled === false) return [];
102
- const name = String(d.heading ?? d.name ?? '');
103
- return [
104
- {
105
- value: slugOf(f.path, 'src/data/popups'),
106
- label: siteText('{hvad} ({navn})', { hvad: siteText(opens.label), navn: name }),
107
- on: { everywhere: d.where === 'all', pages: d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [] },
108
- ...(opens.as ? { as: opens.as } : { text: String(d.heading ?? '') }),
109
- },
110
- ];
111
- })
112
- .sort(byLabel),
113
- // The PDFs in "Filer", by their address on the site (/filer/<name>.pdf), named by their file name.
114
- files: inFolder(config.folders.files, '.pdf')
115
- .map((f) => ({ value: f.path.replace(/^public/, ''), label: f.path.split('/').pop()! }))
116
- .sort(byLabel),
117
- };
118
- }
119
-
120
- const frontPage = (files: RepoFile[]) => ({
121
- to: standardFile(files, 'front')?.path ?? config.standardPages,
122
- text: siteText('Rediger forsiden'),
123
- });
124
-
125
- // Where a button that opens a promotion is: a page (`path`), the shared buttons (on any page: neither), or an action bar
126
- // (`bar`: the pages it is shown on, or none for every page or the posts with its tags).
127
- type Opener = { file: string; title: string; path?: string; bar?: { pages?: string[] } };
128
-
129
- // The buttons that open a promotion ("Knapper": "Åbn promovering", the button's `open`), where they are: the page with
130
- // the block (its file, title and the address it is shown at: the front page's /), the shared buttons
131
- // ("Standardblokke" › "Knapper", on any page: no address), or an action bar ("Promovering").
132
- function openersOf(slug: string, files: { path: string; text: string }[]): Opener[] {
133
- const opens = (items: unknown) =>
134
- Array.isArray(items) &&
135
- items.some(
136
- (b) =>
137
- (b as Record<string, unknown>)?.type !== 'text' &&
138
- (b as Record<string, unknown>)?.to === 'promotion' &&
139
- (b as Record<string, unknown>).open === slug,
140
- );
141
- const found: Opener[] = [];
142
- for (const f of files) {
143
- if (f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md')) {
144
- // Also a "Knapper" block in a block's text (a "Banner"'s), where its quotes are written as &quot;.
145
- if (!/data-block=(?:"|\\&quot;)buttons/.test(f.text)) continue;
146
- const d = fm(f as RepoFile);
147
- if (
148
- blocksIn(f.text, true).some((b: { type: string; props: Record<string, unknown> }) => b.type === 'buttons' && opens(b.props.items))
149
- )
150
- found.push({ file: f.path, title: String(d.title ?? ''), path: shownAt(pagePath(f as RepoFile), files) });
151
- } else if (f.path.startsWith('src/data/popups/') && f.path.endsWith('.json')) {
152
- const d = json(f as RepoFile);
153
- if (d.type === 'bookingbar' && d.enabled !== false && opens(d.buttons))
154
- found.push({
155
- file: f.path,
156
- title: siteText('Handlingslinje'),
157
- bar: { pages: d.where === 'all' || d.where === 'tags' ? undefined : Array.isArray(d.pages) ? d.pages.map(String) : [] },
158
- });
159
- } else if (f.path === config.blockTexts) {
160
- if (opens((json(f as RepoFile).buttons as Record<string, unknown> | undefined)?.items))
161
- found.push({ file: 'blokke/buttons', title: siteText('Standardknapper') });
162
- }
163
- }
164
- return found;
165
- }
166
-
167
- // Whether a promotion as it is (or is about to be saved) is shown where a button opens it: turned on, of a kind a button
168
- // can open, and on every page, or on the button's page (the shared buttons are on any page), or on every page the
169
- // action bar is on.
170
- function shownFor(d: Record<string, unknown>, at: Opener) {
171
- if (d.enabled === false || !opensOf(d.type ? String(d.type) : undefined)) return false;
172
- if (d.where === 'all') return true;
173
- const pages = d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [];
174
- if (at.bar) return !!at.bar.pages && at.bar.pages.every((p) => pages.includes(p));
175
- return !!at.path && pages.includes(at.path);
176
- }
177
-
178
- const openerText = (at: Opener) =>
179
- at.bar
180
- ? siteText(
181
- 'En knap i handlingslinjen åbner promoveringen, så den skal være slået til og vises alle steder, hvor linjen vises. Ret knappen først.',
182
- )
183
- : at.path
184
- ? siteText('En knap på siden "{side}" åbner promoveringen, så den skal være slået til og vises dér. Ret knappen først.', {
185
- side: at.title,
186
- })
187
- : siteText('Standardknapperne åbner promoveringen, så den skal være slået til og vises på alle sider. Ret dem først.');
188
- const openerEdit = (at: Opener) => ({
189
- to: at.file,
190
- text: siteText(at.bar ? 'Rediger handlingslinjen' : at.path ? 'Rediger siden' : 'Rediger standardknapperne'),
191
- });
192
-
193
- /** Why a promotion cannot be saved like this (turned off, or no longer shown on a page where a button opens it,
194
- * "Knapper": "Åbn promovering"), with a link to the button, or the standard pages (the same page twice, or a page
195
- * without the block it must have, with a link to it), or null. Before it is saved (DataForm.tsx; the standard pages in
196
- * CompanyForm.tsx, without a collection). */
197
- export function keptBy(collection: Collection | null, path: string, data: Record<string, unknown>, files: RepoFile[]): Status {
198
- if (path === config.standardPages) return standardKept(data, files);
199
- if (collection?.kind !== 'folder' || collection.id !== 'popups') return null;
200
- const at = openersOf(slugOf(path, collection.folder), files).find((o) => !shownFor(data, o));
201
- return at ? { text: openerText(at), error: true, edit: openerEdit(at) } : null;
202
- }
203
-
204
- // The standard pages as they are about to be saved: different pages (the cookie policy may be the privacy policy), each
205
- // with the block it must have.
206
- function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status {
207
- const chosen = standardPages.map((p) => ({ ...p, path: String(data[p.key] ?? '') })).filter((p) => p.path);
208
- if (!differentPages(Object.fromEntries(chosen.map((p) => [p.key, p.path]))))
209
- return { text: siteText('Standardsiderne skal være forskellige sider.'), error: true };
210
- for (const p of chosen) {
211
- const page = files.find((f) => isPage(f) && pagePath(f) === p.path);
212
- if (!p.needs || !page || blocksIn(page.text).some((b: { type: string }) => b.type === p.needs!.type)) continue;
213
- return {
214
- text: siteText('Siden "{side}" har ikke blokken {blok}, som {hvad} skal have. Sæt den ind på siden først.', {
215
- side: String(fm(page).title),
216
- blok: siteText(p.needs.label),
217
- hvad: siteText(p.the).toLowerCase(),
218
- }),
219
- error: true,
220
- edit: { to: page.path, text: siteText('Rediger siden') },
221
- };
222
- }
223
- return null;
224
- }
225
-
226
- /** What is wrong on the site by its own rules, a line for each, before it is built (checkSite.ts): a standard page that
227
- * is not a page on the site, or the same page twice, and a button that opens a promotion that is gone, turned off or not
228
- * shown on its page. */
229
- export function siteProblems(files: { path: string; text: string }[]): string[] {
230
- const problems: string[] = [];
231
- // The standard pages are pages on the site ("Indstillinger" › "Udseende" › "Standardsider"), different ones (but for
232
- // the cookie policy, which may be the privacy policy).
233
- if (files.some((f) => f.path === config.standardPages)) {
234
- for (const p of standardPages) {
235
- const page = standardFile(files as RepoFile[], p.key);
236
- if (!page || ['draft', 'scheduled'].includes(String(fm(page).status ?? '')))
237
- problems.push(`${config.standardPages}: ${siteText('{side} skal være en side, der er udgivet.', { side: siteText(p.the) })}`);
238
- }
239
- if (!differentPages(standardOf(files)))
240
- problems.push(`${config.standardPages}: ${siteText('Standardsiderne skal være forskellige sider.')}`);
241
- }
242
- const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
243
- const slugs = new Set<string>();
244
- for (const f of files) for (const m of f.text.matchAll(/(?:&quot;|")open(?:&quot;|"):\s*(?:&quot;|")([^"&]+)/g)) slugs.add(m[1]);
245
- for (const slug of slugs) {
246
- const file = promotions.find((f) => slugOf(f.path, 'src/data/popups') === slug);
247
- for (const at of openersOf(slug, files)) {
248
- if (file && shownFor(json(file as RepoFile), at)) continue;
249
- problems.push(
250
- `${at.file.startsWith('blokke/') ? config.blockTexts : at.file}: ${
251
- file ? openerText(at) : siteText('En knap åbner promoveringen "{navn}", som ikke findes.', { navn: slug })
252
- }`,
253
- );
254
- }
255
- }
256
- return problems;
257
- }
258
-
259
- // The tags a promotion is shown on the posts of (Promovering, "Vises på": "Blogindlæg med tags").
260
- const promotionTags = (d: Record<string, unknown>): string[] => (d.where === 'tags' && Array.isArray(d.tags) ? d.tags.map(String) : []);
261
-
262
- // Why an entry cannot be deleted (something on the site uses it), with a link to where, or null.
263
- export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]): Status {
264
- if (collection.kind === 'files') return null;
265
- const slug = slugOf(file.path, collection.folder);
266
- // A promotion a button opens ("Knapper": "Åbn promovering").
267
- if (collection.id === 'popups') {
268
- const [at] = openersOf(slug, files);
269
- if (at)
270
- return {
271
- text: at.bar
272
- ? siteText('En knap i handlingslinjen åbner promoveringen. Ret knappen først.')
273
- : at.path
274
- ? siteText('En knap på siden "{side}" åbner promoveringen. Ret knappen først.', { side: at.title })
275
- : siteText('Standardknapperne åbner promoveringen. Ret dem først.'),
276
- error: true,
277
- edit: openerEdit(at),
278
- };
279
- }
280
- const home = standardFile(files, 'front');
281
- const front = Object.fromEntries(
282
- (home ? (blocksIn(home.text) as { type: string; props: Record<string, unknown> }[]) : []).map((b) => [b.type, b.props]),
283
- );
284
- if (collection.id === 'kategorier') {
285
- const posts = files.filter(
286
- (f) => f.path.startsWith(`${config.folders.blog}/`) && ((fm(f).categories as string[]) ?? []).includes(slug),
287
- );
288
- if (posts.length)
289
- return {
290
- text:
291
- posts.length === 1
292
- ? siteText('Tagget bruges på et blogindlæg. Fjern det fra det først.')
293
- : siteText('Tagget bruges på {antal} blogindlæg. Fjern det fra dem først.', { antal: posts.length }),
294
- error: true,
295
- };
296
- const quotes = files.filter(
297
- (f) => f.path.startsWith('src/data/testimonials/') && ((json(f).categories as string[] | undefined) ?? []).includes(slug),
298
- );
299
- if (quotes.length)
300
- return {
301
- text:
302
- quotes.length === 1
303
- ? siteText('Tagget bruges på en udtalelse. Fjern det fra den først.')
304
- : siteText('Tagget bruges på {antal} udtalelser. Fjern det fra dem først.', { antal: quotes.length }),
305
- error: true,
306
- };
307
- // A promotion shown on the posts with the tag (Promovering, "Blogindlæg med tags").
308
- const promotion = files.find(
309
- (f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json') && promotionTags(json(f)).includes(slug),
310
- );
311
- if (promotion) {
312
- const d = json(promotion);
313
- return {
314
- text: siteText('Tagget bruges i promoveringen "{navn}". Fjern det fra den først.', { navn: String(d.heading ?? d.name ?? '') }),
315
- error: true,
316
- edit: { to: promotion.path, text: siteText('Rediger promoveringen') },
317
- };
318
- }
319
- const fp = front.posts as Record<string, unknown> | undefined;
320
- if ((fp?.categories as string[] | undefined)?.includes(slug) || fp?.link === slug)
321
- return {
322
- text: siteText('Tagget vælger indlæggene på forsiden under "Fra bloggen". Vælg et andet først.'),
323
- error: true,
324
- edit: frontPage(files),
325
- };
326
- }
327
- if (collection.id === 'blog' && ((front.posts as Record<string, unknown>)?.picked as string[])?.includes(slug))
328
- return {
329
- text: siteText('Indlægget er valgt til forsiden under "Fra bloggen". Vælg et andet først.'),
330
- error: true,
331
- edit: frontPage(files),
332
- };
333
- // A "Blogindlæg" block in a page that picks the post (its props are in the text).
334
- if (collection.id === 'blog') {
335
- const page = files.find(
336
- (f) =>
337
- f.path.startsWith(`${config.folders.pages}/`) && f.text.includes('data-block="posts"') && f.text.includes(`&quot;${slug}&quot;`),
338
- );
339
- if (page)
340
- return {
341
- text: siteText('Indlægget er valgt i en blok med blogindlæg på siden "{side}". Vælg et andet først.', {
342
- side: String(fm(page).title),
343
- }),
344
- error: true,
345
- edit: { to: page.path, text: siteText('Rediger siden') },
346
- };
347
- }
348
- // Any other place under "Bruges på" (usesOf): a "Blogindlæg" or "Udtalelser" block that picks the tag, on a
349
- // page or in a post.
350
- if (collection.id === 'kategorier') {
351
- const use = usesOf(collection, files)(file)[0];
352
- if (use) {
353
- const post = use.path.startsWith(`${config.folders.blog}/`);
354
- return {
355
- text: siteText(
356
- post
357
- ? 'Tagget er valgt i en blok i blogindlægget "{side}". Vælg et andet først.'
358
- : 'Tagget er valgt i en blok på siden "{side}". Vælg et andet først.',
359
- { side: use.name },
360
- ),
361
- error: true,
362
- edit: { to: use.path, text: siteText(post ? 'Rediger indlægget' : 'Rediger siden') },
363
- };
364
- }
365
- }
366
- return null;
367
- }
368
-
369
- // What a file that uses something is called: a page's or post's title, a setting's name, an entry's
370
- // as its list shows it.
371
- function nameOf(file: RepoFile): string {
372
- for (const c of collections)
373
- if (c.kind === 'files') {
374
- const own = c.files.find((f) => f.file === file.path);
375
- if (own) return fixedName({ ...own, label: siteText(own.label) }, file.text);
376
- } else if (c.kind === 'folder' && file.path.startsWith(`${c.folder}/`) && file.path.endsWith('.json'))
377
- return c.summary(json(file)) || siteText(c.label);
378
- return String(fm(file).title ?? file.path);
379
- }
380
-
381
- export type Use = { path: string; name: string };
382
-
383
- // The places on the site that use an entry, by the entry's file, each once (the tag's "Bruges på", DataForm.tsx,
384
- // and the number at it in its list, Library.tsx); [] for none. For a tag: the blog posts, testimonials and
385
- // promotions that have it, and the pages and posts with a "Blogindlæg" or "Udtalelser" block that
386
- // picks it (its tags, or the tag "Alle indlæg" goes to). Made once for the whole list.
387
- export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => Use[] {
388
- if (collection.kind === 'files' || collection.id !== 'kategorier') return () => [];
389
- const uses = tagUses(files);
390
- return (file) =>
391
- [...(uses.get(slugOf(file.path, collection.folder))?.values() ?? [])]
392
- .map((f) => ({ path: f.path, name: nameOf(f) }))
393
- .sort((a, b) => byText(a.name, b.name));
394
- }
395
-
396
- // The files that use each tag (usesOf), by the tag, each file once.
397
- function tagUses(files: RepoFile[]): Map<string, Map<string, RepoFile>> {
398
- const uses = new Map<string, Map<string, RepoFile>>();
399
- const add = (f: RepoFile, slugs: unknown) => {
400
- for (const slug of Array.isArray(slugs) ? slugs.map(String) : []) {
401
- if (!uses.has(slug)) uses.set(slug, new Map());
402
- uses.get(slug)!.set(f.path, f);
403
- }
404
- };
405
- const block = (f: RepoFile, type: unknown, props: Record<string, unknown>) => {
406
- if (type === 'posts' || type === 'testimonials')
407
- add(f, [...(Array.isArray(props.categories) ? props.categories : []), ...(props.link ? [props.link] : [])]);
408
- };
409
- const inFolder = (f: RepoFile, folder: string, ext: string) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext);
410
- for (const f of files) {
411
- if (inFolder(f, config.folders.blog, '.md') || inFolder(f, config.folders.pages, '.md')) {
412
- if (inFolder(f, config.folders.blog, '.md')) add(f, fm(f).categories);
413
- for (const b of blocksIn(f.text) as { type: string; props: Record<string, unknown> }[]) block(f, b.type, b.props);
414
- } else if (inFolder(f, 'src/data/testimonials', '.json')) add(f, json(f).categories);
415
- else if (inFolder(f, 'src/data/popups', '.json')) add(f, promotionTags(json(f)));
416
- }
417
- return uses;
418
- }
419
-
420
- // The number at the right of an entry in the list of a part, by the entry's file, or null for none
421
- // (Library.tsx): at each tag, the number of places that use it (usesOf).
422
- export function countsOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => number | null {
423
- if (collection.kind === 'files' || collection.id !== 'kategorier') return () => null;
424
- const uses = tagUses(files);
425
- return (file) => uses.get(slugOf(file.path, collection.folder))?.size ?? 0;
426
- }
427
-
428
- // The testimonials, in their order on the site, for the cards and previews of the blocks.
429
- export const testimonialsOf = (files: RepoFile[]) =>
430
- files
431
- .filter((f) => f.path.startsWith('src/data/testimonials/') && f.path.endsWith('.json'))
432
- .map((f) => ({ ...(json(f) as TestimonialRow & { order?: number }), slug: slugOf(f.path, 'src/data/testimonials') }))
433
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
434
-
435
- /** What is at an address, as said in a message: "forsiden", 'siden "Om mig" (/om-mig/)',
436
- * or the address (where a redirect sends to, App.tsx). */
437
- export function whatIsAt(address: string, relations: Record<Relation, Option[]>): string {
438
- const base = address.split('#')[0];
439
- if (base === '/') return siteText('forsiden');
440
- if (base === '/blog/') return siteText('bloggen');
441
- const page = [...relations.fixed, ...relations.pages].find((o) => o.value === base);
442
- if (page) return siteText('siden "{navn}" ({adresse})', { navn: page.label, adresse: address });
443
- const post = relations.blog.find((o) => `/${o.value}/` === base);
444
- if (post) return siteText('blogindlægget "{navn}" ({adresse})', { navn: post.label, adresse: address });
445
- const tag = relations.categories.find((o) => `/blog/kategori/${o.value}/` === base);
446
- if (tag) return siteText('tagget "{navn}" på bloggen ({adresse})', { navn: tag.label, adresse: address });
447
- if (base.startsWith(config.folders.media.replace(/^public/, '') + '/'))
448
- return siteText('billedet {navn}', { navn: base.split('/').pop()! });
449
- return address;
450
- }
1
+ // The site's own rules for the editor: the lists the fields that pick something choose from (a page, a
2
+ // post, a tag, a testimonial), the prices and testimonials the blocks show, and why an entry cannot be
3
+ // deleted (something on the site uses it). The messages are shown in the editor's language (siteText,
4
+ // texts.ts).
5
+ import type { RepoFile } from '../src/github';
6
+ import type { Option, PageState, TestimonialRow } from '../src/fields';
7
+ import type { Status } from '../src/FormFrame';
8
+ import { blocksIn } from '../lib/blocks.mjs';
9
+ import { readFields, splitFrontmatter } from '../src/frontmatter.mjs';
10
+ import { siteText } from '../src/i18n';
11
+ import { byText, fixedName, slugOf, type Collection, type Relation } from '../src/schema';
12
+ import { collections } from '@site/collections';
13
+ import { config } from '@site/config';
14
+ import { opensOf } from '@site/opens';
15
+ import { differentPages, standardPages, standardPaths } from '@site/standardPages';
16
+
17
+ const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
18
+ // A page's address: `path`, or /<file name>/ for pages made in the editor.
19
+ const pagePath = (f: RepoFile) => String(fm(f).path ?? `/${slugOf(f.path, config.folders.pages)}/`);
20
+ const isPage = (f: RepoFile) => f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md');
21
+ // The addresses of the standard pages, by their key ("Indstillinger" › "Udseende" › "Standardsider", standardPages.ts).
22
+ const standardOf = (files: { path: string; text: string }[]) => standardPaths(files.find((f) => f.path === config.standardPages)?.text);
23
+ // The page that is a standard page (the front page: 'front'), if it is there.
24
+ const standardFile = (files: RepoFile[], key: string) => {
25
+ const path = standardOf(files)[key];
26
+ return path ? files.find((f) => isPage(f) && pagePath(f) === path) : undefined;
27
+ };
28
+ // Where a page is shown: the front page at /, the blog page at /blog/, the others at their own address.
29
+ const shownAt = (path: string, files: { path: string; text: string }[]) => {
30
+ const paths = standardOf(files);
31
+ return standardPages.find((p) => p.at && paths[p.key] === path)?.at ?? path;
32
+ };
33
+ const json = (file: RepoFile) => JSON.parse(file.text) as Record<string, unknown>;
34
+ const byLabel = (a: Option, b: Option) => byText(a.label, b.label);
35
+ // A page's or post's status (packages/editor/lib/status.mjs), for the filter of the sheet it is picked in (usePickTools).
36
+ const stateOf = (f: RepoFile): PageState => {
37
+ const s = fm(f).status;
38
+ return s === 'draft' || s === 'unlisted' || s === 'scheduled' ? s : 'published';
39
+ };
40
+ // The addresses the menu (config.menu) links to, at every level, without a part of a page (#…).
41
+ type MenuEntry = { href: string; children?: MenuEntry[] };
42
+ function menuAddresses(files: RepoFile[]): Set<string> {
43
+ let entries: MenuEntry[] = [];
44
+ try {
45
+ entries = JSON.parse(files.find((f) => f.path === config.menu)?.text ?? '{}').items ?? [];
46
+ } catch {}
47
+ const all = (e: MenuEntry[]): string[] => e.flatMap((x) => [String(x.href ?? '').replace(/#.*/, ''), ...all(x.children ?? [])]);
48
+ return new Set(all(entries));
49
+ }
50
+
51
+ // The choices for the fields that pick a page, a post, a category or a testimonial.
52
+ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
53
+ const inFolder = (folder: string, ext: string) => files.filter((f) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext));
54
+ // A standard page is in the menu by its own address or where it is shown (the front page at /, the blog page at /blog/).
55
+ const menu = menuAddresses(files);
56
+ const page = (f: RepoFile): Option => ({
57
+ value: pagePath(f),
58
+ label: String(fm(f).title),
59
+ state: stateOf(f),
60
+ inMenu: menu.has(pagePath(f)) || menu.has(shownAt(pagePath(f), files)),
61
+ });
62
+ return {
63
+ // Pages are stored by their address: `path`, or /<file name>/ for pages made in the editor.
64
+ pages: inFolder(config.folders.pages, '.md').map(page).sort(byLabel),
65
+ // The pages that can be a standard page ("Indstillinger" › "Udseende" › "Standardsider"): those on the site, not a
66
+ // draft, one waiting for its time or a new version of a page (/ny/).
67
+ standardPages: inFolder(config.folders.pages, '.md')
68
+ .filter((f) => !['draft', 'scheduled'].includes(String(fm(f).status ?? '')) && !pagePath(f).startsWith('/ny/'))
69
+ .map(page)
70
+ .sort(byLabel),
71
+ // The front page (/) and the blog (/blog/), whichever pages they are, the blog by its page's title.
72
+ fixed: [
73
+ { value: '/', label: siteText('Forside'), inMenu: menu.has('/') },
74
+ {
75
+ value: '/blog/',
76
+ label: String((standardFile(files, 'blog') && fm(standardFile(files, 'blog')!).title) || siteText('Blog')),
77
+ inMenu: menu.has('/blog/'),
78
+ },
79
+ ],
80
+ blog: inFolder(config.folders.blog, '.md')
81
+ .map((f) => ({
82
+ value: slugOf(f.path, config.folders.blog),
83
+ label: String(fm(f).title),
84
+ state: stateOf(f),
85
+ date: String(fm(f).date ?? ''),
86
+ }))
87
+ .sort(byLabel),
88
+ categories: inFolder('src/data/categories', '.json')
89
+ .map((f) => ({ value: slugOf(f.path, 'src/data/categories'), label: String(json(f).name) }))
90
+ .sort(byLabel),
91
+ testimonials: inFolder('src/data/testimonials', '.json')
92
+ .map((f) => ({ value: slugOf(f.path, 'src/data/testimonials'), label: String(json(f).name) }))
93
+ .sort(byLabel),
94
+ // The promotions turned on that a button can open ("Knapper": "Åbn promovering", opens.ts), by what opens with
95
+ // the promotion's name ("Bookingarket (Calendly bookingark)"), where they are shown (a button offers those on its
96
+ // page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book en tid").
97
+ promotions: inFolder('src/data/popups', '.json')
98
+ .flatMap((f) => {
99
+ const d = json(f);
100
+ const opens = opensOf(d.type ? String(d.type) : undefined);
101
+ if (!opens || d.enabled === false) return [];
102
+ const name = String(d.heading ?? d.name ?? '');
103
+ return [
104
+ {
105
+ value: slugOf(f.path, 'src/data/popups'),
106
+ label: siteText('{hvad} ({navn})', { hvad: siteText(opens.label), navn: name }),
107
+ on: { everywhere: d.where === 'all', pages: d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [] },
108
+ ...(opens.as ? { as: opens.as } : { text: String(d.heading ?? '') }),
109
+ },
110
+ ];
111
+ })
112
+ .sort(byLabel),
113
+ // The PDFs in "Filer", by their address on the site (/filer/<name>.pdf), named by their file name.
114
+ files: inFolder(config.folders.files, '.pdf')
115
+ .map((f) => ({ value: f.path.replace(/^public/, ''), label: f.path.split('/').pop()! }))
116
+ .sort(byLabel),
117
+ };
118
+ }
119
+
120
+ const frontPage = (files: RepoFile[]) => ({
121
+ to: standardFile(files, 'front')?.path ?? config.standardPages,
122
+ text: siteText('Rediger forsiden'),
123
+ });
124
+
125
+ // Where a button that opens a promotion is: a page (`path`), the shared buttons (on any page: neither), or an action bar
126
+ // (`bar`: the pages it is shown on, or none for every page or the posts with its tags).
127
+ type Opener = { file: string; title: string; path?: string; bar?: { pages?: string[] } };
128
+
129
+ // The buttons that open a promotion ("Knapper": "Åbn promovering", the button's `open`), where they are: the page with
130
+ // the block (its file, title and the address it is shown at: the front page's /), the shared buttons
131
+ // ("Standardblokke" › "Knapper", on any page: no address), or an action bar ("Promovering").
132
+ function openersOf(slug: string, files: { path: string; text: string }[]): Opener[] {
133
+ const opens = (items: unknown) =>
134
+ Array.isArray(items) &&
135
+ items.some(
136
+ (b) =>
137
+ (b as Record<string, unknown>)?.type !== 'text' &&
138
+ (b as Record<string, unknown>)?.to === 'promotion' &&
139
+ (b as Record<string, unknown>).open === slug,
140
+ );
141
+ const found: Opener[] = [];
142
+ for (const f of files) {
143
+ if (f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md')) {
144
+ // Also a "Knapper" block in a block's text (a "Banner"'s), where its quotes are written as &quot;.
145
+ if (!/data-block=(?:"|\\&quot;)buttons/.test(f.text)) continue;
146
+ const d = fm(f as RepoFile);
147
+ if (
148
+ blocksIn(f.text, true).some((b: { type: string; props: Record<string, unknown> }) => b.type === 'buttons' && opens(b.props.items))
149
+ )
150
+ found.push({ file: f.path, title: String(d.title ?? ''), path: shownAt(pagePath(f as RepoFile), files) });
151
+ } else if (f.path.startsWith('src/data/popups/') && f.path.endsWith('.json')) {
152
+ const d = json(f as RepoFile);
153
+ if (d.type === 'bookingbar' && d.enabled !== false && opens(d.buttons))
154
+ found.push({
155
+ file: f.path,
156
+ title: siteText('Handlingslinje'),
157
+ bar: { pages: d.where === 'all' || d.where === 'tags' ? undefined : Array.isArray(d.pages) ? d.pages.map(String) : [] },
158
+ });
159
+ } else if (f.path === config.blockTexts) {
160
+ if (opens((json(f as RepoFile).buttons as Record<string, unknown> | undefined)?.items))
161
+ found.push({ file: 'blokke/buttons', title: siteText('Standardknapper') });
162
+ }
163
+ }
164
+ return found;
165
+ }
166
+
167
+ // Whether a promotion as it is (or is about to be saved) is shown where a button opens it: turned on, of a kind a button
168
+ // can open, and on every page, or on the button's page (the shared buttons are on any page), or on every page the
169
+ // action bar is on.
170
+ function shownFor(d: Record<string, unknown>, at: Opener) {
171
+ if (d.enabled === false || !opensOf(d.type ? String(d.type) : undefined)) return false;
172
+ if (d.where === 'all') return true;
173
+ const pages = d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [];
174
+ if (at.bar) return !!at.bar.pages && at.bar.pages.every((p) => pages.includes(p));
175
+ return !!at.path && pages.includes(at.path);
176
+ }
177
+
178
+ const openerText = (at: Opener) =>
179
+ at.bar
180
+ ? siteText(
181
+ 'En knap i handlingslinjen åbner promoveringen, så den skal være slået til og vises alle steder, hvor linjen vises. Ret knappen først.',
182
+ )
183
+ : at.path
184
+ ? siteText('En knap på siden "{side}" åbner promoveringen, så den skal være slået til og vises dér. Ret knappen først.', {
185
+ side: at.title,
186
+ })
187
+ : siteText('Standardknapperne åbner promoveringen, så den skal være slået til og vises på alle sider. Ret dem først.');
188
+ const openerEdit = (at: Opener) => ({
189
+ to: at.file,
190
+ text: siteText(at.bar ? 'Rediger handlingslinjen' : at.path ? 'Rediger siden' : 'Rediger standardknapperne'),
191
+ });
192
+
193
+ /** Why a promotion cannot be saved like this (turned off, or no longer shown on a page where a button opens it,
194
+ * "Knapper": "Åbn promovering"), with a link to the button, or the standard pages (the same page twice, or a page
195
+ * without the block it must have, with a link to it), or null. Before it is saved (DataForm.tsx; the standard pages in
196
+ * CompanyForm.tsx, without a collection). */
197
+ export function keptBy(collection: Collection | null, path: string, data: Record<string, unknown>, files: RepoFile[]): Status {
198
+ if (path === config.standardPages) return standardKept(data, files);
199
+ if (collection?.kind !== 'folder' || collection.id !== 'popups') return null;
200
+ const at = openersOf(slugOf(path, collection.folder), files).find((o) => !shownFor(data, o));
201
+ return at ? { text: openerText(at), error: true, edit: openerEdit(at) } : null;
202
+ }
203
+
204
+ // The standard pages as they are about to be saved: different pages (the cookie policy may be the privacy policy), each
205
+ // with the block it must have.
206
+ function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status {
207
+ const chosen = standardPages.map((p) => ({ ...p, path: String(data[p.key] ?? '') })).filter((p) => p.path);
208
+ if (!differentPages(Object.fromEntries(chosen.map((p) => [p.key, p.path]))))
209
+ return { text: siteText('Standardsiderne skal være forskellige sider.'), error: true };
210
+ for (const p of chosen) {
211
+ const page = files.find((f) => isPage(f) && pagePath(f) === p.path);
212
+ if (!p.needs || !page || blocksIn(page.text).some((b: { type: string }) => b.type === p.needs!.type)) continue;
213
+ return {
214
+ text: siteText('Siden "{side}" har ikke blokken {blok}, som {hvad} skal have. Sæt den ind på siden først.', {
215
+ side: String(fm(page).title),
216
+ blok: siteText(p.needs.label),
217
+ hvad: siteText(p.the).toLowerCase(),
218
+ }),
219
+ error: true,
220
+ edit: { to: page.path, text: siteText('Rediger siden') },
221
+ };
222
+ }
223
+ return null;
224
+ }
225
+
226
+ /** What is wrong on the site by its own rules, a line for each, before it is built (checkSite.ts): a standard page that
227
+ * is not a page on the site, or the same page twice, and a button that opens a promotion that is gone, turned off or not
228
+ * shown on its page. */
229
+ export function siteProblems(files: { path: string; text: string }[]): string[] {
230
+ const problems: string[] = [];
231
+ // The standard pages are pages on the site ("Indstillinger" › "Udseende" › "Standardsider"), different ones (but for
232
+ // the cookie policy, which may be the privacy policy).
233
+ if (files.some((f) => f.path === config.standardPages)) {
234
+ for (const p of standardPages) {
235
+ const page = standardFile(files as RepoFile[], p.key);
236
+ if (!page || ['draft', 'scheduled'].includes(String(fm(page).status ?? '')))
237
+ problems.push(`${config.standardPages}: ${siteText('{side} skal være en side, der er udgivet.', { side: siteText(p.the) })}`);
238
+ }
239
+ if (!differentPages(standardOf(files)))
240
+ problems.push(`${config.standardPages}: ${siteText('Standardsiderne skal være forskellige sider.')}`);
241
+ }
242
+ const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
243
+ const slugs = new Set<string>();
244
+ for (const f of files) for (const m of f.text.matchAll(/(?:&quot;|")open(?:&quot;|"):\s*(?:&quot;|")([^"&]+)/g)) slugs.add(m[1]);
245
+ for (const slug of slugs) {
246
+ const file = promotions.find((f) => slugOf(f.path, 'src/data/popups') === slug);
247
+ for (const at of openersOf(slug, files)) {
248
+ if (file && shownFor(json(file as RepoFile), at)) continue;
249
+ problems.push(
250
+ `${at.file.startsWith('blokke/') ? config.blockTexts : at.file}: ${
251
+ file ? openerText(at) : siteText('En knap åbner promoveringen "{navn}", som ikke findes.', { navn: slug })
252
+ }`,
253
+ );
254
+ }
255
+ }
256
+ return problems;
257
+ }
258
+
259
+ // The tags a promotion is shown on the posts of (Promovering, "Vises på": "Blogindlæg med tags").
260
+ const promotionTags = (d: Record<string, unknown>): string[] => (d.where === 'tags' && Array.isArray(d.tags) ? d.tags.map(String) : []);
261
+
262
+ // Why an entry cannot be deleted (something on the site uses it), with a link to where, or null.
263
+ export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]): Status {
264
+ if (collection.kind === 'files') return null;
265
+ const slug = slugOf(file.path, collection.folder);
266
+ // A promotion a button opens ("Knapper": "Åbn promovering").
267
+ if (collection.id === 'popups') {
268
+ const [at] = openersOf(slug, files);
269
+ if (at)
270
+ return {
271
+ text: at.bar
272
+ ? siteText('En knap i handlingslinjen åbner promoveringen. Ret knappen først.')
273
+ : at.path
274
+ ? siteText('En knap på siden "{side}" åbner promoveringen. Ret knappen først.', { side: at.title })
275
+ : siteText('Standardknapperne åbner promoveringen. Ret dem først.'),
276
+ error: true,
277
+ edit: openerEdit(at),
278
+ };
279
+ }
280
+ const home = standardFile(files, 'front');
281
+ const front = Object.fromEntries(
282
+ (home ? (blocksIn(home.text) as { type: string; props: Record<string, unknown> }[]) : []).map((b) => [b.type, b.props]),
283
+ );
284
+ if (collection.id === 'kategorier') {
285
+ const posts = files.filter(
286
+ (f) => f.path.startsWith(`${config.folders.blog}/`) && ((fm(f).categories as string[]) ?? []).includes(slug),
287
+ );
288
+ if (posts.length)
289
+ return {
290
+ text:
291
+ posts.length === 1
292
+ ? siteText('Tagget bruges på et blogindlæg. Fjern det fra det først.')
293
+ : siteText('Tagget bruges på {antal} blogindlæg. Fjern det fra dem først.', { antal: posts.length }),
294
+ error: true,
295
+ };
296
+ const quotes = files.filter(
297
+ (f) => f.path.startsWith('src/data/testimonials/') && ((json(f).categories as string[] | undefined) ?? []).includes(slug),
298
+ );
299
+ if (quotes.length)
300
+ return {
301
+ text:
302
+ quotes.length === 1
303
+ ? siteText('Tagget bruges på en udtalelse. Fjern det fra den først.')
304
+ : siteText('Tagget bruges på {antal} udtalelser. Fjern det fra dem først.', { antal: quotes.length }),
305
+ error: true,
306
+ };
307
+ // A promotion shown on the posts with the tag (Promovering, "Blogindlæg med tags").
308
+ const promotion = files.find(
309
+ (f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json') && promotionTags(json(f)).includes(slug),
310
+ );
311
+ if (promotion) {
312
+ const d = json(promotion);
313
+ return {
314
+ text: siteText('Tagget bruges i promoveringen "{navn}". Fjern det fra den først.', { navn: String(d.heading ?? d.name ?? '') }),
315
+ error: true,
316
+ edit: { to: promotion.path, text: siteText('Rediger promoveringen') },
317
+ };
318
+ }
319
+ const fp = front.posts as Record<string, unknown> | undefined;
320
+ if ((fp?.categories as string[] | undefined)?.includes(slug) || fp?.link === slug)
321
+ return {
322
+ text: siteText('Tagget vælger indlæggene på forsiden under "Fra bloggen". Vælg et andet først.'),
323
+ error: true,
324
+ edit: frontPage(files),
325
+ };
326
+ }
327
+ if (collection.id === 'blog' && ((front.posts as Record<string, unknown>)?.picked as string[])?.includes(slug))
328
+ return {
329
+ text: siteText('Indlægget er valgt til forsiden under "Fra bloggen". Vælg et andet først.'),
330
+ error: true,
331
+ edit: frontPage(files),
332
+ };
333
+ // A "Blogindlæg" block in a page that picks the post (its props are in the text).
334
+ if (collection.id === 'blog') {
335
+ const page = files.find(
336
+ (f) =>
337
+ f.path.startsWith(`${config.folders.pages}/`) && f.text.includes('data-block="posts"') && f.text.includes(`&quot;${slug}&quot;`),
338
+ );
339
+ if (page)
340
+ return {
341
+ text: siteText('Indlægget er valgt i en blok med blogindlæg på siden "{side}". Vælg et andet først.', {
342
+ side: String(fm(page).title),
343
+ }),
344
+ error: true,
345
+ edit: { to: page.path, text: siteText('Rediger siden') },
346
+ };
347
+ }
348
+ // Any other place under "Bruges på" (usesOf): a "Blogindlæg" or "Udtalelser" block that picks the tag, on a
349
+ // page or in a post.
350
+ if (collection.id === 'kategorier') {
351
+ const use = usesOf(collection, files)(file)[0];
352
+ if (use) {
353
+ const post = use.path.startsWith(`${config.folders.blog}/`);
354
+ return {
355
+ text: siteText(
356
+ post
357
+ ? 'Tagget er valgt i en blok i blogindlægget "{side}". Vælg et andet først.'
358
+ : 'Tagget er valgt i en blok på siden "{side}". Vælg et andet først.',
359
+ { side: use.name },
360
+ ),
361
+ error: true,
362
+ edit: { to: use.path, text: siteText(post ? 'Rediger indlægget' : 'Rediger siden') },
363
+ };
364
+ }
365
+ }
366
+ return null;
367
+ }
368
+
369
+ // What a file that uses something is called: a page's or post's title, a setting's name, an entry's
370
+ // as its list shows it.
371
+ function nameOf(file: RepoFile): string {
372
+ for (const c of collections)
373
+ if (c.kind === 'files') {
374
+ const own = c.files.find((f) => f.file === file.path);
375
+ if (own) return fixedName({ ...own, label: siteText(own.label) }, file.text);
376
+ } else if (c.kind === 'folder' && file.path.startsWith(`${c.folder}/`) && file.path.endsWith('.json'))
377
+ return c.summary(json(file)) || siteText(c.label);
378
+ return String(fm(file).title ?? file.path);
379
+ }
380
+
381
+ export type Use = { path: string; name: string };
382
+
383
+ // The places on the site that use an entry, by the entry's file, each once (the tag's "Bruges på", DataForm.tsx,
384
+ // and the number at it in its list, Library.tsx); [] for none. For a tag: the blog posts, testimonials and
385
+ // promotions that have it, and the pages and posts with a "Blogindlæg" or "Udtalelser" block that
386
+ // picks it (its tags, or the tag "Alle indlæg" goes to). Made once for the whole list.
387
+ export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => Use[] {
388
+ if (collection.kind === 'files' || collection.id !== 'kategorier') return () => [];
389
+ const uses = tagUses(files);
390
+ return (file) =>
391
+ [...(uses.get(slugOf(file.path, collection.folder))?.values() ?? [])]
392
+ .map((f) => ({ path: f.path, name: nameOf(f) }))
393
+ .sort((a, b) => byText(a.name, b.name));
394
+ }
395
+
396
+ // The files that use each tag (usesOf), by the tag, each file once.
397
+ function tagUses(files: RepoFile[]): Map<string, Map<string, RepoFile>> {
398
+ const uses = new Map<string, Map<string, RepoFile>>();
399
+ const add = (f: RepoFile, slugs: unknown) => {
400
+ for (const slug of Array.isArray(slugs) ? slugs.map(String) : []) {
401
+ if (!uses.has(slug)) uses.set(slug, new Map());
402
+ uses.get(slug)!.set(f.path, f);
403
+ }
404
+ };
405
+ const block = (f: RepoFile, type: unknown, props: Record<string, unknown>) => {
406
+ if (type === 'posts' || type === 'testimonials')
407
+ add(f, [...(Array.isArray(props.categories) ? props.categories : []), ...(props.link ? [props.link] : [])]);
408
+ };
409
+ const inFolder = (f: RepoFile, folder: string, ext: string) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext);
410
+ for (const f of files) {
411
+ if (inFolder(f, config.folders.blog, '.md') || inFolder(f, config.folders.pages, '.md')) {
412
+ if (inFolder(f, config.folders.blog, '.md')) add(f, fm(f).categories);
413
+ for (const b of blocksIn(f.text) as { type: string; props: Record<string, unknown> }[]) block(f, b.type, b.props);
414
+ } else if (inFolder(f, 'src/data/testimonials', '.json')) add(f, json(f).categories);
415
+ else if (inFolder(f, 'src/data/popups', '.json')) add(f, promotionTags(json(f)));
416
+ }
417
+ return uses;
418
+ }
419
+
420
+ // The number at the right of an entry in the list of a part, by the entry's file, or null for none
421
+ // (Library.tsx): at each tag, the number of places that use it (usesOf).
422
+ export function countsOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => number | null {
423
+ if (collection.kind === 'files' || collection.id !== 'kategorier') return () => null;
424
+ const uses = tagUses(files);
425
+ return (file) => uses.get(slugOf(file.path, collection.folder))?.size ?? 0;
426
+ }
427
+
428
+ // The testimonials, in their order on the site, for the cards and previews of the blocks.
429
+ export const testimonialsOf = (files: RepoFile[]) =>
430
+ files
431
+ .filter((f) => f.path.startsWith('src/data/testimonials/') && f.path.endsWith('.json'))
432
+ .map((f) => ({ ...(json(f) as TestimonialRow & { order?: number }), slug: slugOf(f.path, 'src/data/testimonials') }))
433
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
434
+
435
+ /** What is at an address, as said in a message: "forsiden", 'siden "Om mig" (/om-mig/)',
436
+ * or the address (where a redirect sends to, App.tsx). */
437
+ export function whatIsAt(address: string, relations: Record<Relation, Option[]>): string {
438
+ const base = address.split('#')[0];
439
+ if (base === '/') return siteText('forsiden');
440
+ if (base === '/blog/') return siteText('bloggen');
441
+ const page = [...relations.fixed, ...relations.pages].find((o) => o.value === base);
442
+ if (page) return siteText('siden "{navn}" ({adresse})', { navn: page.label, adresse: address });
443
+ const post = relations.blog.find((o) => `/${o.value}/` === base);
444
+ if (post) return siteText('blogindlægget "{navn}" ({adresse})', { navn: post.label, adresse: address });
445
+ const tag = relations.categories.find((o) => `/blog/kategori/${o.value}/` === base);
446
+ if (tag) return siteText('tagget "{navn}" på bloggen ({adresse})', { navn: tag.label, adresse: address });
447
+ if (base.startsWith(config.folders.media.replace(/^public/, '') + '/'))
448
+ return siteText('billedet {navn}', { navn: base.split('/').pop()! });
449
+ return address;
450
+ }