@morphemecms/cms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -3
  3. package/astro.d.mts +2 -0
  4. package/astro.mjs +162 -0
  5. package/bin/morpheme.mjs +38 -0
  6. package/client.d.ts +10 -0
  7. package/defaults/blockTexts.ts +33 -0
  8. package/defaults/collections.ts +774 -0
  9. package/defaults/colors.ts +125 -0
  10. package/defaults/company.ts +20 -0
  11. package/defaults/editor.ts +178 -0
  12. package/defaults/internalLinks.ts +30 -0
  13. package/defaults/opens.ts +18 -0
  14. package/defaults/popup.tsx +93 -0
  15. package/defaults/privacy.ts +79 -0
  16. package/defaults/rules.ts +450 -0
  17. package/defaults/schema.ts +16 -0
  18. package/defaults/sitedata.ts +129 -0
  19. package/defaults/standard.ts +106 -0
  20. package/defaults/standardPages.ts +87 -0
  21. package/defaults/texts.ts +481 -0
  22. package/demo/README.md +26 -0
  23. package/demo/public/apple-touch-icon.png +0 -0
  24. package/demo/public/editor/apple-touch-icon.png +0 -0
  25. package/demo/public/editor/icon-192.png +0 -0
  26. package/demo/public/editor/icon-512.png +0 -0
  27. package/demo/public/editor/icon-maskable-512.png +0 -0
  28. package/demo/public/favicon.ico +0 -0
  29. package/demo/public/favicon.svg +1 -0
  30. package/demo/public/images/uploads/2026/10/andromeda.webp +0 -0
  31. package/demo/public/images/uploads/2026/10/earth-from-space.webp +0 -0
  32. package/demo/public/images/uploads/2026/10/editor-blog.webp +0 -0
  33. package/demo/public/images/uploads/2026/10/editor-colors.webp +0 -0
  34. package/demo/public/images/uploads/2026/10/editor-home.webp +0 -0
  35. package/demo/public/images/uploads/2026/10/editor-page.webp +0 -0
  36. package/demo/public/images/uploads/2026/10/editor-pages.webp +0 -0
  37. package/demo/public/images/uploads/2026/10/editor-seo.webp +0 -0
  38. package/demo/public/images/uploads/2026/10/editor-start.webp +0 -0
  39. package/demo/public/images/uploads/2026/10/editor-themes.webp +0 -0
  40. package/demo/public/images/uploads/2026/10/moon.webp +0 -0
  41. package/demo/public/images/uploads/2026/10/saturn.webp +0 -0
  42. package/demo/public/logo.svg +1 -0
  43. package/demo/src/content/blog/build-a-page-from-blocks.md +19 -0
  44. package/demo/src/content/blog/colors-everyone-can-read.md +19 -0
  45. package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -0
  46. package/demo/src/content/blog/every-save-is-a-version.md +21 -0
  47. package/demo/src/content/blog/schedule-a-post.md +17 -0
  48. package/demo/src/content/blog/secure-from-day-one.md +17 -0
  49. package/demo/src/content/blog/seo-without-the-guesswork.md +23 -0
  50. package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -0
  51. package/demo/src/content/blog/the-app-store.md +21 -0
  52. package/demo/src/content/pages/about.md +22 -0
  53. package/demo/src/content/pages/app-store.md +30 -0
  54. package/demo/src/content/pages/blocks.md +98 -0
  55. package/demo/src/content/pages/blog-page.md +11 -0
  56. package/demo/src/content/pages/contact.md +16 -0
  57. package/demo/src/content/pages/get-started.md +20 -0
  58. package/demo/src/content/pages/home.md +26 -0
  59. package/demo/src/content/pages/page-not-found.md +11 -0
  60. package/demo/src/content/pages/privacy-policy.md +24 -0
  61. package/demo/src/data/blocks.json +25 -0
  62. package/demo/src/data/booking.json +6 -0
  63. package/demo/src/data/categories/apps.json +6 -0
  64. package/demo/src/data/categories/design.json +6 -0
  65. package/demo/src/data/categories/editing.json +6 -0
  66. package/demo/src/data/categories/security.json +6 -0
  67. package/demo/src/data/categories/seo.json +6 -0
  68. package/demo/src/data/colors.json +75 -0
  69. package/demo/src/data/contact.json +8 -0
  70. package/demo/src/data/cookies.json +21 -0
  71. package/demo/src/data/footer.json +27 -0
  72. package/demo/src/data/menu.json +35 -0
  73. package/demo/src/data/mobile-menu.json +25 -0
  74. package/demo/src/data/popups/booking-bar.json +13 -0
  75. package/demo/src/data/popups/try-the-editor.json +16 -0
  76. package/demo/src/data/standard-pages.json +8 -0
  77. package/demo/src/data/testimonials/sample-bakery.json +10 -0
  78. package/demo/src/data/testimonials/sample-clinic.json +10 -0
  79. package/demo/src/data/testimonials/sample-studio.json +10 -0
  80. package/demo/src/site/colors.ts +11 -0
  81. package/demo/src/site/config.ts +19 -0
  82. package/icons/lucide/LICENSE +43 -0
  83. package/icons/lucide/LICENSE-material-design-icons +20 -0
  84. package/icons/lucide/address-book.svg +1 -0
  85. package/icons/lucide/app-window.svg +1 -0
  86. package/icons/lucide/arrow-counter-clockwise.svg +1 -0
  87. package/icons/lucide/arrow-down.svg +1 -0
  88. package/icons/lucide/arrow-left.svg +1 -0
  89. package/icons/lucide/arrow-right.svg +1 -0
  90. package/icons/lucide/arrow-square-out.svg +1 -0
  91. package/icons/lucide/arrow-u-up-left.svg +1 -0
  92. package/icons/lucide/arrow-u-up-right.svg +1 -0
  93. package/icons/lucide/arrow-up-right.svg +1 -0
  94. package/icons/lucide/arrow-up.svg +1 -0
  95. package/icons/lucide/arrows-down-up.svg +1 -0
  96. package/icons/lucide/arrows-in-line-horizontal.svg +1 -0
  97. package/icons/lucide/arrows-out-line-horizontal.svg +1 -0
  98. package/icons/lucide/article.svg +1 -0
  99. package/icons/lucide/bank.svg +1 -0
  100. package/icons/lucide/bell.svg +1 -0
  101. package/icons/lucide/bicycle.svg +1 -0
  102. package/icons/lucide/binoculars.svg +1 -0
  103. package/icons/lucide/book-open-text.svg +1 -0
  104. package/icons/lucide/book-open.svg +1 -0
  105. package/icons/lucide/browser.svg +1 -0
  106. package/icons/lucide/buildings.svg +1 -0
  107. package/icons/lucide/bus.svg +1 -0
  108. package/icons/lucide/calendar-blank.svg +1 -0
  109. package/icons/lucide/calendar-check.svg +1 -0
  110. package/icons/lucide/car.svg +1 -0
  111. package/icons/lucide/cards.svg +1 -0
  112. package/icons/lucide/caret-down.svg +1 -0
  113. package/icons/lucide/caret-left.svg +1 -0
  114. package/icons/lucide/caret-right.svg +1 -0
  115. package/icons/lucide/caret-up.svg +1 -0
  116. package/icons/lucide/certificate.svg +1 -0
  117. package/icons/lucide/chart-line-up.svg +1 -0
  118. package/icons/lucide/chats.svg +1 -0
  119. package/icons/lucide/check-circle.svg +1 -0
  120. package/icons/lucide/check.svg +1 -0
  121. package/icons/lucide/circle-half.svg +1 -0
  122. package/icons/lucide/circle.svg +1 -0
  123. package/icons/lucide/clock-counter-clockwise.svg +1 -0
  124. package/icons/lucide/clock.svg +1 -0
  125. package/icons/lucide/code.svg +1 -0
  126. package/icons/lucide/coffee.svg +1 -0
  127. package/icons/lucide/columns.svg +1 -0
  128. package/icons/lucide/cookie.svg +1 -0
  129. package/icons/lucide/copy.svg +1 -0
  130. package/icons/lucide/credit-card.svg +1 -0
  131. package/icons/lucide/currency-circle-dollar.svg +1 -0
  132. package/icons/lucide/cursor-click.svg +1 -0
  133. package/icons/lucide/database.svg +1 -0
  134. package/icons/lucide/desktop.svg +1 -0
  135. package/icons/lucide/device-mobile.svg +1 -0
  136. package/icons/lucide/door-open.svg +1 -0
  137. package/icons/lucide/download-simple.svg +1 -0
  138. package/icons/lucide/envelope-simple.svg +1 -0
  139. package/icons/lucide/eye-slash.svg +1 -0
  140. package/icons/lucide/eye.svg +1 -0
  141. package/icons/lucide/file-pdf.svg +1 -0
  142. package/icons/lucide/file-text.svg +1 -0
  143. package/icons/lucide/file-video.svg +1 -0
  144. package/icons/lucide/file-zip.svg +1 -0
  145. package/icons/lucide/flower-lotus.svg +1 -0
  146. package/icons/lucide/folder-simple.svg +1 -0
  147. package/icons/lucide/funnel.svg +1 -0
  148. package/icons/lucide/gauge.svg +1 -0
  149. package/icons/lucide/gear.svg +1 -0
  150. package/icons/lucide/github-logo.svg +1 -0
  151. package/icons/lucide/globe.svg +1 -0
  152. package/icons/lucide/google-logo.svg +1 -0
  153. package/icons/lucide/graduation-cap.svg +1 -0
  154. package/icons/lucide/hammer.svg +1 -0
  155. package/icons/lucide/hand-heart.svg +1 -0
  156. package/icons/lucide/handshake.svg +1 -0
  157. package/icons/lucide/hard-drives.svg +1 -0
  158. package/icons/lucide/heart.svg +1 -0
  159. package/icons/lucide/house.svg +1 -0
  160. package/icons/lucide/image-square.svg +1 -0
  161. package/icons/lucide/image.svg +1 -0
  162. package/icons/lucide/info.svg +1 -0
  163. package/icons/lucide/keyboard.svg +1 -0
  164. package/icons/lucide/leaf.svg +1 -0
  165. package/icons/lucide/lifebuoy.svg +1 -0
  166. package/icons/lucide/lightbulb.svg +1 -0
  167. package/icons/lucide/link-break.svg +1 -0
  168. package/icons/lucide/link-simple.svg +1 -0
  169. package/icons/lucide/list-bullets.svg +1 -0
  170. package/icons/lucide/list-checks.svg +1 -0
  171. package/icons/lucide/list-numbers.svg +1 -0
  172. package/icons/lucide/list.svg +1 -0
  173. package/icons/lucide/lock-simple-open.svg +1 -0
  174. package/icons/lucide/lock-simple.svg +1 -0
  175. package/icons/lucide/magnifying-glass.svg +1 -0
  176. package/icons/lucide/map-pin.svg +1 -0
  177. package/icons/lucide/map-trifold.svg +1 -0
  178. package/icons/lucide/medal.svg +1 -0
  179. package/icons/lucide/megaphone.svg +1 -0
  180. package/icons/lucide/minus.svg +1 -0
  181. package/icons/lucide/moon.svg +1 -0
  182. package/icons/lucide/newspaper.svg +1 -0
  183. package/icons/lucide/pack.json +23 -0
  184. package/icons/lucide/paint-brush-broad.svg +1 -0
  185. package/icons/lucide/palette.svg +1 -0
  186. package/icons/lucide/paper-plane-tilt.svg +1 -0
  187. package/icons/lucide/pencil-simple.svg +1 -0
  188. package/icons/lucide/phone.svg +1 -0
  189. package/icons/lucide/play-fill.svg +1 -0
  190. package/icons/lucide/plus.svg +1 -0
  191. package/icons/lucide/prohibit.svg +1 -0
  192. package/icons/lucide/puzzle-piece.svg +1 -0
  193. package/icons/lucide/question.svg +1 -0
  194. package/icons/lucide/quotes.svg +1 -0
  195. package/icons/lucide/rows.svg +1 -0
  196. package/icons/lucide/ruler.svg +1 -0
  197. package/icons/lucide/scan.svg +1 -0
  198. package/icons/lucide/shapes.svg +1 -0
  199. package/icons/lucide/share-network.svg +1 -0
  200. package/icons/lucide/shield-check.svg +1 -0
  201. package/icons/lucide/sidebar-simple.svg +1 -0
  202. package/icons/lucide/sign-out.svg +1 -0
  203. package/icons/lucide/signpost.svg +1 -0
  204. package/icons/lucide/sliders-horizontal.svg +1 -0
  205. package/icons/lucide/smiley.svg +1 -0
  206. package/icons/lucide/sparkle.svg +1 -0
  207. package/icons/lucide/square-half-bottom.svg +1 -0
  208. package/icons/lucide/square.svg +1 -0
  209. package/icons/lucide/squares-four.svg +1 -0
  210. package/icons/lucide/sticker.svg +1 -0
  211. package/icons/lucide/storefront.svg +1 -0
  212. package/icons/lucide/sun.svg +1 -0
  213. package/icons/lucide/swatches.svg +1 -0
  214. package/icons/lucide/tag.svg +1 -0
  215. package/icons/lucide/text-aa.svg +1 -0
  216. package/icons/lucide/text-align-left.svg +1 -0
  217. package/icons/lucide/text-b.svg +1 -0
  218. package/icons/lucide/text-h-one.svg +1 -0
  219. package/icons/lucide/text-h-three.svg +1 -0
  220. package/icons/lucide/text-h-two.svg +1 -0
  221. package/icons/lucide/text-italic.svg +1 -0
  222. package/icons/lucide/text-t.svg +1 -0
  223. package/icons/lucide/ticket.svg +1 -0
  224. package/icons/lucide/toggle-right.svg +1 -0
  225. package/icons/lucide/train.svg +1 -0
  226. package/icons/lucide/trash.svg +1 -0
  227. package/icons/lucide/upload-simple.svg +1 -0
  228. package/icons/lucide/user-circle.svg +1 -0
  229. package/icons/lucide/user.svg +1 -0
  230. package/icons/lucide/users-three.svg +1 -0
  231. package/icons/lucide/users.svg +1 -0
  232. package/icons/lucide/video-camera.svg +1 -0
  233. package/icons/lucide/warning-circle.svg +1 -0
  234. package/icons/lucide/warning.svg +1 -0
  235. package/icons/lucide/wave-sine.svg +1 -0
  236. package/icons/lucide/wheelchair.svg +1 -0
  237. package/icons/lucide/wrench.svg +1 -0
  238. package/icons/lucide/x.svg +1 -0
  239. package/icons/material-symbols/LICENSE +201 -0
  240. package/icons/material-symbols/LICENSE-material-design-icons +20 -0
  241. package/icons/material-symbols/address-book.svg +1 -0
  242. package/icons/material-symbols/app-window.svg +1 -0
  243. package/icons/material-symbols/arrow-counter-clockwise.svg +1 -0
  244. package/icons/material-symbols/arrow-down.svg +1 -0
  245. package/icons/material-symbols/arrow-left.svg +1 -0
  246. package/icons/material-symbols/arrow-right.svg +1 -0
  247. package/icons/material-symbols/arrow-square-out.svg +1 -0
  248. package/icons/material-symbols/arrow-u-up-left.svg +1 -0
  249. package/icons/material-symbols/arrow-u-up-right.svg +1 -0
  250. package/icons/material-symbols/arrow-up-right.svg +1 -0
  251. package/icons/material-symbols/arrow-up.svg +1 -0
  252. package/icons/material-symbols/arrows-down-up.svg +1 -0
  253. package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -0
  254. package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -0
  255. package/icons/material-symbols/article.svg +1 -0
  256. package/icons/material-symbols/bank.svg +1 -0
  257. package/icons/material-symbols/bell.svg +1 -0
  258. package/icons/material-symbols/bicycle.svg +1 -0
  259. package/icons/material-symbols/binoculars.svg +1 -0
  260. package/icons/material-symbols/book-open-text.svg +1 -0
  261. package/icons/material-symbols/book-open.svg +1 -0
  262. package/icons/material-symbols/browser.svg +1 -0
  263. package/icons/material-symbols/buildings.svg +1 -0
  264. package/icons/material-symbols/bus.svg +1 -0
  265. package/icons/material-symbols/calendar-blank.svg +1 -0
  266. package/icons/material-symbols/calendar-check.svg +1 -0
  267. package/icons/material-symbols/car.svg +1 -0
  268. package/icons/material-symbols/cards.svg +1 -0
  269. package/icons/material-symbols/caret-down.svg +1 -0
  270. package/icons/material-symbols/caret-left.svg +1 -0
  271. package/icons/material-symbols/caret-right.svg +1 -0
  272. package/icons/material-symbols/caret-up.svg +1 -0
  273. package/icons/material-symbols/certificate.svg +1 -0
  274. package/icons/material-symbols/chart-line-up.svg +1 -0
  275. package/icons/material-symbols/chats.svg +1 -0
  276. package/icons/material-symbols/check-circle.svg +1 -0
  277. package/icons/material-symbols/check.svg +1 -0
  278. package/icons/material-symbols/circle-half.svg +1 -0
  279. package/icons/material-symbols/circle.svg +1 -0
  280. package/icons/material-symbols/clock-counter-clockwise.svg +1 -0
  281. package/icons/material-symbols/clock.svg +1 -0
  282. package/icons/material-symbols/code.svg +1 -0
  283. package/icons/material-symbols/coffee.svg +1 -0
  284. package/icons/material-symbols/columns.svg +1 -0
  285. package/icons/material-symbols/cookie.svg +1 -0
  286. package/icons/material-symbols/copy.svg +1 -0
  287. package/icons/material-symbols/credit-card.svg +1 -0
  288. package/icons/material-symbols/currency-circle-dollar.svg +1 -0
  289. package/icons/material-symbols/cursor-click.svg +1 -0
  290. package/icons/material-symbols/database.svg +1 -0
  291. package/icons/material-symbols/desktop.svg +1 -0
  292. package/icons/material-symbols/device-mobile.svg +1 -0
  293. package/icons/material-symbols/door-open.svg +1 -0
  294. package/icons/material-symbols/download-simple.svg +1 -0
  295. package/icons/material-symbols/envelope-simple.svg +1 -0
  296. package/icons/material-symbols/eye-slash.svg +1 -0
  297. package/icons/material-symbols/eye.svg +1 -0
  298. package/icons/material-symbols/file-pdf.svg +1 -0
  299. package/icons/material-symbols/file-text.svg +1 -0
  300. package/icons/material-symbols/file-video.svg +1 -0
  301. package/icons/material-symbols/file-zip.svg +1 -0
  302. package/icons/material-symbols/flower-lotus.svg +1 -0
  303. package/icons/material-symbols/folder-simple.svg +1 -0
  304. package/icons/material-symbols/funnel.svg +1 -0
  305. package/icons/material-symbols/gauge.svg +1 -0
  306. package/icons/material-symbols/gear.svg +1 -0
  307. package/icons/material-symbols/github-logo.svg +1 -0
  308. package/icons/material-symbols/globe.svg +1 -0
  309. package/icons/material-symbols/google-logo.svg +1 -0
  310. package/icons/material-symbols/graduation-cap.svg +1 -0
  311. package/icons/material-symbols/hammer.svg +1 -0
  312. package/icons/material-symbols/hand-heart.svg +1 -0
  313. package/icons/material-symbols/handshake.svg +1 -0
  314. package/icons/material-symbols/hard-drives.svg +1 -0
  315. package/icons/material-symbols/heart.svg +1 -0
  316. package/icons/material-symbols/house.svg +1 -0
  317. package/icons/material-symbols/image-square.svg +1 -0
  318. package/icons/material-symbols/image.svg +1 -0
  319. package/icons/material-symbols/info.svg +1 -0
  320. package/icons/material-symbols/keyboard.svg +1 -0
  321. package/icons/material-symbols/leaf.svg +1 -0
  322. package/icons/material-symbols/lifebuoy.svg +1 -0
  323. package/icons/material-symbols/lightbulb.svg +1 -0
  324. package/icons/material-symbols/link-break.svg +1 -0
  325. package/icons/material-symbols/link-simple.svg +1 -0
  326. package/icons/material-symbols/list-bullets.svg +1 -0
  327. package/icons/material-symbols/list-checks.svg +1 -0
  328. package/icons/material-symbols/list-numbers.svg +1 -0
  329. package/icons/material-symbols/list.svg +1 -0
  330. package/icons/material-symbols/lock-simple-open.svg +1 -0
  331. package/icons/material-symbols/lock-simple.svg +1 -0
  332. package/icons/material-symbols/magnifying-glass.svg +1 -0
  333. package/icons/material-symbols/map-pin.svg +1 -0
  334. package/icons/material-symbols/map-trifold.svg +1 -0
  335. package/icons/material-symbols/medal.svg +1 -0
  336. package/icons/material-symbols/megaphone.svg +1 -0
  337. package/icons/material-symbols/minus.svg +1 -0
  338. package/icons/material-symbols/moon.svg +1 -0
  339. package/icons/material-symbols/newspaper.svg +1 -0
  340. package/icons/material-symbols/pack.json +29 -0
  341. package/icons/material-symbols/paint-brush-broad.svg +1 -0
  342. package/icons/material-symbols/palette.svg +1 -0
  343. package/icons/material-symbols/paper-plane-tilt.svg +1 -0
  344. package/icons/material-symbols/pencil-simple.svg +1 -0
  345. package/icons/material-symbols/phone.svg +1 -0
  346. package/icons/material-symbols/play-fill.svg +1 -0
  347. package/icons/material-symbols/plus.svg +1 -0
  348. package/icons/material-symbols/prohibit.svg +1 -0
  349. package/icons/material-symbols/puzzle-piece.svg +1 -0
  350. package/icons/material-symbols/question.svg +1 -0
  351. package/icons/material-symbols/quotes.svg +1 -0
  352. package/icons/material-symbols/rows.svg +1 -0
  353. package/icons/material-symbols/ruler.svg +1 -0
  354. package/icons/material-symbols/scan.svg +1 -0
  355. package/icons/material-symbols/shapes.svg +1 -0
  356. package/icons/material-symbols/share-network.svg +1 -0
  357. package/icons/material-symbols/shield-check.svg +1 -0
  358. package/icons/material-symbols/sidebar-simple.svg +1 -0
  359. package/icons/material-symbols/sign-out.svg +1 -0
  360. package/icons/material-symbols/signpost.svg +1 -0
  361. package/icons/material-symbols/sliders-horizontal.svg +1 -0
  362. package/icons/material-symbols/smiley.svg +1 -0
  363. package/icons/material-symbols/sparkle.svg +1 -0
  364. package/icons/material-symbols/square-half-bottom.svg +1 -0
  365. package/icons/material-symbols/square.svg +1 -0
  366. package/icons/material-symbols/squares-four.svg +1 -0
  367. package/icons/material-symbols/sticker.svg +1 -0
  368. package/icons/material-symbols/storefront.svg +1 -0
  369. package/icons/material-symbols/sun.svg +1 -0
  370. package/icons/material-symbols/swatches.svg +1 -0
  371. package/icons/material-symbols/tag.svg +1 -0
  372. package/icons/material-symbols/text-aa.svg +1 -0
  373. package/icons/material-symbols/text-align-left.svg +1 -0
  374. package/icons/material-symbols/text-b.svg +1 -0
  375. package/icons/material-symbols/text-h-one.svg +1 -0
  376. package/icons/material-symbols/text-h-three.svg +1 -0
  377. package/icons/material-symbols/text-h-two.svg +1 -0
  378. package/icons/material-symbols/text-italic.svg +1 -0
  379. package/icons/material-symbols/text-t.svg +1 -0
  380. package/icons/material-symbols/ticket.svg +1 -0
  381. package/icons/material-symbols/toggle-right.svg +1 -0
  382. package/icons/material-symbols/train.svg +1 -0
  383. package/icons/material-symbols/trash.svg +1 -0
  384. package/icons/material-symbols/upload-simple.svg +1 -0
  385. package/icons/material-symbols/user-circle.svg +1 -0
  386. package/icons/material-symbols/user.svg +1 -0
  387. package/icons/material-symbols/users-three.svg +1 -0
  388. package/icons/material-symbols/users.svg +1 -0
  389. package/icons/material-symbols/video-camera.svg +1 -0
  390. package/icons/material-symbols/warning-circle.svg +1 -0
  391. package/icons/material-symbols/warning.svg +1 -0
  392. package/icons/material-symbols/wave-sine.svg +1 -0
  393. package/icons/material-symbols/wheelchair.svg +1 -0
  394. package/icons/material-symbols/wrench.svg +1 -0
  395. package/icons/material-symbols/x.svg +1 -0
  396. package/icons/phosphor/LICENSE +21 -0
  397. package/icons/phosphor/address-book.svg +1 -0
  398. package/icons/phosphor/app-window.svg +1 -0
  399. package/icons/phosphor/arrow-counter-clockwise.svg +1 -0
  400. package/icons/phosphor/arrow-down.svg +1 -0
  401. package/icons/phosphor/arrow-left.svg +1 -0
  402. package/icons/phosphor/arrow-right.svg +1 -0
  403. package/icons/phosphor/arrow-square-out.svg +1 -0
  404. package/icons/phosphor/arrow-u-up-left.svg +1 -0
  405. package/icons/phosphor/arrow-u-up-right.svg +1 -0
  406. package/icons/phosphor/arrow-up-right.svg +1 -0
  407. package/icons/phosphor/arrow-up.svg +1 -0
  408. package/icons/phosphor/arrows-down-up.svg +1 -0
  409. package/icons/phosphor/arrows-in-line-horizontal.svg +1 -0
  410. package/icons/phosphor/arrows-out-line-horizontal.svg +1 -0
  411. package/icons/phosphor/article.svg +1 -0
  412. package/icons/phosphor/bank.svg +1 -0
  413. package/icons/phosphor/bell.svg +1 -0
  414. package/icons/phosphor/bicycle.svg +1 -0
  415. package/icons/phosphor/binoculars.svg +1 -0
  416. package/icons/phosphor/book-open-text.svg +1 -0
  417. package/icons/phosphor/book-open.svg +1 -0
  418. package/icons/phosphor/browser.svg +1 -0
  419. package/icons/phosphor/buildings.svg +1 -0
  420. package/icons/phosphor/bus.svg +1 -0
  421. package/icons/phosphor/calendar-blank.svg +1 -0
  422. package/icons/phosphor/calendar-check.svg +1 -0
  423. package/icons/phosphor/car.svg +1 -0
  424. package/icons/phosphor/cards.svg +1 -0
  425. package/icons/phosphor/caret-down.svg +1 -0
  426. package/icons/phosphor/caret-left.svg +1 -0
  427. package/icons/phosphor/caret-right.svg +1 -0
  428. package/icons/phosphor/caret-up.svg +1 -0
  429. package/icons/phosphor/certificate.svg +1 -0
  430. package/icons/phosphor/chart-line-up.svg +1 -0
  431. package/icons/phosphor/chats.svg +1 -0
  432. package/icons/phosphor/check-circle.svg +1 -0
  433. package/icons/phosphor/check.svg +1 -0
  434. package/icons/phosphor/circle-half.svg +1 -0
  435. package/icons/phosphor/circle.svg +1 -0
  436. package/icons/phosphor/clock-counter-clockwise.svg +1 -0
  437. package/icons/phosphor/clock.svg +1 -0
  438. package/icons/phosphor/code.svg +1 -0
  439. package/icons/phosphor/coffee.svg +1 -0
  440. package/icons/phosphor/columns.svg +1 -0
  441. package/icons/phosphor/cookie.svg +1 -0
  442. package/icons/phosphor/copy.svg +1 -0
  443. package/icons/phosphor/credit-card.svg +1 -0
  444. package/icons/phosphor/currency-circle-dollar.svg +1 -0
  445. package/icons/phosphor/cursor-click.svg +1 -0
  446. package/icons/phosphor/database.svg +1 -0
  447. package/icons/phosphor/desktop.svg +1 -0
  448. package/icons/phosphor/device-mobile.svg +1 -0
  449. package/icons/phosphor/door-open.svg +1 -0
  450. package/icons/phosphor/download-simple.svg +1 -0
  451. package/icons/phosphor/envelope-simple.svg +1 -0
  452. package/icons/phosphor/eye-slash.svg +1 -0
  453. package/icons/phosphor/eye.svg +1 -0
  454. package/icons/phosphor/file-pdf.svg +1 -0
  455. package/icons/phosphor/file-text.svg +1 -0
  456. package/icons/phosphor/file-video.svg +1 -0
  457. package/icons/phosphor/file-zip.svg +1 -0
  458. package/icons/phosphor/flower-lotus.svg +1 -0
  459. package/icons/phosphor/folder-simple.svg +1 -0
  460. package/icons/phosphor/funnel.svg +1 -0
  461. package/icons/phosphor/gauge.svg +1 -0
  462. package/icons/phosphor/gear.svg +1 -0
  463. package/icons/phosphor/github-logo.svg +1 -0
  464. package/icons/phosphor/globe.svg +1 -0
  465. package/icons/phosphor/google-logo.svg +1 -0
  466. package/icons/phosphor/graduation-cap.svg +1 -0
  467. package/icons/phosphor/hammer.svg +1 -0
  468. package/icons/phosphor/hand-heart.svg +1 -0
  469. package/icons/phosphor/handshake.svg +1 -0
  470. package/icons/phosphor/hard-drives.svg +1 -0
  471. package/icons/phosphor/heart.svg +1 -0
  472. package/icons/phosphor/house.svg +1 -0
  473. package/icons/phosphor/image-square.svg +1 -0
  474. package/icons/phosphor/image.svg +1 -0
  475. package/icons/phosphor/info.svg +1 -0
  476. package/icons/phosphor/keyboard.svg +1 -0
  477. package/icons/phosphor/leaf.svg +1 -0
  478. package/icons/phosphor/lifebuoy.svg +1 -0
  479. package/icons/phosphor/lightbulb.svg +1 -0
  480. package/icons/phosphor/link-break.svg +1 -0
  481. package/icons/phosphor/link-simple.svg +1 -0
  482. package/icons/phosphor/list-bullets.svg +1 -0
  483. package/icons/phosphor/list-checks.svg +1 -0
  484. package/icons/phosphor/list-numbers.svg +1 -0
  485. package/icons/phosphor/list.svg +1 -0
  486. package/icons/phosphor/lock-simple-open.svg +1 -0
  487. package/icons/phosphor/lock-simple.svg +1 -0
  488. package/icons/phosphor/magnifying-glass.svg +1 -0
  489. package/icons/phosphor/map-pin.svg +1 -0
  490. package/icons/phosphor/map-trifold.svg +1 -0
  491. package/icons/phosphor/medal.svg +1 -0
  492. package/icons/phosphor/megaphone.svg +1 -0
  493. package/icons/phosphor/minus.svg +1 -0
  494. package/icons/phosphor/moon.svg +1 -0
  495. package/icons/phosphor/newspaper.svg +1 -0
  496. package/icons/phosphor/pack.json +8 -0
  497. package/icons/phosphor/paint-brush-broad.svg +1 -0
  498. package/icons/phosphor/palette.svg +1 -0
  499. package/icons/phosphor/paper-plane-tilt.svg +1 -0
  500. package/icons/phosphor/pencil-simple.svg +1 -0
  501. package/icons/phosphor/phone.svg +1 -0
  502. package/icons/phosphor/play-fill.svg +1 -0
  503. package/icons/phosphor/plus.svg +1 -0
  504. package/icons/phosphor/prohibit.svg +1 -0
  505. package/icons/phosphor/puzzle-piece.svg +1 -0
  506. package/icons/phosphor/question.svg +1 -0
  507. package/icons/phosphor/quotes.svg +1 -0
  508. package/icons/phosphor/rows.svg +1 -0
  509. package/icons/phosphor/ruler.svg +1 -0
  510. package/icons/phosphor/scan.svg +1 -0
  511. package/icons/phosphor/shapes.svg +1 -0
  512. package/icons/phosphor/share-network.svg +1 -0
  513. package/icons/phosphor/shield-check.svg +1 -0
  514. package/icons/phosphor/sidebar-simple.svg +1 -0
  515. package/icons/phosphor/sign-out.svg +1 -0
  516. package/icons/phosphor/signpost.svg +1 -0
  517. package/icons/phosphor/sliders-horizontal.svg +1 -0
  518. package/icons/phosphor/smiley.svg +1 -0
  519. package/icons/phosphor/sparkle.svg +1 -0
  520. package/icons/phosphor/square-half-bottom.svg +1 -0
  521. package/icons/phosphor/square.svg +1 -0
  522. package/icons/phosphor/squares-four.svg +1 -0
  523. package/icons/phosphor/sticker.svg +1 -0
  524. package/icons/phosphor/storefront.svg +1 -0
  525. package/icons/phosphor/sun.svg +1 -0
  526. package/icons/phosphor/swatches.svg +1 -0
  527. package/icons/phosphor/tag.svg +1 -0
  528. package/icons/phosphor/text-aa.svg +1 -0
  529. package/icons/phosphor/text-align-left.svg +1 -0
  530. package/icons/phosphor/text-b.svg +1 -0
  531. package/icons/phosphor/text-h-one.svg +1 -0
  532. package/icons/phosphor/text-h-three.svg +1 -0
  533. package/icons/phosphor/text-h-two.svg +1 -0
  534. package/icons/phosphor/text-italic.svg +1 -0
  535. package/icons/phosphor/text-t.svg +1 -0
  536. package/icons/phosphor/ticket.svg +1 -0
  537. package/icons/phosphor/toggle-right.svg +1 -0
  538. package/icons/phosphor/train.svg +1 -0
  539. package/icons/phosphor/trash.svg +1 -0
  540. package/icons/phosphor/upload-simple.svg +1 -0
  541. package/icons/phosphor/user-circle.svg +1 -0
  542. package/icons/phosphor/user.svg +1 -0
  543. package/icons/phosphor/users-three.svg +1 -0
  544. package/icons/phosphor/users.svg +1 -0
  545. package/icons/phosphor/video-camera.svg +1 -0
  546. package/icons/phosphor/warning-circle.svg +1 -0
  547. package/icons/phosphor/warning.svg +1 -0
  548. package/icons/phosphor/wave-sine.svg +1 -0
  549. package/icons/phosphor/wheelchair.svg +1 -0
  550. package/icons/phosphor/wrench.svg +1 -0
  551. package/icons/phosphor/x.svg +1 -0
  552. package/lib/apps.d.mts +28 -0
  553. package/lib/apps.mjs +139 -0
  554. package/lib/blocks.d.mts +23 -0
  555. package/lib/blocks.mjs +90 -0
  556. package/lib/colors.ts +86 -0
  557. package/lib/contactBar.ts +18 -0
  558. package/lib/cookieCategories.d.mts +17 -0
  559. package/lib/cookieCategories.mjs +28 -0
  560. package/lib/crop.d.mts +34 -0
  561. package/lib/crop.mjs +83 -0
  562. package/lib/csp.d.mts +22 -0
  563. package/lib/csp.mjs +130 -0
  564. package/lib/cspSources.d.mts +24 -0
  565. package/lib/cspSources.mjs +60 -0
  566. package/lib/dynamic-colors.ts +426 -0
  567. package/lib/git.d.mts +24 -0
  568. package/lib/git.mjs +64 -0
  569. package/lib/hooks.ts +51 -0
  570. package/lib/inline.ts +20 -0
  571. package/lib/license-texts/Apache-2.0.txt +191 -0
  572. package/lib/license-texts/BSD-2-Clause.txt +24 -0
  573. package/lib/license-texts/BSD-3-Clause.txt +28 -0
  574. package/lib/license-texts/ISC.txt +15 -0
  575. package/lib/license-texts/MIT.txt +21 -0
  576. package/lib/licenses.d.mts +190 -0
  577. package/lib/licenses.mjs +683 -0
  578. package/lib/markdown.ts +42 -0
  579. package/lib/motion-schemes.d.mts +2 -0
  580. package/lib/motion-schemes.mjs +5 -0
  581. package/lib/motion.ts +131 -0
  582. package/lib/notice.d.mts +3 -0
  583. package/lib/notice.mjs +34 -0
  584. package/lib/scheduled.d.mts +15 -0
  585. package/lib/scheduled.mjs +37 -0
  586. package/lib/snackbar.ts +149 -0
  587. package/lib/status.d.mts +7 -0
  588. package/lib/status.mjs +18 -0
  589. package/lib/svg.d.mts +22 -0
  590. package/lib/svg.mjs +231 -0
  591. package/lib/text-colors.d.mts +11 -0
  592. package/lib/text-colors.mjs +43 -0
  593. package/lib/video-id.d.mts +14 -0
  594. package/lib/video-id.mjs +72 -0
  595. package/lib/video.d.mts +20 -0
  596. package/lib/video.mjs +91 -0
  597. package/package.json +131 -6
  598. package/pages/404.astro +10 -0
  599. package/pages/[...slug].astro +51 -0
  600. package/pages/_headers.txt +31 -0
  601. package/pages/blog/index.astro +10 -0
  602. package/pages/blog/kategori/[category].astro +18 -0
  603. package/pages/editor/code-images.json.js +25 -0
  604. package/pages/editor/index.astro +75 -0
  605. package/pages/editor/manifest.webmanifest.js +23 -0
  606. package/pages/editor/sw.js +33 -0
  607. package/pages/editor/sw.js.js +4 -0
  608. package/pages/editor/thumbnails.json.js +21 -0
  609. package/pages/editor/version.json.js +22 -0
  610. package/pages/headers.js +4 -0
  611. package/pages/index.astro +10 -0
  612. package/pages/ny.astro +6 -0
  613. package/pages/robots.txt.js +3 -0
  614. package/pages/rss.xml.js +20 -0
  615. package/scripts/app-screenshots.mjs +164 -0
  616. package/scripts/app-workers.mjs +6 -0
  617. package/scripts/bin.mjs +12 -0
  618. package/scripts/built-site.mjs +99 -0
  619. package/scripts/check-consent.mjs +113 -0
  620. package/scripts/check-content.mjs +149 -0
  621. package/scripts/check-demo.mjs +24 -0
  622. package/scripts/check-themes.mjs +45 -0
  623. package/scripts/check-worker.mjs +569 -0
  624. package/scripts/compare-html.mjs +81 -0
  625. package/scripts/demo-site.mjs +42 -0
  626. package/scripts/editor-icons.mjs +56 -0
  627. package/scripts/editor-roundtrip.mjs +134 -0
  628. package/scripts/editor-screens.mjs +101 -0
  629. package/scripts/git-env.mjs +9 -0
  630. package/scripts/icon-packs.mjs +151 -0
  631. package/scripts/link-counts.mjs +22 -0
  632. package/scripts/page-colors.mjs +42 -0
  633. package/scripts/releases.d.mts +10 -0
  634. package/scripts/releases.mjs +80 -0
  635. package/scripts/responsive-images.mjs +94 -0
  636. package/scripts/scheduled-list.mjs +15 -0
  637. package/scripts/theme-screenshots.mjs +88 -0
  638. package/scripts/types.mjs +21 -0
  639. package/scripts/video-thumbnails.mjs +89 -0
  640. package/scripts/worker-modules.mjs +34 -0
  641. package/site/appCookies.d.mts +9 -0
  642. package/site/appCookies.mjs +74 -0
  643. package/site/apps.ts +21 -0
  644. package/site/autolinks.d.mts +11 -0
  645. package/site/autolinks.mjs +200 -0
  646. package/site/blockScripts.ts +31 -0
  647. package/site/categories.ts +16 -0
  648. package/site/consent.ts +26 -0
  649. package/site/content.config.ts +84 -0
  650. package/site/content.ts +104 -0
  651. package/site/cookieList.ts +17 -0
  652. package/site/defineSite.d.mts +142 -0
  653. package/site/defineSite.mjs +136 -0
  654. package/site/directives/interact.d.mts +2 -0
  655. package/site/directives/interact.mjs +31 -0
  656. package/site/editor-version.d.mts +1 -0
  657. package/site/editor-version.mjs +26 -0
  658. package/site/fill.ts +13 -0
  659. package/site/format.ts +11 -0
  660. package/site/home.ts +66 -0
  661. package/site/iconPacks.d.mts +20 -0
  662. package/site/iconPacks.mjs +43 -0
  663. package/site/icons.d.mts +693 -0
  664. package/site/icons.mjs +605 -0
  665. package/site/images.d.mts +43 -0
  666. package/site/images.mjs +157 -0
  667. package/site/motion.ts +13 -0
  668. package/site/noindex.d.mts +6 -0
  669. package/site/noindex.mjs +21 -0
  670. package/site/pageColors.ts +110 -0
  671. package/site/popups.ts +98 -0
  672. package/site/schema.ts +189 -0
  673. package/site/sections.ts +3 -0
  674. package/site/seo.ts +28 -0
  675. package/site/site.ts +76 -0
  676. package/site/siteConfig.d.mts +1 -0
  677. package/site/siteConfig.mjs +8 -0
  678. package/site/siteCsp.d.mts +10 -0
  679. package/site/siteCsp.mjs +75 -0
  680. package/site/siteModules.d.mts +7 -0
  681. package/site/siteModules.mjs +27 -0
  682. package/site/standardPages.ts +59 -0
  683. package/site/testimonials.ts +48 -0
  684. package/site/text.ts +43 -0
  685. package/site/theme.d.mts +83 -0
  686. package/site/theme.mjs +472 -0
  687. package/site/tracking.client.ts +225 -0
  688. package/site/tracking.ts +9 -0
  689. package/src/ActionParts.tsx +205 -0
  690. package/src/Advanced.tsx +152 -0
  691. package/src/App.tsx +904 -0
  692. package/src/AppList.tsx +1345 -0
  693. package/src/BlockDialog.tsx +360 -0
  694. package/src/BlockParts.tsx +652 -0
  695. package/src/BlockSettings.tsx +533 -0
  696. package/src/BrandForm.tsx +164 -0
  697. package/src/Colors.tsx +476 -0
  698. package/src/CommunicationParts.tsx +199 -0
  699. package/src/CompanyForm.tsx +209 -0
  700. package/src/Consents.tsx +175 -0
  701. package/src/ContainmentParts.tsx +185 -0
  702. package/src/Crop.tsx +152 -0
  703. package/src/DataForm.tsx +434 -0
  704. package/src/Deleted.tsx +177 -0
  705. package/src/EntryForm.tsx +740 -0
  706. package/src/FileView.tsx +30 -0
  707. package/src/FormFrame.tsx +237 -0
  708. package/src/Keywords.tsx +225 -0
  709. package/src/Library.tsx +1563 -0
  710. package/src/Licenses.tsx +336 -0
  711. package/src/LinkSheet.tsx +138 -0
  712. package/src/MarkdownEditor.tsx +1075 -0
  713. package/src/Media.tsx +677 -0
  714. package/src/NavigationParts.tsx +350 -0
  715. package/src/NavigationRestParts.tsx +247 -0
  716. package/src/Notifications.tsx +59 -0
  717. package/src/PagePicker.tsx +136 -0
  718. package/src/Parts.tsx +991 -0
  719. package/src/PartsShown.tsx +168 -0
  720. package/src/PickerParts.tsx +183 -0
  721. package/src/Redirects.tsx +450 -0
  722. package/src/Screen.tsx +184 -0
  723. package/src/SectionsField.tsx +65 -0
  724. package/src/SelectionParts.tsx +134 -0
  725. package/src/SeoScores.tsx +184 -0
  726. package/src/Sheet.tsx +35 -0
  727. package/src/ThemeStore.tsx +376 -0
  728. package/src/Tokens.tsx +346 -0
  729. package/src/Versions.tsx +214 -0
  730. package/src/addresses.ts +75 -0
  731. package/src/allIcons.ts +7 -0
  732. package/src/appFiles.ts +29 -0
  733. package/src/apps.ts +281 -0
  734. package/src/ask.tsx +123 -0
  735. package/src/blocks.ts +251 -0
  736. package/src/brand.ts +153 -0
  737. package/src/cache.ts +42 -0
  738. package/src/check.ts +285 -0
  739. package/src/checkSite.ts +256 -0
  740. package/src/consents.ts +26 -0
  741. package/src/cookieList.ts +70 -0
  742. package/src/countries.ts +59 -0
  743. package/src/deploy.ts +155 -0
  744. package/src/editor.css +602 -0
  745. package/src/examples.ts +85 -0
  746. package/src/fields.tsx +942 -0
  747. package/src/frontmatter.d.mts +6 -0
  748. package/src/frontmatter.mjs +42 -0
  749. package/src/github.ts +376 -0
  750. package/src/i18n/da-DK/advanced.ts +28 -0
  751. package/src/i18n/da-DK/app.ts +71 -0
  752. package/src/i18n/da-DK/apps.ts +90 -0
  753. package/src/i18n/da-DK/blockSettings.ts +38 -0
  754. package/src/i18n/da-DK/blocks.ts +408 -0
  755. package/src/i18n/da-DK/check.ts +25 -0
  756. package/src/i18n/da-DK/colors.ts +64 -0
  757. package/src/i18n/da-DK/common.ts +118 -0
  758. package/src/i18n/da-DK/company.ts +24 -0
  759. package/src/i18n/da-DK/consents.ts +23 -0
  760. package/src/i18n/da-DK/forms.ts +116 -0
  761. package/src/i18n/da-DK/history.ts +35 -0
  762. package/src/i18n/da-DK/index.ts +49 -0
  763. package/src/i18n/da-DK/keywords.ts +19 -0
  764. package/src/i18n/da-DK/library.ts +135 -0
  765. package/src/i18n/da-DK/licenses.ts +25 -0
  766. package/src/i18n/da-DK/media.ts +79 -0
  767. package/src/i18n/da-DK/parts.ts +696 -0
  768. package/src/i18n/da-DK/preview.ts +13 -0
  769. package/src/i18n/da-DK/redirects.ts +51 -0
  770. package/src/i18n/da-DK/seo.ts +20 -0
  771. package/src/i18n/da-DK/text.ts +35 -0
  772. package/src/i18n/da-DK/theme.ts +41 -0
  773. package/src/i18n/en-US/advanced.ts +32 -0
  774. package/src/i18n/en-US/app.ts +84 -0
  775. package/src/i18n/en-US/apps.ts +134 -0
  776. package/src/i18n/en-US/blockSettings.ts +48 -0
  777. package/src/i18n/en-US/blocks.ts +441 -0
  778. package/src/i18n/en-US/check.ts +31 -0
  779. package/src/i18n/en-US/colors.ts +82 -0
  780. package/src/i18n/en-US/common.ts +144 -0
  781. package/src/i18n/en-US/company.ts +25 -0
  782. package/src/i18n/en-US/consents.ts +25 -0
  783. package/src/i18n/en-US/forms.ts +145 -0
  784. package/src/i18n/en-US/history.ts +43 -0
  785. package/src/i18n/en-US/index.ts +51 -0
  786. package/src/i18n/en-US/keywords.ts +24 -0
  787. package/src/i18n/en-US/library.ts +176 -0
  788. package/src/i18n/en-US/licenses.ts +36 -0
  789. package/src/i18n/en-US/media.ts +80 -0
  790. package/src/i18n/en-US/parts.ts +718 -0
  791. package/src/i18n/en-US/preview.ts +15 -0
  792. package/src/i18n/en-US/redirects.ts +66 -0
  793. package/src/i18n/en-US/seo.ts +26 -0
  794. package/src/i18n/en-US/text.ts +41 -0
  795. package/src/i18n/en-US/theme.ts +41 -0
  796. package/src/i18n/index.ts +124 -0
  797. package/src/icon.tsx +160 -0
  798. package/src/iconPacks.ts +89 -0
  799. package/src/images.ts +163 -0
  800. package/src/licenses.ts +79 -0
  801. package/src/markdown.d.mts +12 -0
  802. package/src/markdown.mjs +204 -0
  803. package/src/nav.ts +68 -0
  804. package/src/notFound.ts +28 -0
  805. package/src/notifications.ts +223 -0
  806. package/src/pagePreview.tsx +204 -0
  807. package/src/r2Caching.ts +55 -0
  808. package/src/redirects.ts +149 -0
  809. package/src/registry.tsx +188 -0
  810. package/src/schema.ts +417 -0
  811. package/src/sections.d.mts +45 -0
  812. package/src/sections.mjs +303 -0
  813. package/src/seo.ts +333 -0
  814. package/src/shared.ts +44 -0
  815. package/src/sidebar.ts +31 -0
  816. package/src/site.ts +317 -0
  817. package/src/standard/Block.astro +19 -0
  818. package/src/standard/BlockSite.astro +18 -0
  819. package/src/standard/accordion/index.tsx +47 -0
  820. package/src/standard/accordion/load.ts +23 -0
  821. package/src/standard/accordion/shared.ts +15 -0
  822. package/src/standard/accordion/view.ts +32 -0
  823. package/src/standard/buttons/fields.ts +98 -0
  824. package/src/standard/buttons/index.tsx +23 -0
  825. package/src/standard/buttons/load.ts +34 -0
  826. package/src/standard/buttons/shared.ts +12 -0
  827. package/src/standard/buttons/view.ts +30 -0
  828. package/src/standard/cards/index.tsx +71 -0
  829. package/src/standard/cards/load.ts +17 -0
  830. package/src/standard/cards/shared.ts +27 -0
  831. package/src/standard/cards/view.ts +29 -0
  832. package/src/standard/checklist/index.tsx +27 -0
  833. package/src/standard/checklist/load.ts +5 -0
  834. package/src/standard/checklist/view.ts +13 -0
  835. package/src/standard/cookies/index.tsx +17 -0
  836. package/src/standard/cookies/list.ts +56 -0
  837. package/src/standard/cookies/load.ts +11 -0
  838. package/src/standard/cookies/shared.ts +20 -0
  839. package/src/standard/cookies/view.ts +28 -0
  840. package/src/standard/faq/index.tsx +29 -0
  841. package/src/standard/faq/load.ts +14 -0
  842. package/src/standard/faq/view.ts +21 -0
  843. package/src/standard/head.ts +23 -0
  844. package/src/standard/hero/fields.ts +48 -0
  845. package/src/standard/hero/index.tsx +57 -0
  846. package/src/standard/hero/load.ts +23 -0
  847. package/src/standard/hero/view.ts +38 -0
  848. package/src/standard/links/index.tsx +41 -0
  849. package/src/standard/links/load.ts +17 -0
  850. package/src/standard/links/view.ts +25 -0
  851. package/src/standard/list/index.tsx +50 -0
  852. package/src/standard/list/load.ts +5 -0
  853. package/src/standard/list/view.ts +18 -0
  854. package/src/standard/load.ts +7 -0
  855. package/src/standard/notice/index.tsx +30 -0
  856. package/src/standard/notice/load.ts +16 -0
  857. package/src/standard/notice/view.ts +10 -0
  858. package/src/standard/points/index.tsx +42 -0
  859. package/src/standard/points/load.ts +5 -0
  860. package/src/standard/points/view.ts +12 -0
  861. package/src/standard/posts/index.tsx +132 -0
  862. package/src/standard/posts/load.ts +36 -0
  863. package/src/standard/posts/shared.ts +21 -0
  864. package/src/standard/posts/view.ts +63 -0
  865. package/src/standard/pricecards/index.tsx +57 -0
  866. package/src/standard/pricecards/load.ts +11 -0
  867. package/src/standard/pricecards/view.ts +50 -0
  868. package/src/standard/sharedTexts.ts +6 -0
  869. package/src/standard/site.ts +124 -0
  870. package/src/standard/siteLooks.ts +19 -0
  871. package/src/standard/sites.ts +13 -0
  872. package/src/standard/steps/index.tsx +28 -0
  873. package/src/standard/steps/load.ts +5 -0
  874. package/src/standard/steps/view.ts +11 -0
  875. package/src/standard/testimonials/index.tsx +87 -0
  876. package/src/standard/testimonials/list.ts +43 -0
  877. package/src/standard/testimonials/load.ts +58 -0
  878. package/src/standard/testimonials/shared.ts +13 -0
  879. package/src/standard/testimonials/view.ts +76 -0
  880. package/src/standard/themeBlocks.ts +13 -0
  881. package/src/standard/video/index.tsx +23 -0
  882. package/src/standard/video/load.ts +10 -0
  883. package/src/standard/video/view.ts +42 -0
  884. package/src/standard/view.tsx +105 -0
  885. package/src/standard/views.ts +37 -0
  886. package/src/store.ts +499 -0
  887. package/src/theme.ts +90 -0
  888. package/src/themeUi.ts +97 -0
  889. package/src/themes.ts +91 -0
  890. package/src/ui.tsx +3402 -0
  891. package/src/virtual.d.ts +5 -0
  892. package/src/when.ts +22 -0
  893. package/starter/.githooks/pre-push +4 -0
  894. package/starter/.nvmrc +1 -0
  895. package/starter/.prettierignore +11 -0
  896. package/starter/.prettierrc.json +7 -0
  897. package/starter/README.md +26 -0
  898. package/starter/astro.config.mjs +5 -0
  899. package/starter/package.json +67 -0
  900. package/starter/public/_redirects +1 -0
  901. package/starter/public/apple-touch-icon.png +0 -0
  902. package/starter/public/editor/apple-touch-icon.png +0 -0
  903. package/starter/public/editor/icon-192.png +0 -0
  904. package/starter/public/editor/icon-512.png +0 -0
  905. package/starter/public/editor/icon-maskable-512.png +0 -0
  906. package/starter/public/favicon.ico +0 -0
  907. package/starter/public/favicon.svg +1 -0
  908. package/starter/public/logo.svg +1 -0
  909. package/starter/src/content/pages/blog.md +8 -0
  910. package/starter/src/content/pages/forside.md +9 -0
  911. package/starter/src/content/pages/kontakt.md +7 -0
  912. package/starter/src/content/pages/privatlivspolitik.md +9 -0
  913. package/starter/src/content/pages/siden-findes-ikke.md +7 -0
  914. package/starter/src/content.config.ts +2 -0
  915. package/starter/src/data/apps.json +1 -0
  916. package/starter/src/data/block-settings.json +3 -0
  917. package/starter/src/data/blocks.json +1 -0
  918. package/starter/src/data/booking.json +6 -0
  919. package/starter/src/data/colors.json +70 -0
  920. package/starter/src/data/contact.json +8 -0
  921. package/starter/src/data/cookies.json +21 -0
  922. package/starter/src/data/csp.json +1 -0
  923. package/starter/src/data/footer.json +15 -0
  924. package/starter/src/data/internal-links.json +4 -0
  925. package/starter/src/data/menu.json +4 -0
  926. package/starter/src/data/mobile-menu.json +4 -0
  927. package/starter/src/data/old-addresses.json +1 -0
  928. package/starter/src/data/redirects.json +1 -0
  929. package/starter/src/data/standard-pages.json +8 -0
  930. package/starter/src/data/theme.json +1 -0
  931. package/starter/src/env.d.ts +2 -0
  932. package/starter/src/site/config.ts +13 -0
  933. package/starter/src/site/worker.js +14 -0
  934. package/starter/tsconfig.json +17 -0
  935. package/starter/worker/apps.js +6 -0
  936. package/starter/wrangler.jsonc +39 -0
  937. package/theme/LICENSE +21 -0
  938. package/theme/blocks/ArrowLink.tsx +14 -0
  939. package/theme/blocks/BlockHead.tsx +26 -0
  940. package/theme/blocks/Panel.tsx +32 -0
  941. package/theme/blocks/Person.tsx +26 -0
  942. package/theme/blocks/accordion/PageRow.tsx +25 -0
  943. package/theme/blocks/accordion/View.tsx +17 -0
  944. package/theme/blocks/buttons/Buttons.tsx +28 -0
  945. package/theme/blocks/buttons/View.tsx +12 -0
  946. package/theme/blocks/cards/LinkCards.tsx +72 -0
  947. package/theme/blocks/cards/View.tsx +20 -0
  948. package/theme/blocks/checklist/Checks.tsx +15 -0
  949. package/theme/blocks/checklist/View.tsx +14 -0
  950. package/theme/blocks/cookies/View.tsx +36 -0
  951. package/theme/blocks/faq/Accordion.tsx +34 -0
  952. package/theme/blocks/faq/View.tsx +16 -0
  953. package/theme/blocks/faq/script.ts +56 -0
  954. package/theme/blocks/hero/View.tsx +44 -0
  955. package/theme/blocks/links/LinkGroups.tsx +24 -0
  956. package/theme/blocks/links/View.tsx +17 -0
  957. package/theme/blocks/list/Rows.tsx +24 -0
  958. package/theme/blocks/list/View.tsx +14 -0
  959. package/theme/blocks/notice/View.tsx +13 -0
  960. package/theme/blocks/points/PointList.tsx +34 -0
  961. package/theme/blocks/points/View.tsx +14 -0
  962. package/theme/blocks/posts/View.tsx +26 -0
  963. package/theme/blocks/pricecards/View.tsx +40 -0
  964. package/theme/blocks/steps/StepList.tsx +23 -0
  965. package/theme/blocks/steps/View.tsx +14 -0
  966. package/theme/blocks/testimonials/List.tsx +46 -0
  967. package/theme/blocks/testimonials/Slider.tsx +44 -0
  968. package/theme/blocks/testimonials/Testimonial.tsx +124 -0
  969. package/theme/blocks/testimonials/View.tsx +21 -0
  970. package/theme/blocks/testimonials/script.ts +52 -0
  971. package/theme/blocks/video/View.tsx +41 -0
  972. package/theme/components/ActionBar.astro +63 -0
  973. package/theme/components/AppSlot.astro +21 -0
  974. package/theme/components/ArrowLink.tsx +21 -0
  975. package/theme/components/BlogIndex.astro +48 -0
  976. package/theme/components/BlogPosts.astro +18 -0
  977. package/theme/components/BlogPosts.tsx +45 -0
  978. package/theme/components/BookButton.tsx +22 -0
  979. package/theme/components/Container.astro +11 -0
  980. package/theme/components/CookieConsent.astro +121 -0
  981. package/theme/components/EndBlock.astro +21 -0
  982. package/theme/components/FontPreload.astro +6 -0
  983. package/theme/components/Footer.astro +85 -0
  984. package/theme/components/Header.astro +186 -0
  985. package/theme/components/Icon.astro +16 -0
  986. package/theme/components/Img.astro +29 -0
  987. package/theme/components/Img.tsx +33 -0
  988. package/theme/components/Inline.astro +11 -0
  989. package/theme/components/Logo.astro +23 -0
  990. package/theme/components/LogoMark.astro +10 -0
  991. package/theme/components/Markdown.astro +17 -0
  992. package/theme/components/MobileMenu.tsx +224 -0
  993. package/theme/components/MobileNav.astro +33 -0
  994. package/theme/components/PageHeader.astro +22 -0
  995. package/theme/components/PageSection.astro +24 -0
  996. package/theme/components/PhoneButton.tsx +11 -0
  997. package/theme/components/Popup.astro +145 -0
  998. package/theme/components/PopupButton.tsx +24 -0
  999. package/theme/components/PopupView.tsx +48 -0
  1000. package/theme/components/PostList.tsx +58 -0
  1001. package/theme/components/PostMeta.tsx +17 -0
  1002. package/theme/components/RichText.astro +28 -0
  1003. package/theme/components/Section.astro +29 -0
  1004. package/theme/components/SectionHeading.tsx +8 -0
  1005. package/theme/components/TextSection.astro +16 -0
  1006. package/theme/components/home/PageBlock.astro +19 -0
  1007. package/theme/components/home/Text.astro +13 -0
  1008. package/theme/components/loadPopup.ts +22 -0
  1009. package/theme/components/loadPosts.ts +16 -0
  1010. package/theme/components/posts.ts +15 -0
  1011. package/theme/editor/looks.mjs +7 -0
  1012. package/theme/editor/previews.tsx +56 -0
  1013. package/theme/layouts/Base.astro +273 -0
  1014. package/theme/layouts/PageLayout.astro +126 -0
  1015. package/theme/layouts/PostLayout.astro +137 -0
  1016. package/theme/screenshots/1-front.jpg +0 -0
  1017. package/theme/screenshots/2-front-dark.jpg +0 -0
  1018. package/theme/screenshots/3-blog.jpg +0 -0
  1019. package/theme/screenshots/4-contact.jpg +0 -0
  1020. package/theme/scripts/motion.ts +29 -0
  1021. package/theme/styles/base.css +320 -0
  1022. package/theme/styles/blocks.css +1808 -0
  1023. package/theme/styles/components.css +206 -0
  1024. package/theme/styles/global.css +1302 -0
  1025. package/theme/styles/ui.css +6544 -0
  1026. package/theme/templates/Blog.astro +38 -0
  1027. package/theme/templates/Category.astro +19 -0
  1028. package/theme/templates/Home.astro +36 -0
  1029. package/theme/templates/New.astro +68 -0
  1030. package/theme/templates/NotFound.astro +117 -0
  1031. package/theme/theme.json +28 -0
  1032. package/theme/ui/Badge.tsx +45 -0
  1033. package/theme/ui/BottomSheet.tsx +148 -0
  1034. package/theme/ui/Button.tsx +79 -0
  1035. package/theme/ui/ButtonGroup.tsx +31 -0
  1036. package/theme/ui/Card.tsx +45 -0
  1037. package/theme/ui/Carousel.tsx +124 -0
  1038. package/theme/ui/Checkbox.tsx +59 -0
  1039. package/theme/ui/Chip.tsx +101 -0
  1040. package/theme/ui/ChipSet.tsx +27 -0
  1041. package/theme/ui/DatePicker.tsx +149 -0
  1042. package/theme/ui/DatePickerContainer.tsx +316 -0
  1043. package/theme/ui/Dialog.tsx +100 -0
  1044. package/theme/ui/Divider.tsx +27 -0
  1045. package/theme/ui/ExposedDropdownMenu.tsx +77 -0
  1046. package/theme/ui/Fab.tsx +88 -0
  1047. package/theme/ui/FabMenu.tsx +67 -0
  1048. package/theme/ui/IconButton.tsx +78 -0
  1049. package/theme/ui/List.tsx +287 -0
  1050. package/theme/ui/LoadingIndicator.tsx +29 -0
  1051. package/theme/ui/Menu.tsx +278 -0
  1052. package/theme/ui/NavigationBar.tsx +62 -0
  1053. package/theme/ui/NavigationDrawer.tsx +80 -0
  1054. package/theme/ui/NavigationRail.tsx +154 -0
  1055. package/theme/ui/ProgressIndicator.tsx +162 -0
  1056. package/theme/ui/RadioButton.tsx +50 -0
  1057. package/theme/ui/Search.tsx +129 -0
  1058. package/theme/ui/SegmentedButton.tsx +49 -0
  1059. package/theme/ui/SideSheet.tsx +54 -0
  1060. package/theme/ui/Slider.tsx +155 -0
  1061. package/theme/ui/Snackbar.tsx +67 -0
  1062. package/theme/ui/SplitButton.tsx +67 -0
  1063. package/theme/ui/Switch.tsx +60 -0
  1064. package/theme/ui/Tabs.tsx +114 -0
  1065. package/theme/ui/TextField.tsx +278 -0
  1066. package/theme/ui/TimePicker.tsx +128 -0
  1067. package/theme/ui/TimePickerContainer.tsx +234 -0
  1068. package/theme/ui/Toolbar.tsx +67 -0
  1069. package/theme/ui/Tooltip.tsx +58 -0
  1070. package/theme/ui/TopAppBar.tsx +77 -0
  1071. package/theme/ui/bottomSheet.script.ts +8 -0
  1072. package/theme/ui/bottomSheet.ts +211 -0
  1073. package/theme/ui/carousel.script.ts +28 -0
  1074. package/theme/ui/carouselLayout.ts +229 -0
  1075. package/theme/ui/checkbox.script.ts +8 -0
  1076. package/theme/ui/datePicker.script.ts +302 -0
  1077. package/theme/ui/dates.ts +185 -0
  1078. package/theme/ui/dialog.ts +48 -0
  1079. package/theme/ui/dropdownMenu.script.ts +166 -0
  1080. package/theme/ui/fabMenu.script.ts +57 -0
  1081. package/theme/ui/navigationRail.script.ts +47 -0
  1082. package/theme/ui/ripple.script.ts +159 -0
  1083. package/theme/ui/search.script.ts +92 -0
  1084. package/theme/ui/sideSheet.ts +48 -0
  1085. package/theme/ui/slider.script.ts +12 -0
  1086. package/theme/ui/sliderUpdate.ts +52 -0
  1087. package/theme/ui/snackbar.script.ts +16 -0
  1088. package/theme/ui/splitButton.script.ts +63 -0
  1089. package/theme/ui/tabs.script.ts +48 -0
  1090. package/theme/ui/textField.script.ts +17 -0
  1091. package/theme/ui/textFieldUpdate.ts +44 -0
  1092. package/theme/ui/timePicker.script.ts +248 -0
  1093. package/theme/ui/toolbar.script.ts +43 -0
  1094. package/theme/ui/tooltip.script.ts +68 -0
  1095. package/worker/consent.js +141 -0
  1096. package/worker/editor-github.js +167 -0
  1097. package/worker/editor-host.js +74 -0
  1098. package/worker/files.js +401 -0
  1099. package/worker/github.js +36 -0
  1100. package/worker/index.js +184 -0
  1101. package/worker/not-found.js +97 -0
  1102. package/worker/oauth.js +95 -0
  1103. package/worker/publish.js +52 -0
  1104. package/worker/push.js +270 -0
  1105. package/worker/r2-status.js +50 -0
  1106. package/worker/session.js +58 -0
@@ -0,0 +1,1563 @@
1
+ // The start screen of the editor (the parts of the site in groups, like a phone's settings) and the
2
+ // list of each part: blog posts newest first, the rest by name, prices and testimonials in their order
3
+ // on the site (with arrows to move them), and a search. Lists that are not in an order of their own can
4
+ // be sorted another way (a list of kinds, by its kinds too); the choice is kept in this browser. Pages
5
+ // and posts can be filtered by their status and by what they pick (a post's tags), a list of what is
6
+ // shown on pages (Promovering) by a page, its kind and whether it is on, as "Filer" (Filters). In a list that can be deleted from, the clock in the top bar ("Slettede …") opens
7
+ // what was deleted (Deleted.tsx), as in "Filer". The tabs the apps installed add to a list (AppDefinition.tabs, Cookiescanner's
8
+ // "Problemer" on "Cookies") are in its top app bar after the list's own, each at #liste/<list>/<app>.
9
+ import { Fragment, useEffect, useMemo, useState, type ReactNode } from 'react';
10
+ import {
11
+ ArrowDown,
12
+ ArrowsDownUp,
13
+ ArrowUp,
14
+ Check,
15
+ ClockCounterClockwise,
16
+ CircleHalf,
17
+ FileText,
18
+ Gauge,
19
+ GithubLogo,
20
+ GoogleLogo,
21
+ List as ListIcon,
22
+ ListBullets,
23
+ Moon,
24
+ Palette,
25
+ Plus,
26
+ Ruler,
27
+ Shapes,
28
+ SignOut,
29
+ Sun,
30
+ type Icon,
31
+ } from '@phosphor-icons/react';
32
+ import { byText, entryData, type Collection, type Part, type PartScreen } from './schema';
33
+ import { config, countsOf, header, relationsOf, siteName } from '@site/editor';
34
+ import { iconPack, iconPacks } from './iconPacks';
35
+ import { theme, themes, type ThemeInfo } from './themes';
36
+ import {
37
+ appearance,
38
+ backFrom,
39
+ blockGroups,
40
+ blockParts,
41
+ collections,
42
+ companyParts,
43
+ groups,
44
+ icons,
45
+ lists,
46
+ listsShown,
47
+ partScreens,
48
+ seoParts,
49
+ settings,
50
+ } from './site';
51
+ import { readFields, splitFrontmatter } from './frontmatter.mjs';
52
+ import { ConflictError, type RepoFile, type Viewer } from './github';
53
+ import Screen from './Screen';
54
+ import {
55
+ ImageCarousel,
56
+ InputChip,
57
+ Choice,
58
+ Filters,
59
+ ChoiceListItem,
60
+ Group,
61
+ IconButton,
62
+ LeadingIcon,
63
+ NewChoice,
64
+ SearchRow,
65
+ useKept,
66
+ SegmentedButton,
67
+ UnfoldButton,
68
+ GroupTitle,
69
+ SwitchRow,
70
+ IconLink,
71
+ ScoreRing,
72
+ scoreOf,
73
+ Button,
74
+ EmptyNote,
75
+ ChoiceRow,
76
+ ColorSwatches,
77
+ IconSwatches,
78
+ ListItem,
79
+ } from './ui';
80
+ import { openAtSeo } from './EntryForm';
81
+ import { isRated } from './SeoScores';
82
+ import { fileRating } from './seo';
83
+ import { dateName, headed, letterOf } from './Sheet';
84
+ import type { Env } from './App';
85
+ import { kindAndName, savedText } from './deploy';
86
+ import { when } from './when';
87
+ import { untilTime } from '../lib/notice.mjs';
88
+ import { savedTheme, setTheme, shownMode, type Theme } from './theme';
89
+ import { browserLocale, locale, localeName, locales, savedLocale, setLocale, siteLocale, siteT, siteText, t, type Locale } from './i18n';
90
+ import { appType, EDITOR_TYPES, setDevice, setOn, useNotificationChoices } from './notifications';
91
+ import { snackbar } from '../lib/snackbar';
92
+ import { ask } from './ask';
93
+ import { pagesWith } from './BlockSettings';
94
+ import { filesOf, mediaOf } from './Media';
95
+ import { restorable } from './Deleted';
96
+ import { installedApps, kindsOffered, tabsOf } from './apps';
97
+ import { appPartsIn, DeveloperRow } from './AppList';
98
+ import { kindLists } from './BlockParts';
99
+ import {
100
+ chosenPack,
101
+ chosenTheme,
102
+ ThemePicture,
103
+ ThemeStoreButtons,
104
+ themeInstalled,
105
+ themePending,
106
+ themeUpdateOf,
107
+ useThemeCatalog,
108
+ } from './ThemeStore';
109
+ import { STANDARD_THEME, STORE_ROUTE } from './store';
110
+ import { MOTION_SCHEMES, schemeInUse, type MotionScheme } from '../lib/motion';
111
+ import { applyMotion } from './theme';
112
+ import { colorsOf } from './Colors';
113
+ import { OwnerLicenses } from './Licenses';
114
+ import { apps } from './site';
115
+ import { offBlocks } from './registry';
116
+
117
+ // Where a part opens: its list, or the file itself when it is one file (the menu).
118
+ const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
119
+
120
+ const countOf = (c: Collection, files: RepoFile[]): number =>
121
+ c.kind === 'files' ? c.files.length : files.filter((f) => f.path.startsWith(`${c.folder}/`)).length;
122
+
123
+ // A part's name, where it opens, and how many entries it has (null for one file).
124
+ export function partOf(id: string, files: RepoFile[]): { label: string; href: string; count: number | null } {
125
+ const { parts } = t.common;
126
+ if (id === 'virksomheden') return { label: parts.company, href: '#virksomheden', count: null };
127
+ if (id === 'header') return { label: parts.header, href: '#header', count: null };
128
+ if (id === 'lister') return { label: parts.lists, href: '#lister', count: listsShown(files).length };
129
+ if (id === 'blokke') return { label: parts.blocks, href: '#blokke', count: blockParts.length };
130
+ if (id === 'filer') return { label: parts.media, href: '#filer', count: mediaOf(files).length + filesOf(files).length };
131
+ if (id === 'udseende') return { label: parts.appearance, href: '#udseende', count: null };
132
+ if (id === 'blokvalg') return { label: parts.blockSettings, href: '#blokvalg', count: null };
133
+ if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
134
+ if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
135
+ if (id === 'avanceret') return { label: parts.advanced, href: '#avanceret', count: null };
136
+ if (id === 'licenser') return { label: parts.licenses, href: '#licenser', count: null };
137
+ const screen = partScreens.find((s) => s.id === id);
138
+ if (screen) return { label: screen.label, href: `#${id}`, count: null };
139
+ if (id === 'seo') return { label: parts.seo, href: '#seo', count: null };
140
+ const c = collections.find((x) => x.id === id)!;
141
+ return { label: c.label, href: hrefOf(c), count: c.kind === 'files' && c.files.length === 1 ? null : countOf(c, files) };
142
+ }
143
+
144
+ /** The parts of a group on the start screen (and in the sidebar), A–Å by their names in the editor's language. */
145
+ export const partsIn = (ids: string[], files: RepoFile[]) =>
146
+ ids.map((id) => ({ id, ...partOf(id, files) })).sort((a, b) => byText(a.label, b.label));
147
+
148
+ export { LeadingIcon };
149
+
150
+ // A part of the site as a row: its icon, its name and how many entries it has.
151
+ function PartRow({ id, label, href, count }: { id: string } & ReturnType<typeof partOf>) {
152
+ return <ListItem href={href} icon={icons[id]} headline={label} count={count} arrow />;
153
+ }
154
+
155
+ // The start screen, with when the site was last updated (built on Cloudflare, App.tsx) under the title;
156
+ // on a computer the groups two side by side.
157
+ export function Start({ files, builtAt }: { files: RepoFile[]; builtAt: Date | null }) {
158
+ return (
159
+ <Screen
160
+ title={siteName}
161
+ wide
162
+ status={builtAt ? t.library.siteUpdated(when(builtAt, { short: true })) : undefined}
163
+ // The notifications, the settings and the account in the top bar, on a phone too (Screen.tsx).
164
+ home
165
+ >
166
+ <div className="md:grid md:grid-cols-2 md:items-start md:gap-x-6">
167
+ {groups.map((g) => (
168
+ <Group key={g.title} title={g.title}>
169
+ {partsIn(g.ids, files).map((p) => (
170
+ <PartRow key={p.id} {...p} />
171
+ ))}
172
+ </Group>
173
+ ))}
174
+ </div>
175
+ </Screen>
176
+ );
177
+ }
178
+
179
+ /** "Indstillinger" (#indstillinger), which the gear in the top bar opens (Screen.tsx SettingsLink): the settings of the
180
+ * whole site (settings in packages/editor/defaults/editor.ts: "Analytics", "Apps", "Privatliv", "Promovering", "Søgemaskineoptimering",
181
+ * "Udseende"), A–Å as a group of the start screen, each going back here (backFrom). */
182
+ export function Settings({ files }: { files: RepoFile[] }) {
183
+ return (
184
+ <Screen title={t.common.parts.settings} wide back={{ href: '#', label: siteName }}>
185
+ <Group>
186
+ {partsIn(settings, files).map((p) => (
187
+ <PartRow key={p.id} {...p} />
188
+ ))}
189
+ </Group>
190
+ </Screen>
191
+ );
192
+ }
193
+
194
+ // "Virksomheden": its parts (CompanyForm.tsx), each on a screen of its own; or one of them with parts
195
+ // of its own ("Betaling"), and the settings of the apps installed of its kinds (Part.apps: MobilePay), going back to
196
+ // "Virksomheden".
197
+ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
198
+ if (part?.parts)
199
+ return (
200
+ <Parts
201
+ title={part.label}
202
+ route={`virksomheden/${part.id}`}
203
+ parts={[...part.parts, ...appPartsIn(part, files)]}
204
+ back={{ href: '#virksomheden', label: t.common.parts.company }}
205
+ />
206
+ );
207
+ return <Parts title={t.common.parts.company} route="virksomheden" parts={companyParts} />;
208
+ }
209
+
210
+ // "Udseende": first "Temaer" (Themes), with the theme the site is built with at the end of its row (or, after a swap,
211
+ // until the site is built with the new one, that one), "Ikonpakker" (IconPacks), with the pack chosen, and "Bevægelse"
212
+ // (MotionSchemes), with the scheme chosen; then how the
213
+ // site looks, the colours (Colors.tsx), the logo and
214
+ // icon (BrandForm.tsx) and the site's own parts there (appearance in packages/editor/defaults/editor.ts: "Standardsider", a screen of its
215
+ // own, CompanyForm.tsx, going back here), and how the editor and the blocks look, their parts ("Dele", PartsHome), each on a
216
+ // screen of its own, with the icons the site gives them.
217
+ // The blocks that can be put in are under "Blokke" ("Blokindstillinger", BlockSettings.tsx).
218
+ export function Theme({ files }: { files: RepoFile[] }) {
219
+ const chosen = themes.find((x) => x.id === chosenTheme(files)) ?? theme;
220
+ const parts: Part[] = [
221
+ { id: 'temaer', label: t.common.parts.themes, icon: icons.temaer, detail: chosen.name },
222
+ {
223
+ id: 'ikonpakker',
224
+ label: t.common.parts.iconPacks,
225
+ icon: icons.ikonpakker,
226
+ detail: iconPacks.find((x) => x.id === chosenPack(files))?.name,
227
+ },
228
+ { id: 'bevaegelse', label: t.common.parts.motion, icon: icons.bevaegelse, detail: t.library.motionSchemes[motionOf(files)] },
229
+ { id: 'farver', label: t.common.parts.colors, icon: icons.farver, trailing: <ThemeColors files={files} /> },
230
+ { id: 'logo', label: t.common.parts.brand, icon: icons.logo },
231
+ ...appearance,
232
+ { id: 'dele', label: t.common.parts.editorParts, icon: icons.dele },
233
+ ];
234
+ return <Parts title={t.common.parts.appearance} route="udseende" parts={parts} back={backFrom('udseende')} />;
235
+ }
236
+
237
+ /** By "Farver": the site's primary, secondary and tertiary colours (accent, secondary, tertiary) in the mode the editor
238
+ * is shown in, as the site has them. */
239
+ function ThemeColors({ files }: { files: RepoFile[] }) {
240
+ const mode = shownMode();
241
+ const colors = colorsOf(files)[mode];
242
+ return (
243
+ <ColorSwatches colors={['accent', 'secondary', 'tertiary'].map((n) => colors[n])} label={t.colors.pages.swatches(t.colors[mode])} />
244
+ );
245
+ }
246
+
247
+ const backToAppearance = { href: '#udseende', label: t.common.parts.appearance };
248
+ const backToThemes = { href: '#udseende/temaer', label: t.common.parts.themes };
249
+
250
+ /** Under a theme's name: whether the site is built with it ("I brug") or is changing to it ("Skifter hertil"). */
251
+ function themeStatus(id: string, files: RepoFile[]): string | undefined {
252
+ if (id === theme.id) return t.library.themeInUse;
253
+ if (id === chosenTheme(files)) return t.library.themeNext;
254
+ }
255
+
256
+ /** "Temaer" (#udseende/temaer): the themes installed (src/themes/, packages/editor/src/themes.ts; and one from the store the site's build
257
+ * does not have yet, as "Installeres …"), by name, each with its picture, its version and who made it, and whether it is
258
+ * in use or the store has a newer version, opening its page (ThemePage). More are in "Appstore", under "Indstillinger". */
259
+ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
260
+ const catalog = useThemeCatalog(env);
261
+ const store = Array.isArray(catalog) ? catalog : [];
262
+ const rows = [...themes.filter((x) => themeInstalled(x.id)), ...store.filter((x) => themePending(x.id))].sort((a, b) =>
263
+ byText(a.name, b.name),
264
+ );
265
+ return (
266
+ <Screen title={t.common.parts.themes} wide back={backToAppearance}>
267
+ <Group>
268
+ {rows.map((x) => {
269
+ const installed = themes.find((y) => y.id === x.id);
270
+ const status = !installed
271
+ ? t.apps.pending
272
+ : (themeStatus(x.id, files) ?? (themeUpdateOf(installed, catalog) ? t.apps.updateFound : undefined));
273
+ return (
274
+ <ListItem
275
+ key={x.id}
276
+ href={`#udseende/temaer/${x.id}`}
277
+ leading={<ThemePicture src={x.icon} />}
278
+ headline={x.name}
279
+ supporting={t.library.themeByline(x.version, x.author)}
280
+ truncate
281
+ count={status}
282
+ arrow
283
+ />
284
+ );
285
+ })}
286
+ </Group>
287
+ </Screen>
288
+ );
289
+ }
290
+
291
+ /** "Ikonpakker" (#udseende/ikonpakker): the icon packs installed (the system's own, packages/editor/icons/, and those the themes
292
+ * installed bring; iconPacks.ts), by name, each with who made it, its licence, the theme it came with and a few of its
293
+ * icons as they are, the one chosen ticked (chosenPack), with "I brug" by the one the site and the editor are built with
294
+ * and "Skifter hertil …" by the one chosen until they are. A tap on another asks first and then saves it in the site's
295
+ * file (config.icons) at once, in its own commit, as a theme is; the site and the editor have its icons after the
296
+ * next build (themePlugin in packages/editor/site/theme.mjs). */
297
+ export function IconPacks({ env, files }: { env: Env; files: RepoFile[] }) {
298
+ const [saving, setSaving] = useState(false);
299
+ const chosen = chosenPack(files);
300
+ const shown = iconPacks.filter((x) => !x.theme || themeInstalled(x.theme));
301
+
302
+ async function choose(id: string, name: string) {
303
+ if (saving || id === chosen) return;
304
+ if (!(await ask(t.library.changeIconPackAsk(name), t.library.changeIconPack))) return;
305
+ const path = config.icons;
306
+ const file = files.find((f) => f.path === path);
307
+ const text = `${JSON.stringify({ pack: id }, null, 2)}\n`;
308
+ setSaving(true);
309
+ env.notify({ text: t.common.saving });
310
+ try {
311
+ const change = t.app.changes.chosen(kindAndName(t.app.changes.iconPack, name));
312
+ const oids = await env.commit(siteT.library.iconPackCommit(name), [{ path, content: text, oid: file?.oid || null }], change);
313
+ env.onSaved({ path, oid: oids[path], text });
314
+ env.notify({ text: t.library.iconPackSaved(capital(change)), kind: 'saved' });
315
+ } catch (e) {
316
+ env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
317
+ } finally {
318
+ setSaving(false);
319
+ }
320
+ }
321
+
322
+ return (
323
+ <Screen title={t.common.parts.iconPacks} wide back={backToAppearance}>
324
+ <Group>
325
+ {shown.map((x) => {
326
+ const status = x.id === iconPack ? t.library.iconPackInUse : x.id === chosen ? t.library.iconPackNext : undefined;
327
+ const from = x.theme && themes.find((y) => y.id === x.theme)?.name;
328
+ return (
329
+ <ChoiceRow
330
+ key={x.id}
331
+ selected={x.id === chosen}
332
+ trailing={<IconSwatches icons={x.samples} label={t.library.iconPackSamples(x.name)} />}
333
+ onClick={() => choose(x.id, x.name)}
334
+ headline={x.name}
335
+ supporting={
336
+ <>
337
+ {t.library.iconPackByline(x.author, x.license)}
338
+ {from && <span className="block">{t.library.iconPackFrom(from)}</span>}
339
+ {status && <span className="block">{status}</span>}
340
+ </>
341
+ }
342
+ />
343
+ );
344
+ })}
345
+ </Group>
346
+ </Screen>
347
+ );
348
+ }
349
+
350
+ /** How the site moves (lib/motion.ts): the scheme chosen in the site's file (config.motion, `{ "scheme": "<scheme>" }`),
351
+ * as it is saved, else the theme the editor is built with's ("motion" in its theme.json), else the standard theme's. */
352
+ export function motionOf(files: RepoFile[]): MotionScheme {
353
+ let scheme: unknown;
354
+ try {
355
+ scheme = JSON.parse(files.find((f) => f.path === config.motion)?.text ?? '{}').scheme;
356
+ } catch {}
357
+ return schemeInUse(scheme, theme.motion, themes.find((x) => x.id === STANDARD_THEME)?.motion);
358
+ }
359
+
360
+ /** "Bevægelse" (#udseende/bevaegelse): Material 3's motion schemes and our calm one (lib/motion.ts), each with how it
361
+ * moves, the one in use ticked (motionOf) and "Temaets valg" by the theme's own. A tap on another asks first and then
362
+ * saves it in the site's file (config.motion) at once, in its own commit, as an icon pack is; the editor moves by it at
363
+ * once (applyMotion, App.tsx), the site after it is built. */
364
+ export function MotionSchemes({ env, files }: { env: Env; files: RepoFile[] }) {
365
+ const [saving, setSaving] = useState(false);
366
+ const chosen = motionOf(files);
367
+ const ofTheme = schemeInUse(undefined, theme.motion, themes.find((x) => x.id === STANDARD_THEME)?.motion);
368
+
369
+ async function choose(scheme: MotionScheme) {
370
+ if (saving || scheme === chosen) return;
371
+ const name = t.library.motionSchemes[scheme];
372
+ if (!(await ask(t.library.changeMotionAsk(name), t.library.changeMotion))) return;
373
+ const path = config.motion;
374
+ const file = files.find((f) => f.path === path);
375
+ const text = `${JSON.stringify({ scheme }, null, 2)}\n`;
376
+ setSaving(true);
377
+ env.notify({ text: t.common.saving });
378
+ try {
379
+ const change = t.app.changes.chosen(kindAndName(t.app.changes.motion, name));
380
+ const oids = await env.commit(
381
+ siteT.library.motionCommit(siteT.library.motionSchemes[scheme]),
382
+ [{ path, content: text, oid: file?.oid || null }],
383
+ change,
384
+ );
385
+ env.onSaved({ path, oid: oids[path], text });
386
+ applyMotion(scheme);
387
+ env.notify({ text: t.library.motionSaved(capital(change)), kind: 'saved' });
388
+ } catch (e) {
389
+ env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
390
+ } finally {
391
+ setSaving(false);
392
+ }
393
+ }
394
+
395
+ return (
396
+ <Screen title={t.common.parts.motion} wide back={backToAppearance}>
397
+ <Group>
398
+ {MOTION_SCHEMES.map((scheme) => (
399
+ <ChoiceRow
400
+ key={scheme}
401
+ selected={scheme === chosen}
402
+ onClick={() => choose(scheme)}
403
+ headline={t.library.motionSchemes[scheme]}
404
+ supporting={
405
+ <>
406
+ {t.library.motionHints[scheme]}
407
+ {scheme === ofTheme && <span className="block">{t.library.motionOfTheme}</span>}
408
+ </>
409
+ }
410
+ />
411
+ ))}
412
+ </Group>
413
+ </Screen>
414
+ );
415
+ }
416
+
417
+ /** A theme's page (#udseende/temaer/<id>), the same for every theme, made to show what it would make of the site, so
418
+ * it can be chosen or not: its picture, name, version and who made it, and whether it is in use; "Brug temaet", for any
419
+ * other than the one chosen, which asks first and then saves it in the site's file (config.theme) at once, in its own
420
+ * commit, and the site is built with it; the pictures of the site in it (its screenshots/, packages/editor/scripts/theme-screenshots.mjs),
421
+ * each opening a sheet with it whole (ImageCarousel); what it is (its description) and what it gives the site (its highlights), in the
422
+ * editor's language, else the first it has; and what stays as it is. Every theme has every block (what it does not draw
423
+ * itself, the standard theme does), so changing it never leaves a block without its drawing; who made it, a row to their
424
+ * page; and, from the store, "Opdater til …" and "Afinstaller" (ThemeStoreButtons). */
425
+ export function ThemePage({ of, env, files }: { of: ThemeInfo; env: Env; files: RepoFile[] }) {
426
+ const [saving, setSaving] = useState(false);
427
+ const chosen = chosenTheme(files);
428
+ const status = of.id === chosen && of.id !== theme.id ? t.library.themeChanging(of.name) : themeStatus(of.id, files);
429
+ const inLocale = <T,>(x?: Record<string, T>) => x && (x[locale] ?? Object.values(x)[0]);
430
+ const description = inLocale(of.description);
431
+ const highlights = inLocale(of.highlights) ?? [];
432
+ const shotName = (key: string) => t.library.themeShots[key] ?? of.name;
433
+ const kept: [Icon, string][] = [
434
+ [FileText, t.library.themeKeptContent],
435
+ [Palette, t.library.themeKeptBrand],
436
+ [Shapes, t.library.themeKeptBlocks],
437
+ ];
438
+
439
+ async function use() {
440
+ if (!(await ask(t.library.changeThemeAsk(of.name), t.library.changeTheme))) return;
441
+ const path = config.theme;
442
+ const file = files.find((f) => f.path === path);
443
+ const text = `${JSON.stringify({ theme: of.id }, null, 2)}\n`;
444
+ setSaving(true);
445
+ env.notify({ text: t.common.saving });
446
+ try {
447
+ const change = t.app.changes.chosen(kindAndName(t.app.changes.theme, of.name));
448
+ const oids = await env.commit(siteT.library.themeCommit(of.name), [{ path, content: text, oid: file?.oid || null }], change);
449
+ env.onSaved({ path, oid: oids[path], text });
450
+ env.notify({ text: savedText(change), kind: 'saved' });
451
+ } catch (e) {
452
+ env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
453
+ } finally {
454
+ setSaving(false);
455
+ }
456
+ }
457
+
458
+ return (
459
+ <Screen title={of.name} wide back={backToThemes}>
460
+ <div className="mb-8 flex items-center gap-4 px-4">
461
+ <img src={of.icon} alt="" className="size-16 shrink-0 rounded-2xl" />
462
+ <span className="min-w-0 flex-1">
463
+ <span className="block truncate font-medium">{of.name}</span>
464
+ <span className="block truncate text-sm text-muted">{t.library.themeByline(of.version, of.author)}</span>
465
+ {status && <span className="block text-sm text-muted">{status}</span>}
466
+ </span>
467
+ </div>
468
+ {of.id !== chosen && themeInstalled(of.id) && (
469
+ <div className="mb-8">
470
+ <Button wide disabled={saving} onClick={use}>
471
+ {t.library.useTheme}
472
+ </Button>
473
+ </div>
474
+ )}
475
+ <ThemeStoreButtons of={of} env={env} files={files} top />
476
+ {of.screenshots.length > 0 && (
477
+ <section className="mb-8">
478
+ <GroupTitle>{t.library.themeLooks}</GroupTitle>
479
+ <ImageCarousel label={t.library.themeLooks} images={of.screenshots.map((x) => ({ src: x.src, label: shotName(x.key) }))} />
480
+ </section>
481
+ )}
482
+ {description && (
483
+ <section className="mb-8">
484
+ <GroupTitle>{t.library.themeAbout}</GroupTitle>
485
+ <p className="px-4">{description}</p>
486
+ </section>
487
+ )}
488
+ {highlights.length > 0 && (
489
+ <Group title={t.library.themeHighlights}>
490
+ {highlights.map((h) => (
491
+ <ListItem key={h} icon={Check} headline={h} />
492
+ ))}
493
+ </Group>
494
+ )}
495
+ <Group title={t.library.themeKept}>
496
+ {kept.map(([glyph, text]) => (
497
+ <ListItem key={text} icon={glyph} headline={text} />
498
+ ))}
499
+ </Group>
500
+ <OwnerLicenses id={`tema/${of.id}`} name={of.name} author={of.author} version={of.version} declared={of} />
501
+ <DeveloperRow name={of.author} />
502
+ <ThemeStoreButtons of={of} env={env} files={files} />
503
+ </Screen>
504
+ );
505
+ }
506
+
507
+ /** "Dele" (#udseende/dele): the parts the editor and the site are built of, each on a screen of its own: the editor's
508
+ * (Parts.tsx), the theme's UI components, the blocks (the standard ones and the apps') and each list of kinds
509
+ * ("Promovering", its own kinds and the apps'), as the theme shows them (BlockParts.tsx), and the theme's design tokens ("Værdier", Tokens.tsx). */
510
+ export function PartsHome() {
511
+ const parts: Part[] = [
512
+ { id: 'editor', label: t.parts.screens.editor, icon: icons.dele },
513
+ { id: 'ui', label: t.parts.screens.components, icon: icons.udseende },
514
+ { id: 'blokke', label: t.parts.screens.blocks, icon: icons.blokke },
515
+ ...kindLists().map((c) => ({ id: `liste/${c.id}`, label: c.label, icon: icons[c.id] ?? icons.lister })),
516
+ { id: 'vaerdier', label: t.parts.screens.values, icon: Ruler },
517
+ ];
518
+ return <Parts title={t.common.parts.editorParts} route="udseende/dele" parts={parts} back={backToAppearance} />;
519
+ }
520
+
521
+ /** "Søgemaskineoptimering" (#seo) under Indstillinger: "SEO-vurdering" (SeoScores.tsx, #seo/vurdering), "Videresendelser"
522
+ * (Redirects.tsx, #seo/videresendelser) and then the site's own parts (seoParts: "Interne links"), each on a screen of
523
+ * its own (CompanyForm.tsx). */
524
+ export function Seo() {
525
+ const parts: Part[] = [
526
+ { id: 'vurdering', label: t.seo.title, icon: Gauge },
527
+ { id: 'videresendelser', label: t.common.parts.redirects, icon: icons.videresendelser },
528
+ ...seoParts,
529
+ ];
530
+ return <Parts title={t.common.parts.seo} route="seo" parts={parts} back={backFrom('seo')} />;
531
+ }
532
+
533
+ // A screen with parts of the site's own (partScreens): "Privatliv"; and with the settings of the apps installed with its
534
+ // category (PartScreen.apps): "Analytics" and "Promovering", each opening the app's form (#maaling/google-analytics).
535
+ export function PartsScreen({ screen, files }: { screen: PartScreen; files: RepoFile[] }) {
536
+ const parts = [...screen.parts, ...appPartsIn(screen, files)];
537
+ return (
538
+ <Parts title={screen.label} route={screen.id} parts={parts} back={backFrom(screen.id)}>
539
+ {!parts.length && <EmptyNote>{t.apps.noneHere}</EmptyNote>}
540
+ </Parts>
541
+ );
542
+ }
543
+
544
+ // "Blokke": the blocks, each with the texts all its copies have (blockTexts.ts), in groups by where they come
545
+ // from: the standard blocks, the theme's and each app's under its name (blockGroups). Over them, as over the
546
+ // lists, a search by name, the order (by group, A–Å or Å–A, kept in this browser; A–Å under the first letter)
547
+ // and the funnel with the filters by group and by whether the block is on a page, each picked one as a chip.
548
+ type BlockSort = 'group' | 'az' | 'za';
549
+ const BLOCK_SORTS: BlockSort[] = ['group', 'az', 'za'];
550
+ const BLOCK_SORT_KEY = 'editor-sort-blokke';
551
+ type Use = '' | 'used' | 'unused';
552
+
553
+ export function Blocks({ files }: { files: RepoFile[] }) {
554
+ const [query, setQuery] = useKept('blokke-soeg', '');
555
+ const [sort, setSort] = useState<BlockSort>(() => {
556
+ try {
557
+ const saved = localStorage.getItem(BLOCK_SORT_KEY);
558
+ if (BLOCK_SORTS.some((s) => s === saved)) return saved as BlockSort;
559
+ } catch {}
560
+ return 'group';
561
+ });
562
+ const [group, setGroup] = useKept('blokke-gruppe', '');
563
+ const [use, setUse] = useKept<Use>('blokke-brug', '');
564
+ // The blocks on a page (any copy, also a hidden one), by their type (a part's id).
565
+ const used = useMemo(() => new Set(blockParts.filter((p) => pagesWith(p.id, files).length).map((p) => p.id)), [files]);
566
+ const bothUses = blockParts.some((p) => used.has(p.id)) && blockParts.some((p) => !used.has(p.id));
567
+ const shownUse = bothUses ? use : '';
568
+ const shownGroup = blockGroups.some((g) => g.title === group) ? group : '';
569
+
570
+ function pickSort(next: BlockSort) {
571
+ setSort(next);
572
+ try {
573
+ localStorage.setItem(BLOCK_SORT_KEY, next);
574
+ } catch {}
575
+ }
576
+
577
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
578
+ const fits = (p: Part) =>
579
+ words.every((w) => p.label.toLowerCase().includes(w)) && (!shownUse || used.has(p.id) === (shownUse === 'used'));
580
+ const inGroups = blockGroups
581
+ .filter((g) => !shownGroup || g.title === shownGroup)
582
+ .map((g) => ({ title: g.title, parts: g.parts.filter(fits) }))
583
+ .filter((g) => g.parts.length > 0);
584
+ // A–Å or Å–A: all in one list, under the first letter of their names (headed, from HEADINGS_FROM).
585
+ const lettered =
586
+ sort === 'group'
587
+ ? []
588
+ : headed(
589
+ inGroups.flatMap((g) => g.parts).sort((a, b) => (sort === 'za' ? -1 : 1) * byText(a.label, b.label)),
590
+ (p) => letterOf(p.label),
591
+ );
592
+ const useLabel = (u: Use) => (u === 'used' ? t.library.inUse : t.common.notInUse);
593
+ const filters = [
594
+ blockGroups.length > 1 && {
595
+ title: t.library.groupFilter,
596
+ value: shownGroup,
597
+ options: [{ value: '', label: t.library.allGroups }, ...blockGroups.map((g) => ({ value: g.title, label: g.title }))],
598
+ onChange: setGroup,
599
+ },
600
+ bothUses && {
601
+ title: t.library.useFilter,
602
+ value: shownUse,
603
+ options: [
604
+ { value: '', label: t.library.allUses },
605
+ { value: 'used', label: useLabel('used') },
606
+ { value: 'unused', label: useLabel('unused') },
607
+ ],
608
+ onChange: (v: string) => setUse(v as Use),
609
+ },
610
+ ].filter((g) => !!g);
611
+ // Each filter picked, as a chip that removes it.
612
+ const chips = [
613
+ shownGroup && { key: 'group', label: shownGroup, clear: () => setGroup('') },
614
+ shownUse && { key: 'use', label: useLabel(shownUse), clear: () => setUse('') },
615
+ ].filter((c) => !!c);
616
+
617
+ return (
618
+ <Screen title={t.common.parts.blocks} wide back={{ href: '#', label: siteName }}>
619
+ <SearchRow
620
+ value={query}
621
+ onChange={setQuery}
622
+ label={t.common.searchIn(t.common.parts.blocks)}
623
+ sort={
624
+ <Choice
625
+ title={t.library.sort}
626
+ icon={ArrowsDownUp}
627
+ value={sort}
628
+ options={BLOCK_SORTS.map((s) => ({ value: s, label: t.library.sorts[s] }))}
629
+ onChange={pickSort}
630
+ />
631
+ }
632
+ filter={filters.length > 0 && <Filters title={t.library.filter} selected={chips.length > 0} groups={filters} />}
633
+ className="mb-6"
634
+ />
635
+ {chips.length > 0 && (
636
+ <div className="-mt-2 mb-6 flex flex-wrap gap-2">
637
+ {chips.map((c) => (
638
+ <InputChip key={c.key} removeLabel={t.library.removeFilter(c.label)} onRemove={c.clear}>
639
+ {c.label}
640
+ </InputChip>
641
+ ))}
642
+ </div>
643
+ )}
644
+ {!inGroups.length ? (
645
+ <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
646
+ ) : sort === 'group' ? (
647
+ inGroups.map((g) => <PartGroup key={g.title} title={g.title} route="blokke" parts={g.parts} />)
648
+ ) : (
649
+ lettered.map((g) => <PartGroup key={g.key ?? 'all'} title={g.key ?? undefined} route="blokke" parts={g.items} />)
650
+ )}
651
+ </Screen>
652
+ );
653
+ }
654
+
655
+ // A screen with parts, each opening its own screen at #<route>/<part>; `children` over them. It goes back
656
+ // to the start screen, or to `back`.
657
+ function Parts({
658
+ title,
659
+ route,
660
+ parts,
661
+ back = { href: '#', label: siteName },
662
+ children,
663
+ }: {
664
+ title: string;
665
+ route: string;
666
+ parts: Part[];
667
+ back?: { href: string; label: string };
668
+ children?: ReactNode;
669
+ }) {
670
+ return (
671
+ <Screen title={title} wide back={back}>
672
+ {children}
673
+ <PartGroup route={route} parts={parts} />
674
+ </Screen>
675
+ );
676
+ }
677
+
678
+ function PartGroup({ title, route, parts }: { title?: string; route: string; parts: Part[] }) {
679
+ return (
680
+ <Group title={title}>
681
+ {parts.map((p) => (
682
+ <ListItem
683
+ key={p.id}
684
+ href={p.href ?? `#${route}/${p.id}`}
685
+ icon={p.icon}
686
+ headline={p.label}
687
+ count={p.detail}
688
+ trailing={p.trailing}
689
+ arrow
690
+ />
691
+ ))}
692
+ </Group>
693
+ );
694
+ }
695
+
696
+ // A screen with parts of the site as rows, under one on the start screen: "Header" (the menus) and
697
+ // "Lister" (the short lists: the site's, testimonials, and those of the apps installed, prices), A–Å as on the start screen.
698
+ function PartsOf({ title, ids, files }: { title: string; ids: string[]; files: RepoFile[] }) {
699
+ return (
700
+ <Screen title={title} wide back={{ href: '#', label: siteName }}>
701
+ <Group>
702
+ {partsIn(ids, files).map((p) => (
703
+ <PartRow key={p.id} {...p} />
704
+ ))}
705
+ </Group>
706
+ </Screen>
707
+ );
708
+ }
709
+
710
+ export const Header = ({ files }: { files: RepoFile[] }) => <PartsOf title={t.common.parts.header} ids={header} files={files} />;
711
+ export const Lists = ({ files }: { files: RepoFile[] }) => <PartsOf title={t.common.parts.lists} ids={listsShown(files)} files={files} />;
712
+
713
+ // The account: who is logged in, light or dark, the language, and "Log ud".
714
+ export function Account({ user, files, onLogout }: { user: Viewer | null; files: RepoFile[]; onLogout: () => void }) {
715
+ return (
716
+ // Titled with the name of the one logged in.
717
+ <Screen title={user ? (user.name ?? user.login) : t.common.parts.account} back={{ href: '#', label: siteName }}>
718
+ {/* Who is logged in, as plain text: nothing to tap. */}
719
+ <div className="mb-8 flex items-center gap-3 px-4">
720
+ <span className="flex size-12 shrink-0 items-center justify-center rounded-full text-muted">
721
+ <GithubLogo size={28} />
722
+ </span>
723
+ <span className="min-w-0 flex-1">
724
+ <span className="block truncate font-medium">{user ? (user.name ?? user.login) : ''}</span>
725
+ {user && <span className="block truncate text-sm text-muted">{t.library.githubAccount(user.login)}</span>}
726
+ </span>
727
+ </div>
728
+ <section className="mb-8">
729
+ <GroupTitle>{t.library.theme}</GroupTitle>
730
+ <ThemePicker />
731
+ </section>
732
+ <LocalePicker />
733
+ <NotificationChoices files={files} />
734
+ <Group>
735
+ <ListItem tone="accent" onClick={onLogout} leading={<SignOut size={20} />} headline={t.library.logOut} />
736
+ </Group>
737
+ </Screen>
738
+ );
739
+ }
740
+
741
+ // "Notifikationer" (notifications.ts): "På telefonen", a switch for each of the editor's types, and under the name of each
742
+ // installed app that sends any, a switch for each of its types (`notifications` in its index.ts).
743
+ function NotificationChoices({ files }: { files: RepoFile[] }) {
744
+ const { off, device } = useNotificationChoices();
745
+ const installed = installedApps(files);
746
+ const choice = (type: string, label: string) => (
747
+ <SwitchRow
748
+ key={type}
749
+ label={label}
750
+ texts={[t.library.on, t.library.off]}
751
+ selected={!off.includes(type)}
752
+ onChange={(on) => setOn(type, on)}
753
+ />
754
+ );
755
+ return (
756
+ <>
757
+ <section className="mb-8">
758
+ <GroupTitle>{t.library.notifications}</GroupTitle>
759
+ <div className="space-y-5 px-4">
760
+ <SwitchRow
761
+ label={t.library.onDevice}
762
+ hint={t.library.onDeviceHint}
763
+ texts={[t.library.on, t.library.off]}
764
+ selected={device}
765
+ onChange={async (on) => {
766
+ if (!(await setDevice(on))) snackbar({ text: t.library.deviceBlocked, error: true });
767
+ }}
768
+ />
769
+ {EDITOR_TYPES.map((type) => choice(type, t.library.notificationTypes[type]))}
770
+ </div>
771
+ </section>
772
+ {apps
773
+ .filter((a) => a.notifications && installed.includes(a.id))
774
+ .map((app) => (
775
+ <section key={app.id} className="mb-8">
776
+ <GroupTitle>{siteText(app.label)}</GroupTitle>
777
+ <div className="space-y-5 px-4">
778
+ {Object.entries(app.notifications!.types).map(([id, label]) => choice(appType(app.id, id), siteText(label)))}
779
+ </div>
780
+ </section>
781
+ ))}
782
+ </>
783
+ );
784
+ }
785
+
786
+ // Light, as the phone or computer is set (Auto, the default), or dark, as three buttons side by side.
787
+ function ThemePicker() {
788
+ const [theme, pick] = useState(savedTheme);
789
+ const choices: { value: Theme; label: string; icon: Icon }[] = [
790
+ { value: 'light', label: t.library.themes.light, icon: Sun },
791
+ { value: 'auto', label: t.library.themes.auto, icon: CircleHalf },
792
+ { value: 'dark', label: t.library.themes.dark, icon: Moon },
793
+ ];
794
+ return (
795
+ <SegmentedButton
796
+ label={t.library.theme}
797
+ value={theme}
798
+ options={choices}
799
+ onChange={(value) => {
800
+ pick(value);
801
+ setTheme(value);
802
+ }}
803
+ />
804
+ );
805
+ }
806
+
807
+ // The editor's language: a row with the one chosen, which opens a sheet with Automatic (the browser's,
808
+ // else the site's; the default) and the languages, each by its own name, with a search that finds a
809
+ // language by that, by its name in the editor's language ("Danish") and by its code (da-DK). A choice
810
+ // opens the editor again in it.
811
+ function LocalePicker() {
812
+ const picked = savedLocale();
813
+ const choices: { value: Locale | 'auto'; label: string; lang?: string; words?: string }[] = [
814
+ { value: 'auto', label: t.library.languageAuto(localeName(browserLocale() ?? siteLocale)) },
815
+ ...locales.map((l) => ({ value: l, label: localeName(l), lang: l, words: `${localeName(l, locale)} ${l}` })),
816
+ ];
817
+ return (
818
+ <Group>
819
+ <ChoiceListItem
820
+ title={t.library.language}
821
+ value={picked}
822
+ options={choices}
823
+ search={t.library.languageSearch}
824
+ onChange={(v) => v !== picked && setLocale(v)}
825
+ />
826
+ </Group>
827
+ );
828
+ }
829
+
830
+ // path: a page's address, for the tree of pages. count: the number at the right of the row, as on the
831
+ // start screen (the blog posts with a tag).
832
+ type Item = {
833
+ href: string;
834
+ title: string;
835
+ detail: string;
836
+ date?: string;
837
+ /** Kladde, Skjult or Planlagt (statusOf), before its date; an entry not yet looked through, what it says
838
+ * (FolderCollection.fresh: "Ny"). */
839
+ status?: string;
840
+ /** Added by something else and not yet looked through (FolderCollection.fresh): at the top of the list. */
841
+ fresh?: boolean;
842
+ /** Which of them, or published: what the choices over the list show. */
843
+ state?: State;
844
+ file?: RepoFile;
845
+ path?: string;
846
+ count?: number | null;
847
+ /** In a list of kinds: the name of its kind, for sorting by it. */
848
+ kind?: string;
849
+ /** In a list of what is shown on pages (FolderCollection.shownOn): on every page, or on these pages
850
+ * and the posts with these tags. */
851
+ shownOn?: { everywhere: boolean; pages: string[]; tags: string[] };
852
+ /** In a list of what can be turned on and off (FolderCollection.onOff): whether it is on. */
853
+ on?: boolean;
854
+ /** Pages and posts: what is picked in each field that picks several (a post's tags), by the field's name; a list's
855
+ * entries: the value of each field it is filtered by (FolderCollection.filters). */
856
+ picks?: Record<string, string[]>;
857
+ };
858
+
859
+ // The statuses of a page or post (`status`, packages/editor/lib/status.mjs), in the order of the choices over the list.
860
+ type State = 'published' | 'draft' | 'unlisted' | 'scheduled';
861
+ const STATES: State[] = ['published', 'draft', 'unlisted', 'scheduled'];
862
+ const stateOf = (d: Record<string, unknown>): State =>
863
+ d.status === 'draft' || d.status === 'unlisted' || d.status === 'scheduled' ? d.status : 'published';
864
+
865
+ // What a page or post is when not simply published (`status`, packages/editor/lib/status.mjs): "Kladde", "Skjult" or
866
+ // "Planlagt 1. nov. kl. 8.00".
867
+ function statusOf(d: Record<string, unknown>): string | undefined {
868
+ if (d.status === 'draft') return t.library.status.draft;
869
+ if (d.status === 'unlisted') return t.library.status.unlisted;
870
+ if (d.status !== 'scheduled') return undefined;
871
+ const at = untilTime(String(d.publishAt ?? ''));
872
+ return at === null ? t.library.status.scheduled : t.library.status.scheduledAt(when(new Date(at)));
873
+ }
874
+
875
+ function itemsOf(collection: Collection, files: RepoFile[]): Item[] {
876
+ if (collection.kind === 'files') return collection.files.map((f) => ({ href: `#${f.file}`, title: f.label, detail: f.view }));
877
+ const own = files.filter((f) => f.path.startsWith(`${collection.folder}/`));
878
+ if (collection.kind === 'markdown') {
879
+ const items: Item[] = own
880
+ .filter((f) => f.path.endsWith('.md'))
881
+ .map((file) => {
882
+ const d = readFields(splitFrontmatter(file.text).yaml);
883
+ // Blog posts show their date; pages when they were last saved (`saved`, asked in List).
884
+ const detail = collection.id === 'blog' ? String(d.date ?? '') : '';
885
+ const date = collection.id === 'blog' ? detail : undefined;
886
+ const path = String(d.path ?? `/${file.path.slice(collection.folder.length + 1).replace(/(\/index)?\.md$/, '')}/`);
887
+ return {
888
+ href: `#${file.path}`,
889
+ title: String(d.title ?? file.path),
890
+ detail,
891
+ date,
892
+ status: statusOf(d),
893
+ state: stateOf(d),
894
+ file,
895
+ path,
896
+ picks: Object.fromEntries(
897
+ collection.fields
898
+ .filter((f) => f.type === 'relation' && f.multiple)
899
+ .map((f) => [f.name, Array.isArray(d[f.name]) ? (d[f.name] as unknown[]).map(String) : []]),
900
+ ),
901
+ };
902
+ });
903
+ return items.sort((a, b) => (collection.id === 'blog' ? b.detail.localeCompare(a.detail) : byText(a.title, b.title)));
904
+ }
905
+ const items = own
906
+ .filter((f) => f.path.endsWith('.json'))
907
+ .map((file) => ({ file, data: entryData(collection, file.text, files) }))
908
+ .sort(
909
+ (a, b) =>
910
+ (collection.orderable ? Number(a.data.order) - Number(b.data.order) : 0) ||
911
+ byText(collection.summary(a.data), collection.summary(b.data)),
912
+ );
913
+ // The number the site has for each entry (the places that use a tag).
914
+ const count = countsOf(collection, files);
915
+ // In a list of kinds, each entry says its kind beside its name (Pop op-vindue, Handlingslinje, Mailchimp).
916
+ const kinds = collection.fields.find((f) => f.name === collection.kinds)?.options ?? [];
917
+ const kindLabel = (data: Record<string, unknown>) =>
918
+ kinds.find((o) => o.value === (data[collection.kinds!] ?? kinds[0]?.value))?.label ?? '';
919
+ const on = collection.shownOn;
920
+ const onOff = collection.fields.find((f) => f.name === collection.onOff);
921
+ const fresh = collection.fresh;
922
+ return items.map(({ file, data }) => ({
923
+ href: `#${file.path}`,
924
+ ...(fresh && data[fresh.field] === true ? { fresh: true, status: fresh.label(data) } : {}),
925
+ // Without a name of its own (the contact bar), by its kind's name.
926
+ title: collection.summary(data) || kindLabel(data),
927
+ detail: collection.kinds ? kindLabel(data) : (collection.detail?.(data) ?? ''),
928
+ picks: Object.fromEntries(
929
+ (collection.filters ?? []).map(({ field: name }) => {
930
+ const value = data[name] ?? collection.fields.find((f) => f.name === name)?.default;
931
+ return [name, value == null || value === '' ? [] : [String(value)]];
932
+ }),
933
+ ),
934
+ count: count(file),
935
+ file,
936
+ kind: collection.kinds ? kindLabel(data) : undefined,
937
+ shownOn: on && {
938
+ everywhere: !!on.everywhere && data[on.everywhere.field] === on.everywhere.is,
939
+ pages: Array.isArray(data[on.pages]) ? (data[on.pages] as unknown[]).map(String) : [],
940
+ tags: on.tags && Array.isArray(data[on.tags]) ? (data[on.tags] as unknown[]).map(String) : [],
941
+ },
942
+ on: onOff && (data[onOff.name] ?? onOff.default) !== false,
943
+ }));
944
+ }
945
+
946
+ // The pages as the menu on the site has them (src/data/menu.json, "Hovedmenu"): each item of the menu, by
947
+ // the name it has there, with the page it goes to and the items under it; then the pages that are
948
+ // not in the menu, A–Å. A link to a part of a page (/kontakt/#find-vej) is that page; an item whose
949
+ // page is gone is shown without a link.
950
+ type Branch = { key: string; item: Item; children: Branch[] };
951
+ type MenuEntry = { label: string; href: string; children?: MenuEntry[] };
952
+ function menuOf(items: Item[], files: RepoFile[]): { menu: Branch[]; rest: Item[] } {
953
+ let entries: MenuEntry[] = [];
954
+ try {
955
+ entries = JSON.parse(files.find((f) => f.path === config.menu)?.text ?? '{}').items ?? [];
956
+ } catch {}
957
+ const used = new Set<Item>();
958
+ const branch = (e: MenuEntry, key: string): Branch => {
959
+ const page = items.find((i) => i.path === e.href.replace(/#.*/, ''));
960
+ if (page) used.add(page);
961
+ return {
962
+ key,
963
+ item: { ...(page ?? { href: '', detail: '' }), title: e.label },
964
+ children: (e.children ?? []).map((c, i) => branch(c, `${key}-${i}`)),
965
+ };
966
+ };
967
+ const menu = entries.map((e, i) => branch(e, `m${i}`));
968
+ return { menu, rest: items.filter((i) => !used.has(i)).sort((a, b) => byText(a.title, b.title)) };
969
+ }
970
+
971
+ // List or as the menu (pages), as picked last time in this browser.
972
+ type View = 'liste' | 'menu';
973
+ // Whether the pages and posts show their SEO rating ("Vis SEO-vurdering"), as left last time in this browser.
974
+ const seoKey = (c: Collection) => `editor-seo-${c.id}`;
975
+ function savedSeo(c: Collection): boolean {
976
+ try {
977
+ return localStorage.getItem(seoKey(c)) === 'vis';
978
+ } catch {
979
+ return false;
980
+ }
981
+ }
982
+
983
+ const viewKey = (c: Collection) => `editor-view-${c.id}`;
984
+ function savedView(c: Collection): View {
985
+ try {
986
+ return localStorage.getItem(viewKey(c)) === 'menu' ? 'menu' : 'liste';
987
+ } catch {
988
+ return 'liste';
989
+ }
990
+ }
991
+
992
+ // The items of the menu that are open (their pages under them shown), as left last time in this browser.
993
+ const openKey = (c: Collection) => `editor-menu-${c.id}`;
994
+ function savedOpen(c: Collection): Set<string> {
995
+ try {
996
+ return new Set(JSON.parse(localStorage.getItem(openKey(c)) ?? '[]') as string[]);
997
+ } catch {
998
+ return new Set();
999
+ }
1000
+ }
1001
+
1002
+ // When each page was last saved, kept while the editor is open. Asked again for a page whose file has
1003
+ // changed since (another blob id), so a page just saved shows its new time.
1004
+ const saved = new Map<string, { oid: string; date: Date }>();
1005
+
1006
+ // The ways a list can be sorted; by time only where the entries have one: blog posts by their date,
1007
+ // pages by when they were last saved; by their kind where they have kinds (Promovering).
1008
+ type Sort = 'nyeste' | 'aeldste' | 'az' | 'za' | 'type';
1009
+ const sortLabel = (c: Collection, s: Sort) => {
1010
+ const { sorts } = t.library;
1011
+ return c.id === 'pages' && s === 'nyeste' ? sorts.savedNewest : c.id === 'pages' && s === 'aeldste' ? sorts.savedOldest : sorts[s];
1012
+ };
1013
+
1014
+ const byTitle = (a: Item, b: Item) => byText(a.title, b.title);
1015
+ const compare: Record<Sort, (a: Item, b: Item) => number> = {
1016
+ nyeste: (a, b) => (b.date ?? '').localeCompare(a.date ?? '') || byTitle(a, b),
1017
+ aeldste: (a, b) => (a.date ?? '').localeCompare(b.date ?? '') || byTitle(a, b),
1018
+ az: byTitle,
1019
+ za: (a, b) => byTitle(b, a),
1020
+ type: (a, b) => byText(a.kind ?? '', b.kind ?? '') || byTitle(a, b),
1021
+ };
1022
+
1023
+ // The sorting picked for a list last time, in this browser.
1024
+ const sortKey = (c: Collection) => `editor-sort-${c.id}`;
1025
+
1026
+ function savedSort(c: Collection, sorts: Sort[], first: Sort): Sort {
1027
+ try {
1028
+ const s = localStorage.getItem(sortKey(c)) as Sort | null;
1029
+ if (s && sorts.includes(s)) return s;
1030
+ } catch {}
1031
+ return first;
1032
+ }
1033
+
1034
+ // "i dag kl. 13.42" as "I dag kl. 13.42", alone on its line.
1035
+ const capital = (s: string) => s.charAt(0).toLocaleUpperCase(locale) + s.slice(1);
1036
+
1037
+ // "2023-10-01" as "1. okt. 2023" (in the editor's language).
1038
+ const date = (iso: string) =>
1039
+ /^\d{4}-\d{2}-\d{2}$/.test(iso)
1040
+ ? new Date(`${iso}T12:00`).toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' })
1041
+ : iso;
1042
+
1043
+ export function List({ collection, tab, files, env }: { collection: Collection; tab?: string; files: RepoFile[]; env: Env }) {
1044
+ // The kinds a new entry can be: the list's own and those of the apps installed and not turned off (apps.ts).
1045
+ const kinds = collection.kind === 'folder' ? kindsOffered(collection, apps, installedApps(files), offBlocks(files)) : [];
1046
+ // The tabs the apps installed add to it, and the one open (none: the list's own).
1047
+ const appTabs = tabsOf(collection.id, apps, installedApps(files));
1048
+ const appTab = appTabs.find((a) => a.app.id === tab);
1049
+ const [query, setQuery] = useKept(`${collection.id}-soeg`, '');
1050
+ const [busy, setBusy] = useState(false);
1051
+ const items = useMemo(() => itemsOf(collection, files), [collection, files]);
1052
+ const pages = collection.id === 'pages';
1053
+ // Pages and posts: their address in a column on a computer, and a filter that shows only those with
1054
+ // one status (when they do not all have the same).
1055
+ const markdown = collection.kind === 'markdown';
1056
+ const [picked, setState] = useKept<State | ''>(`${collection.id}-status`, '');
1057
+ const states = markdown ? STATES.filter((s) => items.some((i) => i.state === s)) : [];
1058
+ // All again when the status picked is no longer there to pick (the last draft was published meanwhile).
1059
+ const state = states.length > 1 && states.includes(picked as State) ? picked : '';
1060
+ // Prices and testimonials are in their order on the site; the other lists of entries can be sorted.
1061
+ // Blog posts newest first, pages A–Å, until another is picked.
1062
+ const sorts: Sort[] =
1063
+ collection.kind === 'files' || (collection.kind === 'folder' && collection.orderable) || items.length < 2
1064
+ ? []
1065
+ : collection.id === 'blog' || pages
1066
+ ? ['nyeste', 'aeldste', 'az', 'za']
1067
+ : collection.kind === 'folder' && collection.kinds
1068
+ ? ['az', 'za', 'type']
1069
+ : ['az', 'za'];
1070
+ const [sort, setSort] = useState(() => savedSort(collection, sorts, pages ? 'az' : sorts[0]));
1071
+ // Pages and posts: the Google icon by the order shows the score of each one's SEO rating at the start of its row
1072
+ // (ScoreRing, the same as under "Søgemaskiner" and "SEO-vurdering"); a tap on the ring opens it there, on the rest of
1073
+ // the row at its top.
1074
+ const rated = isRated(collection);
1075
+ const [seo, setSeo] = useState(() => rated && savedSeo(collection));
1076
+ const scores = useMemo(
1077
+ () =>
1078
+ seo
1079
+ ? new Map(
1080
+ items.flatMap((i) =>
1081
+ i.file ? [[i.file.path, scoreOf(fileRating(i.file.text, collection.id === 'blog', env.relations))]] : [],
1082
+ ),
1083
+ )
1084
+ : null,
1085
+ [seo, items, env.relations],
1086
+ );
1087
+ const ring = (item: Item) => {
1088
+ const score = item.file ? scores?.get(item.file.path) : undefined;
1089
+ return score === undefined ? null : <ScoreRing score={score} label={t.seo.scoreLabel(score)} onClick={openAtSeo} />;
1090
+ };
1091
+ const [, setSavedLoaded] = useState(0);
1092
+ useEffect(() => {
1093
+ if (!pages) return;
1094
+ const missing = items.flatMap((i) => (i.file && saved.get(i.file.path)?.oid !== i.file.oid ? [i.file] : []));
1095
+ if (!missing.length) return;
1096
+ env.savedAt(missing.map((f) => f.path)).then(
1097
+ (times) => {
1098
+ for (const f of missing) if (times[f.path]) saved.set(f.path, { oid: f.oid, date: times[f.path] });
1099
+ setSavedLoaded((n) => n + 1);
1100
+ },
1101
+ () => {},
1102
+ );
1103
+ }, [items]);
1104
+ // A list of what is shown on pages: only those on one page or the posts of one tag (and those on every
1105
+ // page), picked among the pages and tags the entries are on, by their names (relationsOf), the tags
1106
+ // under a heading of their own (`tag:<slug>`); all again when that one is no longer among them.
1107
+ const [onPage, setOnPage] = useKept(`${collection.id}-side`, '');
1108
+ const pageNames = useMemo(() => {
1109
+ if (!items.some((i) => i.shownOn)) return [];
1110
+ const { pages: all, fixed, categories } = relationsOf(files);
1111
+ const pages = new Set(items.flatMap((i) => i.shownOn?.pages ?? []));
1112
+ const tags = new Set(items.flatMap((i) => i.shownOn?.tags ?? []));
1113
+ const onTags = categories.filter((c) => tags.has(c.value));
1114
+ return [
1115
+ ...[...fixed, ...all]
1116
+ .filter((p) => pages.has(p.value))
1117
+ .map((p) => ({ ...p, group: onTags.length ? t.library.pagesGroup : undefined })),
1118
+ ...onTags.map((c) => ({ value: `tag:${c.value}`, label: c.label, group: t.library.tagsGroup })),
1119
+ ];
1120
+ }, [items, files]);
1121
+ const page = pageNames.some((p) => p.value === onPage) ? onPage : '';
1122
+ // By the kind (when there are two or more) and by whether it is turned on (both, when there are both).
1123
+ const [kind, setKind] = useKept(`${collection.id}-type`, '');
1124
+ const kindNames = [...new Set(items.flatMap((i) => (i.kind ? [i.kind] : [])))].sort(byText);
1125
+ const shownKind = kindNames.length > 1 && kindNames.includes(kind) ? kind : '';
1126
+ const [onOff, setOnOff] = useKept<'' | 'on' | 'off'>(`${collection.id}-til`, '');
1127
+ // Pages and posts: by what is picked in each field that picks several (a post's tags), among those
1128
+ // picked in an entry, by their names (relationsOf); a list's entries by each field it is filtered by
1129
+ // (FolderCollection.filters: a cookie's provider), among the values they have, when they have two or more. All
1130
+ // again when the one picked is no longer among them.
1131
+ const [picks, setPicks] = useKept<Record<string, string>>(`${collection.id}-valgt`, {});
1132
+ const pickFilters = useMemo(() => {
1133
+ if (collection.kind === 'folder')
1134
+ return (collection.filters ?? []).flatMap(({ field: name, all }) => {
1135
+ const f = collection.fields.find((x) => x.name === name);
1136
+ const used = [...new Set(items.flatMap((i) => i.picks?.[name] ?? []))];
1137
+ if (!f || used.length < 2) return [];
1138
+ const options = used
1139
+ .map((value) => ({ value, label: f.options?.find((o) => o.value === value)?.label ?? value }))
1140
+ .sort((a, b) => byText(a.label, b.label));
1141
+ return [{ field: f, all, options, value: options.some((o) => o.value === picks[name]) ? picks[name] : '' }];
1142
+ });
1143
+ if (!markdown) return [];
1144
+ const relations = relationsOf(files);
1145
+ return collection.fields.flatMap((f) => {
1146
+ if (f.type !== 'relation' || !f.multiple || !f.to) return [];
1147
+ const used = new Set(items.flatMap((i) => i.picks?.[f.name] ?? []));
1148
+ const options = relations[f.to].filter((o) => used.has(o.value));
1149
+ return options.length
1150
+ ? [{ field: f, all: '', options, value: options.some((o) => o.value === picks[f.name]) ? picks[f.name] : '' }]
1151
+ : [];
1152
+ });
1153
+ }, [collection, items, files, picks]);
1154
+ const pick = (name: string, value: string) => setPicks({ ...picks, [name]: value });
1155
+ const onOffField = collection.kind === 'folder' ? collection.fields.find((f) => f.name === collection.onOff) : undefined;
1156
+ const onOffLabels = onOffField?.toggle;
1157
+ const bothOnOff = items.some((i) => i.on) && items.some((i) => i.on === false);
1158
+ const shownOnOff = onOffLabels && bothOnOff ? onOff : '';
1159
+ const filters = [
1160
+ states.length > 1 && {
1161
+ title: t.library.statusFilter,
1162
+ value: state,
1163
+ options: [{ value: '', label: t.library.statusShown.all }, ...states.map((s) => ({ value: s, label: t.library.statusShown[s] }))],
1164
+ onChange: (v: string) => setState(v as State | ''),
1165
+ },
1166
+ ...pickFilters.map((p) => ({
1167
+ title: p.field.label,
1168
+ value: p.value,
1169
+ options: [{ value: '', label: p.all || t.library.allOf(p.field.label) }, ...p.options],
1170
+ onChange: (v: string) => pick(p.field.name, v),
1171
+ })),
1172
+ pageNames.length > 0 && {
1173
+ title: t.library.pageFilter,
1174
+ value: page,
1175
+ options: [{ value: '', label: t.library.allPages }, ...pageNames],
1176
+ onChange: setOnPage,
1177
+ },
1178
+ kindNames.length > 1 && {
1179
+ title: t.library.kindFilter,
1180
+ value: shownKind,
1181
+ options: [{ value: '', label: t.library.allKinds }, ...kindNames.map((k) => ({ value: k, label: k }))],
1182
+ onChange: setKind,
1183
+ },
1184
+ onOffLabels &&
1185
+ bothOnOff && {
1186
+ title: t.library.onOffFilter,
1187
+ value: shownOnOff,
1188
+ options: [
1189
+ { value: '', label: t.library.allOnOff },
1190
+ { value: 'on', label: onOffLabels[0] },
1191
+ { value: 'off', label: onOffLabels[1] },
1192
+ ],
1193
+ onChange: (v: string) => setOnOff(v as typeof onOff),
1194
+ },
1195
+ ].filter((g) => !!g);
1196
+ // Each filter picked, as a chip that removes it.
1197
+ const chips = [
1198
+ state && { key: 'state', label: t.library.statusShown[state], clear: () => setState('') },
1199
+ ...pickFilters.flatMap((p) =>
1200
+ p.value
1201
+ ? [{ key: p.field.name, label: p.options.find((o) => o.value === p.value)?.label ?? p.value, clear: () => pick(p.field.name, '') }]
1202
+ : [],
1203
+ ),
1204
+ page && { key: 'page', label: pageNames.find((p) => p.value === page)?.label ?? page, clear: () => setOnPage('') },
1205
+ shownKind && { key: 'kind', label: shownKind, clear: () => setKind('') },
1206
+ shownOnOff && onOffLabels && { key: 'onOff', label: onOffLabels[shownOnOff === 'on' ? 0 : 1], clear: () => setOnOff('') },
1207
+ ].filter((c) => !!c);
1208
+ const q = query.trim().toLowerCase();
1209
+ // Pages get the time they were last saved as their date, once it has come.
1210
+ const dated = pages
1211
+ ? items.map((i) => {
1212
+ const t = i.file && saved.get(i.file.path);
1213
+ return t ? { ...i, date: t.date.toISOString() } : i;
1214
+ })
1215
+ : items;
1216
+ const found = dated.filter(
1217
+ (i) =>
1218
+ // With the line under it, when the list has one of its own (FolderCollection.detail).
1219
+ `${i.title}${collection.kind === 'folder' && collection.detail ? ` ${i.detail}` : ''}`.toLowerCase().includes(q) &&
1220
+ (!state || i.state === state) &&
1221
+ pickFilters.every((p) => !p.value || !!i.picks?.[p.field.name]?.includes(p.value)) &&
1222
+ (!page ||
1223
+ !!i.shownOn?.everywhere ||
1224
+ (page.startsWith('tag:') ? !!i.shownOn?.tags.includes(page.slice(4)) : !!i.shownOn?.pages.includes(page))) &&
1225
+ (!shownKind || i.kind === shownKind) &&
1226
+ (!shownOnOff || i.on === (shownOnOff === 'on')),
1227
+ );
1228
+ const shown = sorts.length ? [...found].sort(compare[sort]) : found;
1229
+ // Pages can be shown as the menu has them; a search or a filter always gives the list of what it finds.
1230
+ const [view, setView] = useState(() => savedView(collection));
1231
+ const tree = pages && view === 'menu' && !q && !chips.length ? menuOf(dated, files) : null;
1232
+ // The items of the menu whose pages under them are shown; all closed at first, kept in this browser.
1233
+ const [openBranches, setOpenBranches] = useState(() => savedOpen(collection));
1234
+ const orderable = collection.kind === 'folder' && collection.orderable && !q;
1235
+
1236
+ function pickSort(s: Sort) {
1237
+ setSort(s);
1238
+ try {
1239
+ localStorage.setItem(sortKey(collection), s);
1240
+ } catch {}
1241
+ }
1242
+
1243
+ function toggleBranch(href: string) {
1244
+ const next = new Set(openBranches);
1245
+ if (!next.delete(href)) next.add(href);
1246
+ setOpenBranches(next);
1247
+ try {
1248
+ localStorage.setItem(openKey(collection), JSON.stringify([...next]));
1249
+ } catch {}
1250
+ }
1251
+
1252
+ function pickSeo(next: boolean) {
1253
+ setSeo(next);
1254
+ try {
1255
+ localStorage.setItem(seoKey(collection), next ? 'vis' : 'skjul');
1256
+ } catch {}
1257
+ }
1258
+
1259
+ function pickView(v: View) {
1260
+ setView(v);
1261
+ try {
1262
+ localStorage.setItem(viewKey(collection), v);
1263
+ } catch {}
1264
+ }
1265
+
1266
+ async function move(i: number, to: number) {
1267
+ const next = items.map((item) => item.file!);
1268
+ [next[i], next[to]] = [next[to], next[i]];
1269
+ setBusy(true);
1270
+ env.notify({ text: t.library.savingOrder });
1271
+ try {
1272
+ env.notify({ text: savedText(await env.reorder(collection, next)), kind: 'saved' });
1273
+ } catch (e) {
1274
+ env.notify({ text: t.library.orderNotSaved((e as Error).message), error: true, kind: 'failed' });
1275
+ } finally {
1276
+ setBusy(false);
1277
+ }
1278
+ }
1279
+
1280
+ // The entries not yet looked through first, under a heading of their own (FolderCollection.fresh); the rest under
1281
+ // headings (headed, from HEADINGS_FROM entries): the first letter by name, the month by date, the kind by kind (none in
1282
+ // their own order, or while the pages' times are coming).
1283
+ const fresh = collection.kind === 'folder' && collection.fresh ? shown.filter((i) => i.fresh) : [];
1284
+ const groups = headed(fresh.length ? shown.filter((i) => !i.fresh) : shown, (item) =>
1285
+ !sorts.length
1286
+ ? ''
1287
+ : sort === 'type'
1288
+ ? (item.kind ?? '')
1289
+ : sort === 'az' || sort === 'za'
1290
+ ? letterOf(item.title)
1291
+ : (item.date ?? '').slice(0, 7),
1292
+ );
1293
+ const listRow = (item: Item) => {
1294
+ const i = shown.indexOf(item);
1295
+ return (
1296
+ <ListItem
1297
+ key={item.href}
1298
+ href={item.href}
1299
+ leading={ring(item)}
1300
+ headline={item.title}
1301
+ supporting={
1302
+ (markdown || pages || item.detail || item.status) && (
1303
+ <Under item={item} address={markdown}>
1304
+ {pages ? (
1305
+ <SavedTime item={item} />
1306
+ ) : (
1307
+ <>
1308
+ <Status item={item} />
1309
+ {collection.id === 'blog' ? date(item.detail) : item.detail}
1310
+ </>
1311
+ )}
1312
+ </Under>
1313
+ )
1314
+ }
1315
+ truncate
1316
+ columns
1317
+ count={item.count}
1318
+ arrow={!orderable}
1319
+ actions={
1320
+ orderable && (
1321
+ <span className="flex shrink-0 pr-2">
1322
+ <IconButton icon={ArrowUp} label={t.common.moveUp} disabled={i === 0 || busy} onClick={() => move(i, i - 1)} />
1323
+ <IconButton
1324
+ icon={ArrowDown}
1325
+ label={t.common.moveDown}
1326
+ disabled={i === shown.length - 1 || busy}
1327
+ onClick={() => move(i, i + 1)}
1328
+ />
1329
+ </span>
1330
+ )
1331
+ }
1332
+ />
1333
+ );
1334
+ };
1335
+ return (
1336
+ <Screen
1337
+ title={collection.label}
1338
+ wide
1339
+ back={
1340
+ lists.includes(collection.id)
1341
+ ? { href: '#lister', label: t.common.parts.lists }
1342
+ : header.includes(collection.id)
1343
+ ? { href: '#header', label: t.common.parts.header }
1344
+ : { href: '#', label: siteName }
1345
+ }
1346
+ tabs={
1347
+ appTabs.length > 0
1348
+ ? {
1349
+ label: collection.label,
1350
+ value: appTab?.app.id ?? '',
1351
+ options: [
1352
+ { value: '', label: collection.label, icon: icons[collection.id as keyof typeof icons] ?? ListIcon },
1353
+ ...appTabs.map((a) => ({ value: a.app.id, label: siteText(a.tab.label), icon: a.tab.icon })),
1354
+ ],
1355
+ onChange: (v) => (location.hash = v ? `liste/${collection.id}/${v}` : `liste/${collection.id}`),
1356
+ }
1357
+ : undefined
1358
+ }
1359
+ actions={
1360
+ <>
1361
+ {/* What was deleted from the list, as in "Filer". */}
1362
+ {restorable(collection) && (
1363
+ <IconButton
1364
+ icon={ClockCounterClockwise}
1365
+ label={t.history.deletedOf(collection.label)}
1366
+
1367
+ onClick={() => (location.hash = `slettet/${collection.id}`)}
1368
+ />
1369
+ )}
1370
+ {/* Of more than one kind (a pop-up: Standard or an app's, e.g. Mailchimp): a sheet to pick it. */}
1371
+ {collection.create &&
1372
+ (kinds.length > 1 ? (
1373
+ <NewChoice title={collection.create} options={kinds} onChange={(kind) => (location.hash = `#ny/${collection.id}/${kind}`)} />
1374
+ ) : (
1375
+ <IconLink href={`#ny/${collection.id}`} label={collection.create!} icon={Plus} />
1376
+ ))}
1377
+ </>
1378
+ }
1379
+ >
1380
+ {/* An app's tab, drawn as its page. */}
1381
+ {appTab && (
1382
+ <div data-app-page={appTab.app.id}>
1383
+ <appTab.tab.page api={(path, init) => env.appApi(appTab.app.id, path, init)} />
1384
+ </div>
1385
+ )}
1386
+ {/* The search, in a long list and in every list that can be sorted or filtered (Promovering), with those. */}
1387
+ {!appTab && (items.length > 8 || sorts.length > 0 || filters.length > 0) && (
1388
+ <SearchRow
1389
+ value={query}
1390
+ onChange={setQuery}
1391
+ label={t.common.searchIn(collection.label)}
1392
+ sort={
1393
+ sorts.length > 0 && (
1394
+ // Greyed out while the pages are shown as the menu, which has its own order, so the buttons do not move.
1395
+ <Choice
1396
+ title={t.library.sort}
1397
+ icon={ArrowsDownUp}
1398
+ value={sort}
1399
+ disabled={!!tree}
1400
+ options={sorts.map((s) => ({ value: s, label: sortLabel(collection, s) }))}
1401
+ onChange={pickSort}
1402
+ />
1403
+ )
1404
+ }
1405
+ filter={filters.length > 0 && <Filters title={t.library.filter} selected={chips.length > 0} groups={filters} />}
1406
+ seo={
1407
+ rated && (
1408
+ <IconButton
1409
+ icon={GoogleLogo}
1410
+ label={seo ? t.library.hideSeo : t.library.showSeo}
1411
+ aria-pressed={seo}
1412
+ selected={seo}
1413
+
1414
+ onClick={() => pickSeo(!seo)}
1415
+ />
1416
+ )
1417
+ }
1418
+ view={
1419
+ pages && (
1420
+ // The menu icon to show the pages as the menu has them, and then the list icon to go back.
1421
+ <IconButton
1422
+ icon={view === 'menu' ? ListBullets : ListIcon}
1423
+ label={view === 'menu' ? t.library.showAsList : t.library.showAsMenu}
1424
+ aria-pressed={view === 'menu'}
1425
+
1426
+ onClick={() => pickView(view === 'menu' ? 'liste' : 'menu')}
1427
+ />
1428
+ )
1429
+ }
1430
+ className="mb-6"
1431
+ />
1432
+ )}
1433
+ {!appTab && chips.length > 0 && (
1434
+ <div className="-mt-2 mb-6 flex flex-wrap gap-2">
1435
+ {chips.map((c) => (
1436
+ <InputChip key={c.key} removeLabel={t.library.removeFilter(c.label)} onRemove={c.clear}>
1437
+ {c.label}
1438
+ </InputChip>
1439
+ ))}
1440
+ </div>
1441
+ )}
1442
+ {!appTab && tree && (
1443
+ <>
1444
+ <Group title={t.library.menu}>
1445
+ <TreeRows branches={tree.menu} depth={0} open={openBranches} toggle={toggleBranch} ring={scores ? ring : undefined} />
1446
+ </Group>
1447
+ {tree.rest.length > 0 && (
1448
+ <Group title={t.library.notInMenu}>
1449
+ <TreeRows
1450
+ branches={tree.rest.map((item) => ({ key: item.href, item, children: [] }))}
1451
+ depth={0}
1452
+ open={openBranches}
1453
+ toggle={toggleBranch}
1454
+ ring={scores ? ring : undefined}
1455
+ />
1456
+ </Group>
1457
+ )}
1458
+ </>
1459
+ )}
1460
+ {!appTab && !tree && fresh.length > 0 && collection.kind === 'folder' && (
1461
+ <Group title={collection.fresh!.heading}>{fresh.map(listRow)}</Group>
1462
+ )}
1463
+ {appTab || tree ? null : shown.length ? (
1464
+ groups.map((g) => (
1465
+ <Group
1466
+ key={g.key || 'all'}
1467
+ title={g.key ? (sort === 'type' || sort === 'az' || sort === 'za' ? g.key : dateName(g.key)) : undefined}
1468
+ >
1469
+ {g.items.map(listRow)}
1470
+ </Group>
1471
+ ))
1472
+ ) : (
1473
+ <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
1474
+ )}
1475
+ </Screen>
1476
+ );
1477
+ }
1478
+
1479
+ // What is under a row's name in a list (its supporting text; beside it on a computer, ListItem's `columns`): its status
1480
+ // and date (`children`), at the right on a computer, and there a page's or post's address before them (`address`).
1481
+ function Under({ item, address, children }: { item: Item; address?: boolean; children: ReactNode }) {
1482
+ return (
1483
+ <span className="flex gap-6">
1484
+ {address && <span className="hidden min-w-0 flex-1 truncate md:block">{item.path}</span>}
1485
+ <span className="min-w-0 truncate md:ml-auto md:shrink-0 md:text-right">{children}</span>
1486
+ </span>
1487
+ );
1488
+ }
1489
+
1490
+ // A page's or post's status (Item.status) before its date or time.
1491
+ const Status = ({ item }: { item: Item }) => (item.status ? <span className="mr-2 font-medium text-accent">{item.status}</span> : null);
1492
+
1493
+ // The time a page was last saved, under its name (a space until it has come, so rows do not jump).
1494
+ function SavedTime({ item }: { item: Item }) {
1495
+ return (
1496
+ <>
1497
+ <Status item={item} />
1498
+ {item.file && saved.has(item.file.path) ? capital(when(saved.get(item.file.path)!.date)) : '\u00a0'}
1499
+ </>
1500
+ );
1501
+ }
1502
+
1503
+ // The rows of the pages as the menu has them: each item, and under it, when it is opened with the
1504
+ // arrow at the right of its row, the items under it, indented a step. An item without a page is not a link.
1505
+ function TreeRows({
1506
+ branches,
1507
+ depth,
1508
+ open,
1509
+ toggle,
1510
+ ring,
1511
+ }: {
1512
+ branches: Branch[];
1513
+ depth: number;
1514
+ open: Set<string>;
1515
+ toggle: (href: string) => void;
1516
+ /** With "Vis SEO-vurdering": the score of a page at the start of its row (a tap on it opens the page at its rating). */
1517
+ ring?: (item: Item) => ReactNode;
1518
+ }) {
1519
+ return (
1520
+ <>
1521
+ {branches.map(({ key, item, children }) => {
1522
+ const shown = open.has(key);
1523
+ return (
1524
+ <Fragment key={key}>
1525
+ <ListItem
1526
+ href={item.href || undefined}
1527
+ tone={item.href ? undefined : 'muted'}
1528
+ // Indented a step under the item it is under, in the editor's segmented lists.
1529
+ style={depth ? { paddingInlineStart: `calc(var(--list-segmented-padding-inline, 1rem) + ${depth * 1.25}rem)` } : undefined}
1530
+ leading={ring?.(item)}
1531
+ headline={item.title}
1532
+ supporting={
1533
+ item.href ? (
1534
+ <Under item={item} address>
1535
+ <SavedTime item={item} />
1536
+ </Under>
1537
+ ) : (
1538
+ t.library.pageGone
1539
+ )
1540
+ }
1541
+ truncate
1542
+ columns
1543
+ actions={
1544
+ children.length > 0 && (
1545
+ <span className="shrink-0 pr-1.5">
1546
+ <UnfoldButton
1547
+ open={shown}
1548
+ count={children.length}
1549
+ label={shown ? t.library.hideSubpagesOf(item.title) : t.library.showSubpagesOf(item.title)}
1550
+ title={shown ? t.library.hideSubpages : t.library.showSubpages}
1551
+ onClick={() => toggle(key)}
1552
+ />
1553
+ </span>
1554
+ )
1555
+ }
1556
+ />
1557
+ {shown && <TreeRows branches={children} depth={depth + 1} open={open} toggle={toggle} ring={ring} />}
1558
+ </Fragment>
1559
+ );
1560
+ })}
1561
+ </>
1562
+ );
1563
+ }